Sfoglia il codice sorgente

fix(core): Correctly deep-merge hydrated entities

Fixes #1229
Michael Bromley 4 anni fa
parent
commit
32d19e3962

+ 26 - 0
packages/core/e2e/entity-hydrator.e2e-spec.ts

@@ -167,6 +167,27 @@ describe('Entity hydration', () => {
         expect(hydrateOrder.id).toBe('T_1');
         expect(hydrateOrder.payments).toEqual([]);
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/1229
+    it('deep merges existing properties', async () => {
+        await shopClient.asAnonymousUser();
+        const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
+            ADD_ITEM_TO_ORDER,
+            {
+                productVariantId: 'T_1',
+                quantity: 2,
+            },
+        );
+        orderResultGuard.assertSuccess(addItemToOrder);
+
+        const { hydrateOrderReturnQuantities } = await adminClient.query<{
+            hydrateOrderReturnQuantities: number[];
+        }>(GET_HYDRATED_ORDER_QUANTITIES, {
+            id: addItemToOrder.id,
+        });
+
+        expect(hydrateOrderReturnQuantities).toEqual([2]);
+    });
 });
 
 function getVariantWithName(product: Product, name: string) {
@@ -195,3 +216,8 @@ const GET_HYDRATED_ORDER = gql`
         hydrateOrder(id: $id)
     }
 `;
+const GET_HYDRATED_ORDER_QUANTITIES = gql`
+    query GetHydratedOrderQuantities($id: ID!) {
+        hydrateOrderReturnQuantities(id: $id)
+    }
+`;

+ 16 - 0
packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts

@@ -73,6 +73,21 @@ export class TestAdminPluginResolver {
         });
         return order;
     }
+
+    // Test case for https://github.com/vendure-ecommerce/vendure/issues/1229
+    @Query()
+    async hydrateOrderReturnQuantities(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
+        const order = await this.orderService.findOne(ctx, args.id);
+        await this.entityHydrator.hydrate(ctx, order!, {
+            relations: [
+                'lines',
+                'lines.productVariant',
+                'lines.productVariant.product',
+                'lines.productVariant.product.assets',
+            ],
+        });
+        return order?.lines.map(line => line.quantity);
+    }
 }
 
 @VendurePlugin({
@@ -85,6 +100,7 @@ export class TestAdminPluginResolver {
                 hydrateProductAsset(id: ID!): JSON
                 hydrateProductVariant(id: ID!): JSON
                 hydrateOrder(id: ID!): JSON
+                hydrateOrderReturnQuantities(id: ID!): JSON
             }
         `,
     },

+ 28 - 1
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -1,5 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { Type } from '@vendure/common/lib/shared-types';
+import { isObject } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
 
 import { RequestContext } from '../../../api/common/request-context';
@@ -89,7 +90,7 @@ export class EntityHydrator {
                     });
                 const propertiesToAdd = unique(missingRelations.map(relation => relation.split('.')[0]));
                 for (const prop of propertiesToAdd) {
-                    (target as any)[prop] = (hydrated as any)[prop];
+                    (target as any)[prop] = this.mergeDeep((target as any)[prop], (hydrated as any)[prop]);
                 }
 
                 const relationsWithEntities = missingRelations.map(relation => ({
@@ -241,4 +242,30 @@ export class EntityHydrator {
             ? input[0]?.hasOwnProperty('translations') ?? false
             : input?.hasOwnProperty('translations') ?? false;
     }
+
+    /**
+     * Merges properties into a target entity. This is needed for the cases in which a
+     * property already exists on the target, but the hydrated version also contains that
+     * property with a different set of properties. This prevents the original target
+     * entity from having data overwritten.
+     */
+    private mergeDeep<T extends { [key: string]: any }>(a: T | undefined, b: T): T {
+        if (!a) {
+            return b;
+        }
+        for (const [key, value] of Object.entries(b)) {
+            if (Object.getOwnPropertyDescriptor(b, key)?.writable) {
+                if (Array.isArray(value)) {
+                    (a as any)[key] = value.map((v, index) =>
+                        this.mergeDeep(a?.[key]?.[index], b[key][index]),
+                    );
+                } else if (isObject(value)) {
+                    (a as any)[key] = this.mergeDeep(a?.[key], b[key]);
+                } else {
+                    (a as any)[key] = b[key];
+                }
+            }
+        }
+        return a ?? b;
+    }
 }