entity-hydrator.e2e-spec.ts 14 KB

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