diff --git a/packages/core/e2e/entity-hydrator.e2e-spec.ts b/packages/core/e2e/entity-hydrator.e2e-spec.ts index 7a596522d7..e6ff9034ab 100644 --- a/packages/core/e2e/entity-hydrator.e2e-spec.ts +++ b/packages/core/e2e/entity-hydrator.e2e-spec.ts @@ -8,6 +8,10 @@ import { ProductVariant, RequestContext, ActiveOrderService, + OrderService, + TransactionalConnection, + OrderLine, + RequestContextService, } from '@vendure/core'; import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; import gql from 'graphql-tag'; @@ -43,7 +47,7 @@ describe('Entity hydration', () => { await server.init({ initialData, productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'), - customerCount: 1, + customerCount: 2, }); await adminClient.asSuperAdmin(); }, TEST_SETUP_TIMEOUT_MS); @@ -290,6 +294,46 @@ describe('Entity hydration', () => { expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0); }); }); + + // https://github.com/vendure-ecommerce/vendure/issues/2546 + it('Preserves ordering when merging arrays of relations', async () => { + await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test'); + await shopClient.query(AddItemToOrderDocument, { + productVariantId: '1', + quantity: 1, + }); + const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, { + productVariantId: '2', + quantity: 2, + }); + orderResultGuard.assertSuccess(addItemToOrder); + const internalOrderId = +addItemToOrder.id.replace(/^\D+/g, ''); + const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' }); + const order = await server.app + .get(OrderService) + .findOne(ctx, internalOrderId, ['lines.productVariant']); + + for (const line of order?.lines ?? []) { + // Assert that things are as we expect before hydrating + expect(line.productVariantId).toBe(line.productVariant.id); + } + + // modify the first order line to make postgres tend to return the lines in the wrong order + await server.app + .get(TransactionalConnection) + .getRepository(ctx, OrderLine) + .update(order!.lines[0].id, { + sellerChannelId: 1, + }); + + await server.app.get(EntityHydrator).hydrate(ctx, order!, { + relations: ['lines.sellerChannel'], + }); + + for (const line of order?.lines ?? []) { + expect(line.productVariantId).toBe(line.productVariant.id); + } + }); }); function getVariantWithName(product: Product, name: string) { diff --git a/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts b/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts index 6316b64aba..7d2497a1c7 100644 --- a/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts +++ b/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts @@ -302,6 +302,27 @@ export class EntityHydrator { if (!a) { return b; } + if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.length > 1) { + if (a[0].hasOwnProperty('id')) { + // If the array contains entities, we can use the id to match them up + // so that we ensure that we don't merge properties from different entities + // with the same index. + const aIds = a.map(e => e.id); + const bIds = b.map(e => e.id); + if (JSON.stringify(aIds) !== JSON.stringify(bIds)) { + // The entities in the arrays are not in the same order, so we can't + // safely merge them. We need to sort the `b` array so that the entities + // are in the same order as the `a` array. + const idToIndexMap = new Map(); + a.forEach((item, index) => { + idToIndexMap.set(item.id, index); + }); + b.sort((_a, _b) => { + return idToIndexMap.get(_a.id) - idToIndexMap.get(_b.id); + }); + } + } + } for (const [key, value] of Object.entries(b)) { if (Object.getOwnPropertyDescriptor(b, key)?.writable) { if (Array.isArray(value)) {