Browse Source

fix(core): Save relation custom fields in addItemToOrder mutation

Fixes #760
Michael Bromley 4 years ago
parent
commit
10d43e8bfe

+ 224 - 82
packages/core/e2e/shop-order.e2e-spec.ts

@@ -1,6 +1,6 @@
 /* tslint:disable:no-non-null-assertion */
 import { pick } from '@vendure/common/lib/pick';
-import { mergeConfig } from '@vendure/core';
+import { Asset, mergeConfig } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
@@ -73,6 +73,7 @@ import {
     SET_CUSTOMER,
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_METHOD,
+    TEST_ORDER_FRAGMENT,
     TRANSITION_TO_STATE,
     UPDATED_ORDER_FRAGMENT,
 } from './graphql/shop-definitions';
@@ -89,10 +90,14 @@ describe('Shop orders', () => {
                 ],
             },
             customFields: {
-                Order: [{ name: 'giftWrap', type: 'boolean', defaultValue: false }],
+                Order: [
+                    { name: 'giftWrap', type: 'boolean', defaultValue: false },
+                    { name: 'orderImage', type: 'relation', entity: Asset },
+                ],
                 OrderLine: [
                     { name: 'notes', type: 'string' },
                     { name: 'privateField', type: 'string', public: false },
+                    { name: 'lineImage', type: 'relation', entity: Asset },
                 ],
             },
             orderOptions: {
@@ -230,98 +235,226 @@ describe('Shop orders', () => {
             expect(addItemToOrder!.lines[0].quantity).toBe(3);
         });
 
-        it(
-            'addItemToOrder with private customFields errors',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<AddItemToOrder.Mutation>(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS, {
-                    productVariantId: 'T_2',
-                    quantity: 1,
-                    customFields: {
-                        privateField: 'oh no!',
+        describe('OrderLine customFields', () => {
+            const GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS = gql`
+                query {
+                    activeOrder {
+                        lines {
+                            id
+                            customFields {
+                                notes
+                                lineImage {
+                                    id
+                                }
+                            }
+                        }
+                    }
+                }
+            `;
+            it(
+                'addItemToOrder with private customFields errors',
+                assertThrowsWithMessage(async () => {
+                    await shopClient.query<AddItemToOrder.Mutation>(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS, {
+                        productVariantId: 'T_2',
+                        quantity: 1,
+                        customFields: {
+                            privateField: 'oh no!',
+                        },
+                    });
+                }, 'Variable "$customFields" got invalid value { privateField: "oh no!" }; Field "privateField" is not defined by type "OrderLineCustomFieldsInput".'),
+            );
+
+            it('addItemToOrder with equal customFields adds quantity to the existing OrderLine', async () => {
+                const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_2',
+                        quantity: 1,
+                        customFields: {
+                            notes: 'note1',
+                        },
                     },
-                });
-            }, 'Variable "$customFields" got invalid value { privateField: "oh no!" }; Field "privateField" is not defined by type "OrderLineCustomFieldsInput".'),
-        );
+                );
+                orderResultGuard.assertSuccess(add1);
+                expect(add1!.lines.length).toBe(2);
+                expect(add1!.lines[1].quantity).toBe(1);
 
-        it('addItemToOrder with equal customFields adds quantity to the existing OrderLine', async () => {
-            const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(
-                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
-                {
-                    productVariantId: 'T_2',
-                    quantity: 1,
-                    customFields: {
-                        notes: 'note1',
+                const { addItemToOrder: add2 } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_2',
+                        quantity: 1,
+                        customFields: {
+                            notes: 'note1',
+                        },
                     },
-                },
-            );
-            orderResultGuard.assertSuccess(add1);
-            expect(add1!.lines.length).toBe(2);
-            expect(add1!.lines[1].quantity).toBe(1);
+                );
+                orderResultGuard.assertSuccess(add2);
+                expect(add2!.lines.length).toBe(2);
+                expect(add2!.lines[1].quantity).toBe(2);
 
-            const { addItemToOrder: add2 } = await shopClient.query<AddItemToOrder.Mutation>(
-                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
-                {
-                    productVariantId: 'T_2',
-                    quantity: 1,
-                    customFields: {
-                        notes: 'note1',
+                await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                    REMOVE_ITEM_FROM_ORDER,
+                    {
+                        orderLineId: add2!.lines[1].id,
                     },
-                },
-            );
-            orderResultGuard.assertSuccess(add2);
-            expect(add2!.lines.length).toBe(2);
-            expect(add2!.lines[1].quantity).toBe(2);
+                );
+            });
 
-            await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
-                REMOVE_ITEM_FROM_ORDER,
-                {
-                    orderLineId: add2!.lines[1].id,
-                },
-            );
-        });
+            it('addItemToOrder with different customFields adds quantity to a new OrderLine', async () => {
+                const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            notes: 'note2',
+                        },
+                    },
+                );
+                orderResultGuard.assertSuccess(add1);
+                expect(add1!.lines.length).toBe(2);
+                expect(add1!.lines[1].quantity).toBe(1);
 
-        it('addItemToOrder with different customFields adds quantity to a new OrderLine', async () => {
-            const { addItemToOrder: add1 } = await shopClient.query<AddItemToOrder.Mutation>(
-                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
-                {
-                    productVariantId: 'T_3',
-                    quantity: 1,
-                    customFields: {
-                        notes: 'note2',
+                const { addItemToOrder: add2 } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            notes: 'note3',
+                        },
                     },
-                },
-            );
-            orderResultGuard.assertSuccess(add1);
-            expect(add1!.lines.length).toBe(2);
-            expect(add1!.lines[1].quantity).toBe(1);
+                );
+                orderResultGuard.assertSuccess(add2);
+                expect(add2!.lines.length).toBe(3);
+                expect(add2!.lines[1].quantity).toBe(1);
+                expect(add2!.lines[2].quantity).toBe(1);
 
-            const { addItemToOrder: add2 } = await shopClient.query<AddItemToOrder.Mutation>(
-                ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
-                {
-                    productVariantId: 'T_3',
+                await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                    REMOVE_ITEM_FROM_ORDER,
+                    {
+                        orderLineId: add2!.lines[1].id,
+                    },
+                );
+                await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                    REMOVE_ITEM_FROM_ORDER,
+                    {
+                        orderLineId: add2!.lines[2].id,
+                    },
+                );
+            });
+
+            it('addItemToOrder with relation customField', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImageId: 'T_1',
+                        },
+                    },
+                );
+
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.lines.length).toBe(2);
+                expect(addItemToOrder!.lines[1].quantity).toBe(1);
+
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+
+                expect(activeOrder.lines[1].customFields.lineImage).toEqual({ id: 'T_1' });
+            });
+
+            it('addItemToOrder with equal relation customField adds to quantity', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImageId: 'T_1',
+                        },
+                    },
+                );
+
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.lines.length).toBe(2);
+                expect(addItemToOrder!.lines[1].quantity).toBe(2);
+
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+
+                expect(activeOrder.lines[1].customFields.lineImage).toEqual({ id: 'T_1' });
+            });
+
+            it('addItemToOrder with different relation customField adds new line', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImageId: 'T_2',
+                        },
+                    },
+                );
+
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.lines.length).toBe(3);
+                expect(addItemToOrder!.lines[2].quantity).toBe(1);
+
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+
+                expect(activeOrder.lines[2].customFields.lineImage).toEqual({ id: 'T_2' });
+            });
+
+            it('adjustOrderLine updates relation reference', async () => {
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+
+                const ADJUST_ORDER_LINE_WITH_CUSTOM_FIELDS = gql`
+                    mutation($orderLineId: ID!, $quantity: Int!, $customFields: OrderLineCustomFieldsInput) {
+                        adjustOrderLine(
+                            orderLineId: $orderLineId
+                            quantity: $quantity
+                            customFields: $customFields
+                        ) {
+                            ... on Order {
+                                lines {
+                                    id
+                                    customFields {
+                                        notes
+                                        lineImage {
+                                            id
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                `;
+                const { adjustOrderLine } = await shopClient.query(ADJUST_ORDER_LINE_WITH_CUSTOM_FIELDS, {
+                    orderLineId: activeOrder.lines[2].id,
                     quantity: 1,
                     customFields: {
-                        notes: 'note3',
+                        lineImageId: 'T_1',
                     },
-                },
-            );
-            orderResultGuard.assertSuccess(add2);
-            expect(add2!.lines.length).toBe(3);
-            expect(add2!.lines[1].quantity).toBe(1);
-            expect(add2!.lines[2].quantity).toBe(1);
+                });
 
-            await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
-                REMOVE_ITEM_FROM_ORDER,
-                {
-                    orderLineId: add2!.lines[1].id,
-                },
-            );
-            await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
-                REMOVE_ITEM_FROM_ORDER,
-                {
-                    orderLineId: add2!.lines[2].id,
-                },
-            );
+                expect(adjustOrderLine.lines[2].customFields.lineImage).toEqual({ id: 'T_1' });
+
+                await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                    REMOVE_ITEM_FROM_ORDER,
+                    {
+                        orderLineId: activeOrder!.lines[2].id,
+                    },
+                );
+                await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
+                    REMOVE_ITEM_FROM_ORDER,
+                    {
+                        orderLineId: activeOrder!.lines[1].id,
+                    },
+                );
+            });
         });
 
         it('addItemToOrder errors when going beyond orderItemsLimit', async () => {
@@ -1428,6 +1561,7 @@ describe('Shop orders', () => {
             const { activeOrder } = await shopClient.query(GET_ORDER_CUSTOM_FIELDS);
 
             expect(activeOrder?.customFields).toEqual({
+                orderImage: null,
                 giftWrap: false,
             });
         });
@@ -1435,17 +1569,19 @@ describe('Shop orders', () => {
         it('setting order custom fields', async () => {
             const { setOrderCustomFields } = await shopClient.query(SET_ORDER_CUSTOM_FIELDS, {
                 input: {
-                    customFields: { giftWrap: true },
+                    customFields: { giftWrap: true, orderImageId: 'T_1' },
                 },
             });
 
             expect(setOrderCustomFields?.customFields).toEqual({
+                orderImage: { id: 'T_1' },
                 giftWrap: true,
             });
 
             const { activeOrder } = await shopClient.query(GET_ORDER_CUSTOM_FIELDS);
 
             expect(activeOrder?.customFields).toEqual({
+                orderImage: { id: 'T_1' },
                 giftWrap: true,
             });
         });
@@ -1481,6 +1617,9 @@ const GET_ORDER_CUSTOM_FIELDS = gql`
             id
             customFields {
                 giftWrap
+                orderImage {
+                    id
+                }
             }
         }
     }
@@ -1493,6 +1632,9 @@ const SET_ORDER_CUSTOM_FIELDS = gql`
                 id
                 customFields {
                     giftWrap
+                    orderImage {
+                        id
+                    }
                 }
             }
             ... on ErrorResult {

+ 49 - 15
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -32,6 +32,7 @@ import { PaymentService } from '../../services/payment.service';
 import { ProductVariantService } from '../../services/product-variant.service';
 import { StockMovementService } from '../../services/stock-movement.service';
 import { TransactionalConnection } from '../../transaction/transactional-connection';
+import { CustomFieldRelationService } from '../custom-field-relation/custom-field-relation.service';
 import { OrderCalculator } from '../order-calculator/order-calculator';
 import { patchEntity } from '../utils/patch-entity';
 import { translateDeep } from '../utils/translate-entity';
@@ -57,6 +58,7 @@ export class OrderModifier {
         private countryService: CountryService,
         private stockMovementService: StockMovementService,
         private productVariantService: ProductVariantService,
+        private customFieldRelationService: CustomFieldRelationService,
     ) {}
 
     /**
@@ -77,18 +79,20 @@ export class OrderModifier {
         return correctedQuantity;
     }
 
-    getExistingOrderLine(
+    async getExistingOrderLine(
         ctx: RequestContext,
         order: Order,
         productVariantId: ID,
         customFields?: { [key: string]: any },
-    ): OrderLine | undefined {
-        return order.lines.find(line => {
-            return (
+    ): Promise<OrderLine | undefined> {
+        for (const line of order.lines) {
+            const match =
                 idsAreEqual(line.productVariant.id, productVariantId) &&
-                this.customFieldsAreEqual(customFields, line.customFields)
-            );
-        });
+                (await this.customFieldsAreEqual(ctx, line, customFields, line.customFields));
+            if (match) {
+                return line;
+            }
+        }
     }
 
     /**
@@ -101,7 +105,7 @@ export class OrderModifier {
         productVariantId: ID,
         customFields?: { [key: string]: any },
     ) {
-        const existingOrderLine = this.getExistingOrderLine(ctx, order, productVariantId, customFields);
+        const existingOrderLine = await this.getExistingOrderLine(ctx, order, productVariantId, customFields);
         if (existingOrderLine) {
             return existingOrderLine;
         }
@@ -115,6 +119,7 @@ export class OrderModifier {
                 customFields,
             }),
         );
+        await this.customFieldRelationService.updateRelations(ctx, OrderLine, { customFields }, orderLine);
         const lineWithRelations = await this.connection.getEntityOrThrow(ctx, OrderLine, orderLine.id, {
             relations: [
                 'items',
@@ -447,20 +452,49 @@ export class OrderModifier {
         });
     }
 
-    private customFieldsAreEqual(
+    private async customFieldsAreEqual(
+        ctx: RequestContext,
+        orderLine: OrderLine,
         inputCustomFields: { [key: string]: any } | null | undefined,
         existingCustomFields?: { [key: string]: any },
-    ): boolean {
+    ): Promise<boolean> {
         if (inputCustomFields == null && typeof existingCustomFields === 'object') {
             // A null value for an OrderLine customFields input is the equivalent
             // of every property of an existing customFields object being null.
             return Object.values(existingCustomFields).every(v => v === null);
         }
-        for (const [key, value] of Object.entries(existingCustomFields || {})) {
-            const valuesMatch = JSON.stringify(inputCustomFields?.[key]) === JSON.stringify(value);
-            const undefinedMatchesNull = value === null && inputCustomFields?.[key] === undefined;
-            if (!valuesMatch && !undefinedMatchesNull) {
-                return false;
+        const customFieldDefs = this.configService.customFields.OrderLine;
+
+        const customFieldRelations = customFieldDefs.filter(d => d.type === 'relation');
+        let lineWithCustomFieldRelations: OrderLine | undefined;
+        if (customFieldRelations.length) {
+            // for relation types, we need to actually query the DB and check if there is an
+            // existing entity assigned.
+            lineWithCustomFieldRelations = await this.connection
+                .getRepository(ctx, OrderLine)
+                .findOne(orderLine.id, {
+                    relations: customFieldRelations.map(r => `customFields.${r.name}`),
+                });
+        }
+
+        for (const def of customFieldDefs) {
+            const key = def.name;
+            const existingValue = existingCustomFields?.[key];
+            if (existingValue) {
+                const valuesMatch =
+                    JSON.stringify(inputCustomFields?.[key]) === JSON.stringify(existingValue);
+                const undefinedMatchesNull = existingValue === null && inputCustomFields?.[key] === undefined;
+                if (!valuesMatch && !undefinedMatchesNull) {
+                    return false;
+                }
+            } else if (def.type === 'relation') {
+                const inputId = `${key}Id`;
+                const inputValue = inputCustomFields?.[inputId];
+                // tslint:disable-next-line:no-non-null-assertion
+                const existingRelation = (lineWithCustomFieldRelations!.customFields as any)[key];
+                if (inputValue && inputValue !== existingRelation?.id) {
+                    return false;
+                }
             }
         }
         return true;

+ 7 - 1
packages/core/src/service/services/order.service.ts

@@ -363,7 +363,7 @@ export class OrderService {
         customFields?: { [key: string]: any },
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
-        const existingOrderLine = this.orderModifier.getExistingOrderLine(
+        const existingOrderLine = await this.orderModifier.getExistingOrderLine(
             ctx,
             order,
             productVariantId,
@@ -425,6 +425,12 @@ export class OrderService {
         }
         if (customFields != null) {
             orderLine.customFields = customFields;
+            await this.customFieldRelationService.updateRelations(
+                ctx,
+                OrderLine,
+                { customFields },
+                orderLine,
+            );
         }
         const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
             ctx,