Explorar el Código

Merge branch 'master' into minor

Michael Bromley hace 3 años
padre
commit
fc9a2e5027
Se han modificado 31 ficheros con 298 adiciones y 143 borrados
  1. 13 0
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 3 3
      packages/admin-ui-plugin/package.json
  4. 2 2
      packages/admin-ui/package.json
  5. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  6. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html
  7. 14 12
      packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts
  8. 2 2
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.html
  9. 17 4
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.ts
  10. 5 22
      packages/admin-ui/src/lib/core/src/shared/pipes/custom-field-label.pipe.ts
  11. 3 3
      packages/asset-server-plugin/package.json
  12. 1 1
      packages/common/package.json
  13. 0 2
      packages/core/e2e/fixtures/test-plugins/with-config.ts
  14. 47 0
      packages/core/e2e/order-promotion.e2e-spec.ts
  15. 0 22
      packages/core/e2e/plugin.e2e-spec.ts
  16. 96 3
      packages/core/e2e/shop-order.e2e-spec.ts
  17. 2 2
      packages/core/package.json
  18. 0 12
      packages/core/src/bootstrap.ts
  19. 7 0
      packages/core/src/config/config-helpers.ts
  20. 9 0
      packages/core/src/config/config.module.ts
  21. 5 2
      packages/core/src/config/order/order-item-price-calculation-strategy.ts
  22. 30 14
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  23. 6 1
      packages/core/src/service/services/promotion.service.ts
  24. 3 3
      packages/create/package.json
  25. 9 9
      packages/dev-server/package.json
  26. 3 3
      packages/elasticsearch-plugin/package.json
  27. 3 3
      packages/email-plugin/package.json
  28. 3 3
      packages/job-queue-plugin/package.json
  29. 4 4
      packages/payments-plugin/package.json
  30. 3 3
      packages/testing/package.json
  31. 4 4
      packages/ui-devkit/package.json

+ 13 - 0
CHANGELOG.md

@@ -1,3 +1,16 @@
+## <small>1.6.5 (2022-08-15)</small>
+
+
+#### Fixes
+
+* **admin-ui** Correctly display translatable custom field labels ([43b7766](https://github.com/vendure-ecommerce/vendure/commit/43b7766))
+* **core** Fix find product by slug with relations (#1709) ([9aac2f5](https://github.com/vendure-ecommerce/vendure/commit/9aac2f5)), closes [#1709](https://github.com/vendure-ecommerce/vendure/issues/1709)
+* **core** Fix ListQueryBuilder language handling logic ([86ac107](https://github.com/vendure-ecommerce/vendure/commit/86ac107)), closes [#1631](https://github.com/vendure-ecommerce/vendure/issues/1631) [#1611](https://github.com/vendure-ecommerce/vendure/issues/1611)
+* **core** Fix orderLine customField equality ([214281e](https://github.com/vendure-ecommerce/vendure/commit/214281e))
+* **core** Fix OrderLine relation customfields ([b3cb9f2](https://github.com/vendure-ecommerce/vendure/commit/b3cb9f2))
+* **core** Reset activeConfig on app shutdown ([8b8e310](https://github.com/vendure-ecommerce/vendure/commit/8b8e310))
+* **core** Take channels into account when validating coupon codes ([4ff8dff](https://github.com/vendure-ecommerce/vendure/commit/4ff8dff)), closes [#1692](https://github.com/vendure-ecommerce/vendure/issues/1692)
+
 ## <small>1.6.4 (2022-07-21)</small>
 
 

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
     "packages/*"
   ],
-  "version": "1.6.4",
+  "version": "1.6.5",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "command": {

+ 3 - 3
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -21,8 +21,8 @@
   "devDependencies": {
     "@types/express": "^4.17.8",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.6.4",
-    "@vendure/core": "^1.6.4",
+    "@vendure/common": "^1.6.5",
+    "@vendure/core": "^1.6.5",
     "express": "^4.17.1",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -39,7 +39,7 @@
     "@ng-select/ng-select": "^7.2.0",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
-    "@vendure/common": "^1.6.4",
+    "@vendure/common": "^1.6.5",
     "@webcomponents/custom-elements": "^1.4.3",
     "apollo-angular": "^2.6.0",
     "apollo-upload-client": "^16.0.0",

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '1.6.4';
+export const ADMIN_UI_VERSION = '1.6.5';

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html

@@ -1,12 +1,12 @@
 <div class="clr-form-control" *ngIf="compact">
-    <label for="basic" class="clr-control-label">{{ customField | customFieldLabel }}</label>
+    <label for="basic" class="clr-control-label">{{ customField | customFieldLabel:(uiLanguage$ | async) }}</label>
     <div class="clr-control-container">
         <div class="clr-input-wrapper">
             <ng-container *ngTemplateOutlet="inputs"></ng-container>
         </div>
     </div>
 </div>
-<vdr-form-field [label]="customField | customFieldLabel" [for]="customField.name" *ngIf="!compact">
+<vdr-form-field [label]="customField | customFieldLabel:(uiLanguage$ | async)" [for]="customField.name" *ngIf="!compact">
     <ng-container *ngTemplateOutlet="inputs"></ng-container>
 </vdr-form-field>
 

+ 14 - 12
packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts

@@ -1,16 +1,10 @@
-import {
-    AfterViewInit,
-    Component,
-    ComponentFactory,
-    Input,
-    OnInit,
-    ViewChild,
-    ViewContainerRef,
-} from '@angular/core';
-import { FormControl, FormGroup } from '@angular/forms';
+import { Component, ComponentFactory, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
 
 import { InputComponentConfig } from '../../../common/component-registry-types';
-import { CustomFieldConfig, CustomFieldsFragment } from '../../../common/generated-types';
+import { CustomFieldConfig, CustomFieldsFragment, LanguageCode } from '../../../common/generated-types';
 import { DataService } from '../../../data/providers/data.service';
 import {
     CustomFieldComponentService,
@@ -27,7 +21,7 @@ import {
     templateUrl: './custom-field-control.component.html',
     styleUrls: ['./custom-field-control.component.scss'],
 })
-export class CustomFieldControlComponent {
+export class CustomFieldControlComponent implements OnInit {
     @Input() entityName: CustomFieldEntityName;
     @Input('customFieldsFormGroup') formGroup: FormGroup;
     @Input() customField: CustomFieldsFragment;
@@ -37,13 +31,21 @@ export class CustomFieldControlComponent {
     hasCustomControl = false;
     @ViewChild('customComponentPlaceholder', { read: ViewContainerRef })
     private customComponentPlaceholder: ViewContainerRef;
+
     private customComponentFactory: ComponentFactory<CustomFieldControl> | undefined;
+    uiLanguage$: Observable<LanguageCode>;
 
     constructor(
         private dataService: DataService,
         private customFieldComponentService: CustomFieldComponentService,
     ) {}
 
+    ngOnInit() {
+        this.uiLanguage$ = this.dataService.client
+            .uiState()
+            .stream$.pipe(map(({ uiState }) => uiState.language));
+    }
+
     getFieldDefinition(): CustomFieldConfig & { ui?: InputComponentConfig } {
         const config: CustomFieldsFragment & { ui?: InputComponentConfig } = {
             ...this.customField,

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.html

@@ -1,6 +1,6 @@
 <select clrSelect [formControl]="formControl" [vdrDisabled]="readonly">
     <option *ngIf="config.nullable" [ngValue]="null"></option>
-    <option *ngFor="let option of options" [ngValue]="option.value">
-        {{ (option | customFieldLabel) || option.label || option.value }}
+    <option *ngFor="let option of options;trackBy:trackByFn" [ngValue]="option.value">
+        {{ (option | customFieldLabel:(uiLanguage$ | async)) || option.label || option.value }}
     </option>
 </select>

+ 17 - 4
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.ts

@@ -1,9 +1,11 @@
-import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
+import { Observable } from 'rxjs';
 
-import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
-import { CustomFieldConfigFragment } from '../../../common/generated-types';
+import { FormInputComponent } from '../../../common/component-registry-types';
+import { CustomFieldConfigFragment, LanguageCode } from '../../../common/generated-types';
+import { DataService } from '../../../data/providers/data.service';
 
 /**
  * @description
@@ -19,13 +21,24 @@ import { CustomFieldConfigFragment } from '../../../common/generated-types';
     styleUrls: ['./select-form-input.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class SelectFormInputComponent implements FormInputComponent {
+export class SelectFormInputComponent implements FormInputComponent, OnInit {
     static readonly id: DefaultFormComponentId = 'select-form-input';
     @Input() readonly: boolean;
     formControl: FormControl;
     config: DefaultFormComponentConfig<'select-form-input'> & CustomFieldConfigFragment;
+    uiLanguage$: Observable<LanguageCode>;
 
     get options() {
         return this.config.ui?.options || this.config.options;
     }
+
+    constructor(private dataService: DataService) {}
+
+    ngOnInit() {
+        this.uiLanguage$ = this.dataService.client.uiState().mapStream(({ uiState }) => uiState.language);
+    }
+
+    trackByFn(index: number, item: any) {
+        return item.value;
+    }
 }

+ 5 - 22
packages/admin-ui/src/lib/core/src/shared/pipes/custom-field-label.pipe.ts

@@ -1,8 +1,6 @@
-import { OnDestroy, Pipe, PipeTransform } from '@angular/core';
-import { Subscription } from 'rxjs';
+import { Pipe, PipeTransform } from '@angular/core';
 
 import { CustomFieldConfig, LanguageCode, StringFieldOption } from '../../common/generated-types';
-import { DataService } from '../../data/providers/data.service';
 
 /**
  * Displays a localized label for a CustomField or StringFieldOption, falling back to the
@@ -10,38 +8,23 @@ import { DataService } from '../../data/providers/data.service';
  */
 @Pipe({
     name: 'customFieldLabel',
-    pure: false,
+    pure: true,
 })
-export class CustomFieldLabelPipe implements PipeTransform, OnDestroy {
-    private readonly subscription: Subscription;
-    private uiLanguageCode: LanguageCode;
-
-    constructor(private dataService: DataService) {
-        this.subscription = dataService.client.uiState().stream$.subscribe(val => {
-            this.uiLanguageCode = val.uiState.language;
-        });
-    }
-
-    transform(value: CustomFieldConfig | StringFieldOption): string {
+export class CustomFieldLabelPipe implements PipeTransform {
+    transform(value: CustomFieldConfig | StringFieldOption, uiLanguageCode: LanguageCode | null): string {
         if (!value) {
             return value;
         }
         const { label } = value;
         const name = this.isCustomFieldConfig(value) ? value.name : value.value;
         if (label) {
-            const match = label.find(l => l.languageCode === this.uiLanguageCode);
+            const match = label.find(l => l.languageCode === uiLanguageCode);
             return match ? match.value : label[0].value;
         } else {
             return name;
         }
     }
 
-    ngOnDestroy(): void {
-        if (this.subscription) {
-            this.subscription.unsubscribe();
-        }
-    }
-
     private isCustomFieldConfig(input: any): input is CustomFieldConfig {
         return input.hasOwnProperty('name');
     }

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -24,8 +24,8 @@
     "@types/fs-extra": "^9.0.8",
     "@types/node-fetch": "^2.5.8",
     "@types/sharp": "^0.30.4",
-    "@vendure/common": "^1.6.4",
-    "@vendure/core": "^1.6.4",
+    "@vendure/common": "^1.6.5",
+    "@vendure/core": "^1.6.5",
     "aws-sdk": "^2.856.0",
     "express": "^4.17.1",
     "node-fetch": "^2.6.1",

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/common",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 0 - 2
packages/core/e2e/fixtures/test-plugins/with-config.ts

@@ -6,12 +6,10 @@ import { ConfigModule, VendurePlugin } from '@vendure/core';
     configuration: config => {
         // tslint:disable-next-line:no-non-null-assertion
         config.defaultLanguageCode = LanguageCode.zh;
-        TestPluginWithConfig.configSpy();
         return config;
     },
 })
 export class TestPluginWithConfig {
-    static configSpy = jest.fn();
     static setup() {
         return TestPluginWithConfig;
     }

+ 47 - 0
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -340,6 +340,53 @@ describe('Promotions applied to Orders', () => {
                 },
             ]);
         });
+
+        describe('coupon codes in other channels', () => {
+            const OTHER_CHANNEL_TOKEN = 'other-channel';
+            const OTHER_CHANNEL_COUPON_CODE = 'OTHER_CHANNEL_CODE';
+
+            beforeAll(async () => {
+                const { createChannel } = await adminClient.query<
+                    CreateChannel.Mutation,
+                    CreateChannel.Variables
+                >(CREATE_CHANNEL, {
+                    input: {
+                        code: 'other-channel',
+                        currencyCode: CurrencyCode.GBP,
+                        pricesIncludeTax: false,
+                        defaultTaxZoneId: 'T_1',
+                        defaultShippingZoneId: 'T_1',
+                        defaultLanguageCode: LanguageCode.en,
+                        token: OTHER_CHANNEL_TOKEN,
+                    },
+                });
+
+                await createPromotion({
+                    enabled: true,
+                    name: 'Other Channel Promo',
+                    couponCode: OTHER_CHANNEL_COUPON_CODE,
+                    conditions: [],
+                    actions: [freeOrderAction],
+                });
+            });
+
+            afterAll(() => {
+                shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+            });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/1692
+            it('does not allow a couponCode from another channel', async () => {
+                shopClient.setChannelToken(OTHER_CHANNEL_TOKEN);
+                const { applyCouponCode } = await shopClient.query<
+                    ApplyCouponCode.Mutation,
+                    ApplyCouponCode.Variables
+                >(APPLY_COUPON_CODE, {
+                    couponCode: OTHER_CHANNEL_COUPON_CODE,
+                });
+                orderResultGuard.assertErrorResult(applyCouponCode);
+                expect(applyCouponCode!.errorCode).toEqual('COUPON_CODE_INVALID_ERROR');
+            });
+        });
     });
 
     describe('default PromotionConditions', () => {

+ 0 - 22
packages/core/e2e/plugin.e2e-spec.ts

@@ -48,7 +48,6 @@ describe('Plugins', () => {
         const configService = server.app.get(ConfigService);
         expect(configService instanceof ConfigService).toBe(true);
         expect(configService.defaultLanguageCode).toBe(LanguageCode.zh);
-        expect(TestPluginWithConfig.configSpy).toHaveBeenCalledTimes(1);
     });
 
     it('extends the admin API', async () => {
@@ -154,24 +153,3 @@ describe('Plugins', () => {
         });
     });
 });
-
-describe('Multiple bootstraps in same process', () => {
-    const activeConfig = testConfig();
-    const { server, adminClient, shopClient } = createTestEnvironment({
-        ...activeConfig,
-        plugins: [TestPluginWithConfig.setup()],
-    });
-
-    beforeAll(async () => {
-        await server.init({
-            initialData,
-            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-            customerCount: 1,
-        });
-        await adminClient.asSuperAdmin();
-    }, TEST_SETUP_TIMEOUT_MS);
-
-    it('plugin `configure` function called only once', async () => {
-        expect(TestPluginWithConfig.configSpy).toHaveBeenCalledTimes(1);
-    });
-});

+ 96 - 3
packages/core/e2e/shop-order.e2e-spec.ts

@@ -130,6 +130,7 @@ describe('Shop orders', () => {
                     { name: 'notes', type: 'string' },
                     { name: 'privateField', type: 'string', public: false },
                     { name: 'lineImage', type: 'relation', entity: Asset },
+                    { name: 'lineImages', type: 'relation', list: true, entity: Asset },
                     { name: 'dropShip', type: 'boolean', defaultValue: false },
                 ],
             },
@@ -280,6 +281,9 @@ describe('Shop orders', () => {
                                 lineImage {
                                     id
                                 }
+                                lineImages {
+                                    id
+                                }
                             }
                         }
                     }
@@ -403,11 +407,13 @@ describe('Shop orders', () => {
                 });
                 expect(adjustOrderLine.lines[1].customFields).toEqual({
                     lineImage: null,
+                    lineImages: [],
                     notes: 'updated notes',
                 });
                 const { activeOrder: ao1 } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
                 expect(ao1.lines[1].customFields).toEqual({
                     lineImage: null,
+                    lineImages: [],
                     notes: 'updated notes',
                 });
                 const updatedNotesLineId = ao1.lines[1].id;
@@ -428,6 +434,7 @@ describe('Shop orders', () => {
                 expect(activeOrder.lines.find((l: any) => l.id === updatedNotesLineId)?.customFields).toEqual(
                     {
                         lineImage: null,
+                        lineImages: [],
                         notes: 'updated notes',
                     },
                 );
@@ -530,12 +537,95 @@ describe('Shop orders', () => {
                         orderLineId: activeOrder!.lines[2].id,
                     },
                 );
+                const { removeOrderLine } = await shopClient.query<
+                    RemoveItemFromOrder.Mutation,
+                    RemoveItemFromOrder.Variables
+                >(REMOVE_ITEM_FROM_ORDER, {
+                    orderLineId: activeOrder!.lines[1].id,
+                });
+                orderResultGuard.assertSuccess(removeOrderLine);
+                expect(removeOrderLine.lines.length).toBe(1);
+            });
+
+            it('addItemToOrder with list relation customField', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImagesIds: ['T_1', 'T_2'],
+                        },
+                    },
+                );
+
+                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.lineImages.length).toBe(2);
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_1' });
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_2' });
+            });
+
+            it('addItemToOrder with equal list 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: {
+                            lineImagesIds: ['T_1', 'T_2'],
+                        },
+                    },
+                );
+
+                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.lineImages.length).toBe(2);
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_1' });
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_2' });
+            });
+
+            it('addItemToOrder with different list 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: {
+                            lineImagesIds: ['T_1'],
+                        },
+                    },
+                );
+
+                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.lineImages).toEqual([{ id: 'T_1' }]);
+
                 await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
                     REMOVE_ITEM_FROM_ORDER,
                     {
-                        orderLineId: activeOrder!.lines[1].id,
+                        orderLineId: activeOrder!.lines[2].id,
                     },
                 );
+                const { removeOrderLine } = await shopClient.query<
+                    RemoveItemFromOrder.Mutation,
+                    RemoveItemFromOrder.Variables
+                >(REMOVE_ITEM_FROM_ORDER, {
+                    orderLineId: activeOrder!.lines[1].id,
+                });
+                orderResultGuard.assertSuccess(removeOrderLine);
+                expect(removeOrderLine.lines.length).toBe(1);
             });
         });
 
@@ -611,7 +701,7 @@ describe('Shop orders', () => {
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Variables
             >(ADJUST_ITEM_QUANTITY, {
-                orderLineId: 'T_10',
+                orderLineId: addItemToOrder!.order.lines[1].id,
                 quantity: 101,
             });
             orderResultGuard.assertErrorResult(adjustOrderLine);
@@ -627,7 +717,7 @@ describe('Shop orders', () => {
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Variables
             >(ADJUST_ITEM_QUANTITY, {
-                orderLineId: 'T_10',
+                orderLineId: addItemToOrder!.order.lines[1].id,
                 quantity: 0,
             });
             orderResultGuard.assertSuccess(adjustLine2);
@@ -2182,6 +2272,9 @@ const ADJUST_ORDER_LINE_WITH_CUSTOM_FIELDS = gql`
                         lineImage {
                             id
                         }
+                        lineImages {
+                            id
+                        }
                     }
                 }
             }

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",
@@ -49,7 +49,7 @@
     "@nestjs/testing": "7.6.17",
     "@nestjs/typeorm": "7.1.5",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.6.4",
+    "@vendure/common": "^1.6.5",
     "apollo-server-express": "2.24.1",
     "bcrypt": "^5.0.0",
     "body-parser": "^1.19.0",

+ 0 - 12
packages/core/src/bootstrap.ts

@@ -154,13 +154,6 @@ export async function preBootstrapConfig(
     return config;
 }
 
-// This is here to prevent a plugin's `configure` function from executing more than once in the
-// same process. Running more than once can occur e.g. if a script runs the `runMigrations()` function
-// followed by the `bootstrap()` function, and will lead to very hard-to-debug errors caused by
-// mutating the config object twice, e.g. plugins pushing custom fields again resulting in duplicate
-// custom field definitions.
-const pluginConfigDidRun = new WeakSet<RuntimeVendureConfig['plugins'][number]>();
-
 /**
  * Initialize any configured plugins.
  */
@@ -168,12 +161,7 @@ async function runPluginConfigurations(config: RuntimeVendureConfig): Promise<Ru
     for (const plugin of config.plugins) {
         const configFn = getConfigurationFunction(plugin);
         if (typeof configFn === 'function') {
-            const configAlreadyRan = pluginConfigDidRun.has(plugin);
-            if (configAlreadyRan) {
-                continue;
-            }
             config = await configFn(config);
-            pluginConfigDidRun.add(plugin);
         }
     }
     return config;

+ 7 - 0
packages/core/src/config/config-helpers.ts

@@ -4,6 +4,13 @@ import { PartialVendureConfig, RuntimeVendureConfig } from './vendure-config';
 
 let activeConfig = defaultConfig;
 
+/**
+ * Reset the activeConfig object back to the initial default state.
+ */
+export function resetConfig() {
+    activeConfig = defaultConfig;
+}
+
 /**
  * Override the default config by merging in the supplied values. Should only be used prior to
  * bootstrapping the app.

+ 9 - 0
packages/core/src/config/config.module.ts

@@ -6,6 +6,7 @@ import { ConfigurableOperationDef } from '../common/configurable-operation';
 import { Injector } from '../common/injector';
 import { InjectableStrategy } from '../common/types/injectable-strategy';
 
+import { resetConfig } from './config-helpers';
 import { ConfigService } from './config.service';
 
 @Module({
@@ -23,6 +24,14 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
     async onApplicationShutdown(signal?: string) {
         await this.destroyInjectableStrategies();
         await this.destroyConfigurableOperations();
+        /**
+         * When the application shuts down, we reset the activeConfig to the default. Usually this is
+         * redundant, as the app shutdown would normally coincide with the process ending. However, in some
+         * circumstances, such as when running migrations immediately followed by app bootstrap, the activeConfig
+         * will persist between these two applications and mutations e.g. to the CustomFields will result in
+         * hard-to-debug errors. So resetting is a precaution against this scenario.
+         */
+        resetConfig();
     }
 
     private async initInjectableStrategies() {

+ 5 - 2
packages/core/src/config/order/order-item-price-calculation-strategy.ts

@@ -5,9 +5,9 @@ import { ProductVariant } from '../../entity/product-variant/product-variant.ent
 
 /**
  * @description
- * The OrderItemPriceCalculationStrategy defines the price of an OrderItem. By default the 
+ * The OrderItemPriceCalculationStrategy defines the price of an OrderItem. By default the
  * {@link DefaultOrderItemPriceCalculationStrategy} is used.
- * 
+ *
  * ### When is the strategy invoked ?
  * * addItemToOrder (only on the new order line)
  * * adjustOrderLine  (only on the adjusted order line)
@@ -48,6 +48,9 @@ export interface OrderItemPriceCalculationStrategy extends InjectableStrategy {
      * @description
      * Receives the ProductVariant to be added to the Order as well as any OrderLine custom fields and returns
      * the price for a single unit.
+     *
+     * Note: if you have any `relation` type custom fields defined on the OrderLine entity, they will only be
+     * passed in to this method if they are set to `eager: true`.
      */
     calculateUnitPrice(
         ctx: RequestContext,

+ 30 - 14
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -6,7 +6,7 @@ import {
     RefundOrderInput,
 } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
-import { summate } from '@vendure/common/lib/shared-utils';
+import { getGraphQlInputName, summate } from '@vendure/common/lib/shared-utils';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
@@ -27,7 +27,9 @@ import {
 import { AdjustmentSource } from '../../../common/types/adjustment-source';
 import { idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
+import { CustomFieldConfig } from '../../../config/custom-field/custom-field-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
+import { VendureEntity } from '../../../entity/base/base.entity';
 import { OrderItem } from '../../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
@@ -591,8 +593,8 @@ export class OrderModifier {
             // or equal to the defaultValue
             for (const def of customFieldDefs) {
                 const key = def.name;
-                const existingValue = existingCustomFields?.[key];
-                if (existingValue != null) {
+                const existingValue = this.coerceValue(def, existingCustomFields);
+                if (existingValue != null && (!def.list || existingValue?.length !== 0)) {
                     if (def.defaultValue != null) {
                         if (existingValue !== def.defaultValue) {
                             return false;
@@ -619,14 +621,8 @@ export class OrderModifier {
 
         for (const def of customFieldDefs) {
             const key = def.name;
-            // This ternary is there because with the MySQL driver, boolean customFields with a default
-            // of `false` were being rep-resented as `0`, thus causing the equality check to fail.
-            // So if it's a boolean, we'll explicitly coerce the value to a boolean.
-            const existingValue =
-                def.type === 'boolean' && typeof existingCustomFields?.[key] === 'number'
-                    ? !!existingCustomFields?.[key]
-                    : existingCustomFields?.[key];
-            if (existingValue !== undefined) {
+            const existingValue = this.coerceValue(def, existingCustomFields);
+            if (def.type !== 'relation' && existingValue !== undefined) {
                 const valuesMatch =
                     JSON.stringify(inputCustomFields?.[key]) === JSON.stringify(existingValue);
                 const undefinedMatchesNull = existingValue === null && inputCustomFields?.[key] === undefined;
@@ -636,18 +632,38 @@ export class OrderModifier {
                     return false;
                 }
             } else if (def.type === 'relation') {
-                const inputId = `${key}Id`;
+                const inputId = getGraphQlInputName(def);
                 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;
+                if (inputValue) {
+                    const customFieldNotEqual = def.list
+                        ? JSON.stringify((inputValue as ID[]).sort()) !==
+                          JSON.stringify(
+                              existingRelation?.map((relation: VendureEntity) => relation.id).sort(),
+                          )
+                        : inputValue !== existingRelation?.id;
+                    if (customFieldNotEqual) {
+                        return false;
+                    }
                 }
             }
         }
         return true;
     }
 
+    /**
+     * This function is required because with the MySQL driver, boolean customFields with a default
+     * of `false` were being represented as `0`, thus causing the equality check to fail.
+     * So if it's a boolean, we'll explicitly coerce the value to a boolean.
+     */
+    private coerceValue(def: CustomFieldConfig, existingCustomFields: { [p: string]: any } | undefined) {
+        const key = def.name;
+        return def.type === 'boolean' && typeof existingCustomFields?.[key] === 'number'
+            ? !!existingCustomFields?.[key]
+            : existingCustomFields?.[key];
+    }
+
     private async getProductVariantOrThrow(
         ctx: RequestContext,
         productVariantId: ID,

+ 6 - 1
packages/core/src/service/services/promotion.service.ts

@@ -229,8 +229,13 @@ export class PromotionService {
                 enabled: true,
                 deletedAt: null,
             },
+            relations: ['channels'],
         });
-        if (!promotion || promotion.couponCode !== couponCode) {
+        if (
+            !promotion ||
+            promotion.couponCode !== couponCode ||
+            !promotion.channels.find(c => idsAreEqual(c.id, ctx.channelId))
+        ) {
             return new CouponCodeInvalidError(couponCode);
         }
         if (promotion.endsAt && +promotion.endsAt < +new Date()) {

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "license": "MIT",
   "bin": {
     "create": "./index.js"
@@ -28,13 +28,13 @@
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.2",
     "@types/semver": "^6.2.2",
-    "@vendure/core": "^1.6.4",
+    "@vendure/core": "^1.6.5",
     "rimraf": "^3.0.2",
     "ts-node": "^10.2.1",
     "typescript": "4.3.5"
   },
   "dependencies": {
-    "@vendure/common": "^1.6.4",
+    "@vendure/common": "^1.6.5",
     "chalk": "^4.1.0",
     "commander": "^7.1.0",
     "cross-spawn": "^7.0.3",

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "dev-server",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "main": "index.js",
   "license": "MIT",
   "private": true,
@@ -14,18 +14,18 @@
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
   },
   "dependencies": {
-    "@vendure/admin-ui-plugin": "^1.6.4",
-    "@vendure/asset-server-plugin": "^1.6.4",
-    "@vendure/common": "^1.6.4",
-    "@vendure/core": "^1.6.4",
-    "@vendure/elasticsearch-plugin": "^1.6.4",
-    "@vendure/email-plugin": "^1.6.4",
+    "@vendure/admin-ui-plugin": "^1.6.5",
+    "@vendure/asset-server-plugin": "^1.6.5",
+    "@vendure/common": "^1.6.5",
+    "@vendure/core": "^1.6.5",
+    "@vendure/elasticsearch-plugin": "^1.6.5",
+    "@vendure/email-plugin": "^1.6.5",
     "typescript": "4.3.5"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^1.6.4",
-    "@vendure/ui-devkit": "^1.6.4",
+    "@vendure/testing": "^1.6.5",
+    "@vendure/ui-devkit": "^1.6.5",
     "commander": "^7.1.0",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3",

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -25,8 +25,8 @@
     "fast-deep-equal": "^3.1.3"
   },
   "devDependencies": {
-    "@vendure/common": "^1.6.4",
-    "@vendure/core": "^1.6.4",
+    "@vendure/common": "^1.6.5",
+    "@vendure/core": "^1.6.5",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
   }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -35,8 +35,8 @@
     "@types/fs-extra": "^9.0.1",
     "@types/handlebars": "^4.1.0",
     "@types/mjml": "^4.0.4",
-    "@vendure/common": "^1.6.4",
-    "@vendure/core": "^1.6.4",
+    "@vendure/common": "^1.6.5",
+    "@vendure/core": "^1.6.5",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
   }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/job-queue-plugin",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "license": "MIT",
   "main": "package/index.js",
   "types": "package/index.d.ts",
@@ -24,8 +24,8 @@
   "devDependencies": {
     "@google-cloud/pubsub": "^2.8.0",
     "@types/ioredis": "^4.28.10",
-    "@vendure/common": "^1.6.4",
-    "@vendure/core": "^1.6.4",
+    "@vendure/common": "^1.6.5",
+    "@vendure/core": "^1.6.5",
     "bullmq": "^1.86.7",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"

+ 4 - 4
packages/payments-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "1.6.4",
+    "version": "1.6.5",
     "license": "MIT",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -28,9 +28,9 @@
     "devDependencies": {
         "@mollie/api-client": "^3.6.0",
         "@types/braintree": "^2.22.15",
-        "@vendure/common": "^1.6.4",
-        "@vendure/core": "^1.6.4",
-        "@vendure/testing": "^1.6.4",
+        "@vendure/common": "^1.6.5",
+        "@vendure/core": "^1.6.5",
+        "@vendure/testing": "^1.6.5",
         "braintree": "^3.0.0",
         "nock": "^13.1.4",
         "rimraf": "^3.0.2",

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/testing",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
     "vendure",
@@ -34,7 +34,7 @@
   },
   "dependencies": {
     "@types/node-fetch": "^2.5.4",
-    "@vendure/common": "^1.6.4",
+    "@vendure/common": "^1.6.5",
     "faker": "^4.1.0",
     "form-data": "^3.0.0",
     "graphql": "15.5.1",
@@ -45,7 +45,7 @@
   "devDependencies": {
     "@types/mysql": "^2.15.15",
     "@types/pg": "^7.14.5",
-    "@vendure/core": "^1.6.4",
+    "@vendure/core": "^1.6.5",
     "mysql": "^2.18.1",
     "pg": "^8.4.0",
     "rimraf": "^3.0.0",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "1.6.4",
+  "version": "1.6.5",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -40,8 +40,8 @@
     "@angular/cli": "12.2.16",
     "@angular/compiler": "12.2.16",
     "@angular/compiler-cli": "12.2.16",
-    "@vendure/admin-ui": "^1.6.4",
-    "@vendure/common": "^1.6.4",
+    "@vendure/admin-ui": "^1.6.5",
+    "@vendure/common": "^1.6.5",
     "chalk": "^4.1.0",
     "chokidar": "^3.5.1",
     "fs-extra": "^10.0.0",
@@ -52,7 +52,7 @@
     "@rollup/plugin-node-resolve": "^11.2.0",
     "@types/fs-extra": "^9.0.8",
     "@types/glob": "^7.1.3",
-    "@vendure/core": "^1.6.4",
+    "@vendure/core": "^1.6.5",
     "rimraf": "^3.0.2",
     "rollup": "^2.40.0",
     "rollup-plugin-terser": "^7.0.2",