entity-hydrator.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. Asset,
  4. ChannelService,
  5. EntityHydrator,
  6. mergeConfig,
  7. Order,
  8. Product,
  9. ProductVariant,
  10. RequestContext,
  11. ActiveOrderService,
  12. OrderService,
  13. TransactionalConnection,
  14. OrderLine,
  15. RequestContextService,
  16. } from '@vendure/core';
  17. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  18. import gql from 'graphql-tag';
  19. import path from 'path';
  20. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  21. import { initialData } from '../../../e2e-common/e2e-initial-data';
  22. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  23. import { AdditionalConfig, HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin';
  24. import { UpdateChannelMutation, UpdateChannelMutationVariables } from './graphql/generated-e2e-admin-types';
  25. import {
  26. AddItemToOrderDocument,
  27. AddItemToOrderMutation,
  28. AddItemToOrderMutationVariables,
  29. UpdatedOrderFragment,
  30. } from './graphql/generated-e2e-shop-types';
  31. import { UPDATE_CHANNEL } from './graphql/shared-definitions';
  32. import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
  33. const orderResultGuard: ErrorResultGuard<UpdatedOrderFragment> = createErrorResultGuard(
  34. input => !!input.lines,
  35. );
  36. describe('Entity hydration', () => {
  37. const { server, adminClient, shopClient } = createTestEnvironment(
  38. mergeConfig(testConfig(), {
  39. plugins: [HydrationTestPlugin],
  40. }),
  41. );
  42. beforeAll(async () => {
  43. await server.init({
  44. initialData,
  45. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  46. customerCount: 2,
  47. });
  48. await adminClient.asSuperAdmin();
  49. const connection = server.app.get(TransactionalConnection).rawConnection;
  50. const asset = await connection.getRepository(Asset).findOne({ where: {} });
  51. await connection.getRepository(AdditionalConfig).save(
  52. new AdditionalConfig({
  53. backgroundImage: asset,
  54. }),
  55. );
  56. }, TEST_SETUP_TIMEOUT_MS);
  57. afterAll(async () => {
  58. await server.destroy();
  59. });
  60. it('includes existing relations', async () => {
  61. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  62. id: 'T_1',
  63. });
  64. expect(hydrateProduct.facetValues).toBeDefined();
  65. expect(hydrateProduct.facetValues.length).toBe(2);
  66. });
  67. it('hydrates top-level single relation', async () => {
  68. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  69. id: 'T_1',
  70. });
  71. expect(hydrateProduct.featuredAsset.name).toBe('derick-david-409858-unsplash.jpg');
  72. });
  73. it('hydrates top-level array relation', async () => {
  74. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  75. id: 'T_1',
  76. });
  77. expect(hydrateProduct.assets.length).toBe(1);
  78. expect(hydrateProduct.assets[0].asset.name).toBe('derick-david-409858-unsplash.jpg');
  79. });
  80. it('hydrates nested single relation', async () => {
  81. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  82. id: 'T_1',
  83. });
  84. expect(hydrateProduct.variants[0].product.id).toBe('T_1');
  85. });
  86. it('hydrates nested array relation', async () => {
  87. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  88. id: 'T_1',
  89. });
  90. expect(hydrateProduct.variants[0].options.length).toBe(2);
  91. });
  92. it('translates top-level translatable', async () => {
  93. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  94. id: 'T_1',
  95. });
  96. expect(hydrateProduct.variants.map(v => v.name).sort()).toEqual([
  97. 'Laptop 13 inch 16GB',
  98. 'Laptop 13 inch 8GB',
  99. 'Laptop 15 inch 16GB',
  100. 'Laptop 15 inch 8GB',
  101. ]);
  102. });
  103. it('translates nested translatable', async () => {
  104. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  105. id: 'T_1',
  106. });
  107. expect(
  108. getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB')
  109. .options.map(o => o.name)
  110. .sort(),
  111. ).toEqual(['13 inch', '8GB']);
  112. });
  113. it('translates nested translatable 2', async () => {
  114. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  115. id: 'T_1',
  116. });
  117. expect(hydrateProduct.assets[0].product.name).toBe('Laptop');
  118. });
  119. it('populates ProductVariant price data', async () => {
  120. const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, {
  121. id: 'T_1',
  122. });
  123. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB').price).toBe(129900);
  124. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB').priceWithTax).toBe(155880);
  125. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 16GB').price).toBe(219900);
  126. expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 16GB').priceWithTax).toBe(263880);
  127. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 8GB').price).toBe(139900);
  128. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 8GB').priceWithTax).toBe(167880);
  129. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 16GB').price).toBe(229900);
  130. expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 16GB').priceWithTax).toBe(275880);
  131. });
  132. // https://github.com/vendure-ecommerce/vendure/issues/1153
  133. it('correctly handles empty array relations', async () => {
  134. // Product T_5 has no asset defined
  135. const { hydrateProductAsset } = await adminClient.query<{ hydrateProductAsset: Product }>(
  136. GET_HYDRATED_PRODUCT_ASSET,
  137. {
  138. id: 'T_5',
  139. },
  140. );
  141. expect(hydrateProductAsset.assets).toEqual([]);
  142. });
  143. // https://github.com/vendure-ecommerce/vendure/issues/1324
  144. it('correctly handles empty nested array relations', async () => {
  145. const { hydrateProductWithNoFacets } = await adminClient.query<{
  146. hydrateProductWithNoFacets: Product;
  147. }>(GET_HYDRATED_PRODUCT_NO_FACETS);
  148. expect(hydrateProductWithNoFacets.facetValues).toEqual([]);
  149. });
  150. // https://github.com/vendure-ecommerce/vendure/issues/1161
  151. it('correctly expands missing relations', async () => {
  152. const { hydrateProductVariant } = await adminClient.query<{ hydrateProductVariant: ProductVariant }>(
  153. GET_HYDRATED_VARIANT,
  154. { id: 'T_1' },
  155. );
  156. expect(hydrateProductVariant.product.id).toBe('T_1');
  157. expect(hydrateProductVariant.product.facetValues.map(fv => fv.id).sort()).toEqual(['T_1', 'T_2']);
  158. });
  159. // https://github.com/vendure-ecommerce/vendure/issues/1172
  160. it('can hydrate entity with getters (Order)', async () => {
  161. const { addItemToOrder } = await shopClient.query<
  162. AddItemToOrderMutation,
  163. AddItemToOrderMutationVariables
  164. >(ADD_ITEM_TO_ORDER, {
  165. productVariantId: 'T_1',
  166. quantity: 1,
  167. });
  168. orderResultGuard.assertSuccess(addItemToOrder);
  169. const { hydrateOrder } = await adminClient.query<{ hydrateOrder: Order }>(GET_HYDRATED_ORDER, {
  170. id: addItemToOrder.id,
  171. });
  172. expect(hydrateOrder.id).toBe('T_1');
  173. expect(hydrateOrder.payments).toEqual([]);
  174. });
  175. // https://github.com/vendure-ecommerce/vendure/issues/1229
  176. it('deep merges existing properties', async () => {
  177. await shopClient.asAnonymousUser();
  178. const { addItemToOrder } = await shopClient.query<
  179. AddItemToOrderMutation,
  180. AddItemToOrderMutationVariables
  181. >(ADD_ITEM_TO_ORDER, {
  182. productVariantId: 'T_1',
  183. quantity: 2,
  184. });
  185. orderResultGuard.assertSuccess(addItemToOrder);
  186. const { hydrateOrderReturnQuantities } = await adminClient.query<{
  187. hydrateOrderReturnQuantities: number[];
  188. }>(GET_HYDRATED_ORDER_QUANTITIES, {
  189. id: addItemToOrder.id,
  190. });
  191. expect(hydrateOrderReturnQuantities).toEqual([2]);
  192. });
  193. // https://github.com/vendure-ecommerce/vendure/issues/1284
  194. it('hydrates custom field relations', async () => {
  195. await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(UPDATE_CHANNEL, {
  196. input: {
  197. id: 'T_1',
  198. customFields: {
  199. thumbId: 'T_2',
  200. },
  201. },
  202. });
  203. const { hydrateChannel } = await adminClient.query<{
  204. hydrateChannel: any;
  205. }>(GET_HYDRATED_CHANNEL, {
  206. id: 'T_1',
  207. });
  208. expect(hydrateChannel.customFields.thumb).toBeDefined();
  209. expect(hydrateChannel.customFields.thumb.id).toBe('T_2');
  210. });
  211. it('hydrates a nested custom field', async () => {
  212. await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(UPDATE_CHANNEL, {
  213. input: {
  214. id: 'T_1',
  215. customFields: {
  216. additionalConfigId: 'T_1',
  217. },
  218. },
  219. });
  220. const { hydrateChannelWithNestedRelation } = await adminClient.query<{
  221. hydrateChannelWithNestedRelation: any;
  222. }>(GET_HYDRATED_CHANNEL_NESTED, {
  223. id: 'T_1',
  224. });
  225. expect(hydrateChannelWithNestedRelation.customFields.additionalConfig).toBeDefined();
  226. });
  227. // https://github.com/vendure-ecommerce/vendure/issues/2682
  228. it('hydrates a nested custom field where the first level is null', async () => {
  229. await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(UPDATE_CHANNEL, {
  230. input: {
  231. id: 'T_1',
  232. customFields: {
  233. additionalConfigId: null,
  234. },
  235. },
  236. });
  237. const { hydrateChannelWithNestedRelation } = await adminClient.query<{
  238. hydrateChannelWithNestedRelation: any;
  239. }>(GET_HYDRATED_CHANNEL_NESTED, {
  240. id: 'T_1',
  241. });
  242. expect(hydrateChannelWithNestedRelation.customFields.additionalConfig).toBeNull();
  243. });
  244. // https://github.com/vendure-ecommerce/vendure/issues/2013
  245. describe('hydration of OrderLine ProductVariantPrices', () => {
  246. let order: Order | undefined;
  247. it('Create order with 3 items', async () => {
  248. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  249. await shopClient.query(AddItemToOrderDocument, {
  250. productVariantId: '1',
  251. quantity: 1,
  252. });
  253. await shopClient.query(AddItemToOrderDocument, {
  254. productVariantId: '2',
  255. quantity: 1,
  256. });
  257. const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, {
  258. productVariantId: '3',
  259. quantity: 1,
  260. });
  261. orderResultGuard.assertSuccess(addItemToOrder);
  262. const channel = await server.app.get(ChannelService).getDefaultChannel();
  263. // This is ugly, but in our real life example we use a CTX constructed by Vendure
  264. const internalOrderId = +addItemToOrder.id.replace(/^\D+/g, '');
  265. const ctx = new RequestContext({
  266. channel,
  267. authorizedAsOwnerOnly: true,
  268. apiType: 'shop',
  269. isAuthorized: true,
  270. session: {
  271. activeOrderId: internalOrderId,
  272. activeChannelId: 1,
  273. user: {
  274. id: 2,
  275. },
  276. } as any,
  277. });
  278. order = await server.app.get(ActiveOrderService).getActiveOrder(ctx, undefined);
  279. await server.app.get(EntityHydrator).hydrate(ctx, order!, {
  280. relations: ['lines.productVariant'],
  281. applyProductVariantPrices: true,
  282. });
  283. });
  284. it('Variant of orderLine 1 has a price', async () => {
  285. expect(order!.lines[0].productVariant.priceWithTax).toBeGreaterThan(0);
  286. });
  287. it('Variant of orderLine 2 has a price', async () => {
  288. expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
  289. });
  290. it('Variant of orderLine 3 has a price', async () => {
  291. expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
  292. });
  293. });
  294. // https://github.com/vendure-ecommerce/vendure/issues/2546
  295. it('Preserves ordering when merging arrays of relations', async () => {
  296. await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
  297. await shopClient.query(AddItemToOrderDocument, {
  298. productVariantId: '1',
  299. quantity: 1,
  300. });
  301. const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, {
  302. productVariantId: '2',
  303. quantity: 2,
  304. });
  305. orderResultGuard.assertSuccess(addItemToOrder);
  306. const internalOrderId = +addItemToOrder.id.replace(/^\D+/g, '');
  307. const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' });
  308. const order = await server.app
  309. .get(OrderService)
  310. .findOne(ctx, internalOrderId, ['lines.productVariant']);
  311. for (const line of order?.lines ?? []) {
  312. // Assert that things are as we expect before hydrating
  313. expect(line.productVariantId).toBe(line.productVariant.id);
  314. }
  315. // modify the first order line to make postgres tend to return the lines in the wrong order
  316. await server.app
  317. .get(TransactionalConnection)
  318. .getRepository(ctx, OrderLine)
  319. .update(order!.lines[0].id, {
  320. sellerChannelId: 1,
  321. });
  322. await server.app.get(EntityHydrator).hydrate(ctx, order!, {
  323. relations: ['lines.sellerChannel'],
  324. });
  325. for (const line of order?.lines ?? []) {
  326. expect(line.productVariantId).toBe(line.productVariant.id);
  327. }
  328. });
  329. });
  330. function getVariantWithName(product: Product, name: string) {
  331. return product.variants.find(v => v.name === name)!;
  332. }
  333. type HydrateProductQuery = { hydrateProduct: Product };
  334. const GET_HYDRATED_PRODUCT = gql`
  335. query GetHydratedProduct($id: ID!) {
  336. hydrateProduct(id: $id)
  337. }
  338. `;
  339. const GET_HYDRATED_PRODUCT_NO_FACETS = gql`
  340. query GetHydratedProductWithNoFacets {
  341. hydrateProductWithNoFacets
  342. }
  343. `;
  344. const GET_HYDRATED_PRODUCT_ASSET = gql`
  345. query GetHydratedProductAsset($id: ID!) {
  346. hydrateProductAsset(id: $id)
  347. }
  348. `;
  349. const GET_HYDRATED_VARIANT = gql`
  350. query GetHydratedVariant($id: ID!) {
  351. hydrateProductVariant(id: $id)
  352. }
  353. `;
  354. const GET_HYDRATED_ORDER = gql`
  355. query GetHydratedOrder($id: ID!) {
  356. hydrateOrder(id: $id)
  357. }
  358. `;
  359. const GET_HYDRATED_ORDER_QUANTITIES = gql`
  360. query GetHydratedOrderQuantities($id: ID!) {
  361. hydrateOrderReturnQuantities(id: $id)
  362. }
  363. `;
  364. const GET_HYDRATED_CHANNEL = gql`
  365. query GetHydratedChannel($id: ID!) {
  366. hydrateChannel(id: $id)
  367. }
  368. `;
  369. const GET_HYDRATED_CHANNEL_NESTED = gql`
  370. query GetHydratedChannelNested($id: ID!) {
  371. hydrateChannelWithNestedRelation(id: $id)
  372. }
  373. `;