entity-hydrator.e2e-spec.ts 17 KB

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