entity-hydrator.e2e-spec.ts 16 KB

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