Quellcode durchsuchen

Merge branch 'master' into minor

Michael Bromley vor 3 Jahren
Ursprung
Commit
024c1e337a
36 geänderte Dateien mit 334 neuen und 144 gelöschten Zeilen
  1. 22 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. 3 3
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.html
  6. 25 15
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts
  7. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  8. 2 0
      packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html
  9. 3 0
      packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.scss
  10. 3 3
      packages/asset-server-plugin/package.json
  11. 1 1
      packages/common/package.json
  12. 3 1
      packages/common/src/shared-utils.ts
  13. 21 19
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  14. 37 0
      packages/core/e2e/order-promotion.e2e-spec.ts
  15. 69 0
      packages/core/e2e/shop-order.e2e-spec.ts
  16. 2 2
      packages/core/package.json
  17. 5 22
      packages/core/src/api/resolvers/admin/collection.resolver.ts
  18. 24 1
      packages/core/src/api/resolvers/entity/collection-entity.resolver.ts
  19. 1 1
      packages/core/src/common/configurable-operation.ts
  20. 1 1
      packages/core/src/config/promotion/actions/order-fixed-discount-action.ts
  21. 1 0
      packages/core/src/i18n/messages/en.json
  22. 6 1
      packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts
  23. 1 0
      packages/core/src/service/index.ts
  24. 14 4
      packages/core/src/service/services/collection.service.ts
  25. 19 7
      packages/core/src/service/services/order.service.ts
  26. 13 23
      packages/core/src/service/services/product.service.ts
  27. 15 1
      packages/core/src/service/services/promotion.service.ts
  28. 3 3
      packages/create/package.json
  29. 9 9
      packages/dev-server/package.json
  30. 3 3
      packages/elasticsearch-plugin/package.json
  31. 3 3
      packages/email-plugin/package.json
  32. 3 3
      packages/job-queue-plugin/package.json
  33. 4 4
      packages/payments-plugin/package.json
  34. 3 3
      packages/testing/package.json
  35. 4 0
      packages/testing/src/test-server.ts
  36. 4 4
      packages/ui-devkit/package.json

+ 22 - 0
CHANGELOG.md

@@ -1,3 +1,25 @@
+## <small>1.7.4 (2022-10-11)</small>
+
+
+#### Perf
+
+* **core** Improve performance when querying product by slug ([742ad36](https://github.com/vendure-ecommerce/vendure/commit/742ad36))
+
+#### Fixes
+
+* **admin-ui** Fix fix of ShippingMethod update error ([2367ab0](https://github.com/vendure-ecommerce/vendure/commit/2367ab0)), closes [#1800](https://github.com/vendure-ecommerce/vendure/issues/1800)
+* **admin-ui** Fix variant editing when 2 options have same name ([56948c8](https://github.com/vendure-ecommerce/vendure/commit/56948c8)), closes [#1813](https://github.com/vendure-ecommerce/vendure/issues/1813)
+* **admin-ui** Wrap long promotion condition/action names ([3eba1c8](https://github.com/vendure-ecommerce/vendure/commit/3eba1c8))
+* **common** Handle edge case in serializing null prototype objects ([02249fb](https://github.com/vendure-ecommerce/vendure/commit/02249fb))
+* **core** Add translation for 'channel-not-found' error ([f7c053f](https://github.com/vendure-ecommerce/vendure/commit/f7c053f))
+* **core** Do not allow negative total with orderFixedDiscount action ([a031956](https://github.com/vendure-ecommerce/vendure/commit/a031956)), closes [#1823](https://github.com/vendure-ecommerce/vendure/issues/1823)
+* **core** Export TranslatorService helper from core (#1826) ([50d5856](https://github.com/vendure-ecommerce/vendure/commit/50d5856)), closes [#1826](https://github.com/vendure-ecommerce/vendure/issues/1826)
+* **core** Fix default search handling of mysql binary operators ([c133cce](https://github.com/vendure-ecommerce/vendure/commit/c133cce)), closes [#1808](https://github.com/vendure-ecommerce/vendure/issues/1808)
+* **core** Fix race condition when updating order addresses in parallel ([d436ea9](https://github.com/vendure-ecommerce/vendure/commit/d436ea9))
+* **core** Improved error handling for malformed collection filters ([cab520b](https://github.com/vendure-ecommerce/vendure/commit/cab520b))
+* **core** Persist customField relations in PromotionService (#1822) ([40fdd80](https://github.com/vendure-ecommerce/vendure/commit/40fdd80)), closes [#1822](https://github.com/vendure-ecommerce/vendure/issues/1822)
+* **testing** Correctly apply beforeListen middleware on TestServer (#1802) ([c1db17e](https://github.com/vendure-ecommerce/vendure/commit/c1db17e)), closes [#1802](https://github.com/vendure-ecommerce/vendure/issues/1802)
+
 ## <small>1.7.3 (2022-09-24)</small>
 
 

+ 1 - 1
lerna.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "1.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/core": "^1.7.3",
+    "@vendure/common": "^1.7.4",
+    "@vendure/core": "^1.7.4",
     "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.7.3",
+  "version": "1.7.4",
   "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.7.3",
+    "@vendure/common": "^1.7.4",
     "@webcomponents/custom-elements": "^1.4.3",
     "apollo-angular": "^2.6.0",
     "apollo-upload-client": "^16.0.0",

+ 3 - 3
packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.html

@@ -10,7 +10,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<div *ngFor="let group of optionGroups" class="option-groups">
+<div *ngFor="let group of optionGroups; index as i" class="option-groups">
     <div class="name">
         <label>{{ 'catalog.option' | translate }}</label>
         <input clrInput [(ngModel)]="group.name" name="name" [readonly]="!group.isNew" />
@@ -22,8 +22,8 @@
             [options]="group.values"
             [groupName]="group.name"
             [disabled]="group.name === ''"
-            (add)="addOption(group.id, $event.name)"
-            (remove)="removeOption(group.id, $event)"
+            (add)="addOption(i, $event.name)"
+            (remove)="removeOption(i, $event)"
         ></vdr-option-value-input>
     </div>
     <div>

+ 25 - 15
packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts

@@ -157,14 +157,17 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
         }
     }
 
-    addOption(groupId: string, optionName: string) {
-        this.optionGroups.find(g => g.id === groupId)?.values.push({ name: optionName, locked: false });
-        this.generateVariants();
-        this.optionsChanged = true;
+    addOption(index: number, optionName: string) {
+        const group = this.optionGroups[index];
+        if (group) {
+            group.values.push({ name: optionName, locked: false });
+            this.generateVariants();
+            this.optionsChanged = true;
+        }
     }
 
-    removeOption(groupId: string, { id, name }: { id?: string; name: string }) {
-        const optionGroup = this.optionGroups.find(g => g.id === groupId);
+    removeOption(index: number, { id, name }: { id?: string; name: string }) {
+        const optionGroup = this.optionGroups[index];
         if (optionGroup) {
             if (!id) {
                 optionGroup.values = optionGroup.values.filter(v => v.name !== name);
@@ -442,15 +445,22 @@ export class ProductVariantsEditorComponent implements OnInit, DeactivateAware {
             .reduce((flat, o) => [...flat, ...o], []);
         const variants = this.generatedVariants
             .filter(v => v.enabled && !v.existing)
-            .map(v => ({
-                price: v.price,
-                sku: v.sku,
-                stock: v.stock,
-                optionIds: v.options
-                    .map(name => options.find(o => o.name === name.name))
-                    .filter(notNullOrUndefined)
-                    .map(o => o.id),
-            }));
+            .map(v => {
+                const optionIds = groups.map((group, index) => {
+                    const option = group.options.find(o => o.name === v.options[index].name);
+                    if (option) {
+                        return option.id;
+                    } else {
+                        throw new Error(`Could not find a matching option for group ${group.name}`);
+                    }
+                });
+                return {
+                    price: v.price,
+                    sku: v.sku,
+                    stock: v.stock,
+                    optionIds,
+                };
+            });
         return this.productDetailService.createProductVariants(
             this.product,
             variants,

+ 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.7.3';
+export const ADMIN_UI_VERSION = '1.7.4';

+ 2 - 0
packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html

@@ -106,6 +106,7 @@
                             *ngFor="let condition of getAvailableConditions()"
                             type="button"
                             vdrDropdownItem
+                            class="item-wrap"
                             (click)="addCondition(condition)"
                         >
                             {{ condition.description }}
@@ -136,6 +137,7 @@
                             *ngFor="let action of getAvailableActions()"
                             type="button"
                             vdrDropdownItem
+                            class="item-wrap"
                             (click)="addAction(action)"
                         >
                             {{ action.description }}

+ 3 - 0
packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.scss

@@ -0,0 +1,3 @@
+.item-wrap {
+    white-space: normal;
+}

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "1.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/core": "^1.7.3",
+    "@vendure/common": "^1.7.4",
+    "@vendure/core": "^1.7.4",
     "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.7.3",
+  "version": "1.7.4",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 3 - 1
packages/common/src/shared-utils.ts

@@ -24,7 +24,9 @@ export function isObject(item: any): item is object {
 }
 
 export function isClassInstance(item: any): boolean {
-    return isObject(item) && item.constructor.name !== 'Object';
+    // Even if item is an object, it might not have a constructor as in the
+    // case when it is a null-prototype object, i.e. created using `Object.create(null)`.
+    return isObject(item) && item.constructor && item.constructor.name !== 'Object';
 }
 
 type NumericPropsOf<T> = {

+ 21 - 19
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -1670,36 +1670,38 @@ describe('Default search plugin', () => {
 
         // https://github.com/vendure-ecommerce/vendure/issues/1789
         describe('input escaping', () => {
-            it('correctly escapes "a & b"', async () => {
-                const result = await adminClient.query<SearchProductsShop.Query, SearchProductShopVariables>(
+            function search(term: string) {
+                return adminClient.query<SearchProductsShop.Query, SearchProductShopVariables>(
                     SEARCH_PRODUCTS,
                     {
-                        input: {
-                            take: 10,
-                            term: 'laptop & camera',
-                        },
+                        input: { take: 10, term },
                     },
                     {
-                        languageCode: LanguageCode.de,
+                        languageCode: LanguageCode.en,
                     },
                 );
+            }
+            it('correctly escapes "a & b"', async () => {
+                const result = await search('laptop & camera');
                 expect(result.search.items).toBeDefined();
             });
+
             it('correctly escapes other special chars', async () => {
-                const result = await adminClient.query<SearchProductsShop.Query, SearchProductShopVariables>(
-                    SEARCH_PRODUCTS,
-                    {
-                        input: {
-                            take: 10,
-                            term: 'a : b ? * (c) ! "foo"',
-                        },
-                    },
-                    {
-                        languageCode: LanguageCode.de,
-                    },
-                );
+                const result = await search('a : b ? * (c) ! "foo"');
                 expect(result.search.items).toBeDefined();
             });
+
+            it('correctly escapes mysql binary mode chars', async () => {
+                expect((await search('foo+')).search.items).toBeDefined();
+                expect((await search('foo-')).search.items).toBeDefined();
+                expect((await search('foo<')).search.items).toBeDefined();
+                expect((await search('foo>')).search.items).toBeDefined();
+                expect((await search('foo*')).search.items).toBeDefined();
+                expect((await search('foo~')).search.items).toBeDefined();
+                expect((await search('foo@bar')).search.items).toBeDefined();
+                expect((await search('foo + - *')).search.items).toBeDefined();
+                expect((await search('foo + - bar')).search.items).toBeDefined();
+            });
         });
     });
 });

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

@@ -63,6 +63,8 @@ import {
     RemoveItemFromOrder,
     SetCustomerForOrder,
     SetShippingMethod,
+    SetShippingMethodMutation,
+    SetShippingMethodMutationVariables,
     TestOrderFragmentFragment,
     TestOrderWithPaymentsFragment,
     UpdatedOrderFragment,
@@ -784,6 +786,41 @@ describe('Promotions applied to Orders', () => {
                 expect(applyCouponCode!.discounts[0].description).toBe('$10 discount on order');
                 expect(applyCouponCode!.totalWithTax).toBe(5000);
             });
+
+            it('does not result in negative total when shipping is included', async () => {
+                shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                const { addItemToOrder } = await shopClient.query<
+                    AddItemToOrder.Mutation,
+                    AddItemToOrder.Variables
+                >(ADD_ITEM_TO_ORDER, {
+                    productVariantId: getVariantBySlug('item-100').id,
+                    quantity: 1,
+                });
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.totalWithTax).toBe(120);
+                expect(addItemToOrder!.discounts.length).toBe(0);
+
+                const { setOrderShippingMethod } = await shopClient.query<
+                    SetShippingMethodMutation,
+                    SetShippingMethodMutationVariables
+                >(SET_SHIPPING_METHOD, {
+                    id: 'T_1',
+                });
+                orderResultGuard.assertSuccess(setOrderShippingMethod);
+                expect(setOrderShippingMethod.totalWithTax).toBe(620);
+
+                const { applyCouponCode } = await shopClient.query<
+                    ApplyCouponCode.Mutation,
+                    ApplyCouponCode.Variables
+                >(APPLY_COUPON_CODE, {
+                    couponCode,
+                });
+                orderResultGuard.assertSuccess(applyCouponCode);
+                expect(applyCouponCode!.discounts.length).toBe(1);
+                expect(applyCouponCode!.discounts[0].description).toBe('$10 discount on order');
+                expect(applyCouponCode!.subTotalWithTax).toBe(0);
+                expect(applyCouponCode!.totalWithTax).toBe(500); // shipping price
+            });
         });
 
         describe('discountOnItemWithFacets', () => {

+ 69 - 0
packages/core/e2e/shop-order.e2e-spec.ts

@@ -2203,6 +2203,75 @@ describe('Shop orders', () => {
             expect(removeAllOrderLines.shippingWithTax).toBe(0);
         });
     });
+
+    describe('edge cases', () => {
+        it('calling setShippingMethod and setBillingMethod in parallel does not introduce race condition', async () => {
+            const shippingAddress: CreateAddressInput = {
+                fullName: 'name',
+                company: 'company',
+                streetLine1: '12 Shipping Street',
+                streetLine2: null,
+                city: 'foo',
+                province: 'bar',
+                postalCode: '123456',
+                countryCode: 'US',
+                phoneNumber: '4444444',
+            };
+            const billingAddress: CreateAddressInput = {
+                fullName: 'name',
+                company: 'company',
+                streetLine1: '22 Billing Avenue',
+                streetLine2: null,
+                city: 'foo',
+                province: 'bar',
+                postalCode: '123456',
+                countryCode: 'US',
+                phoneNumber: '4444444',
+            };
+
+            await Promise.all([
+                shopClient.query<SetBillingAddress.Mutation, SetBillingAddress.Variables>(
+                    SET_BILLING_ADDRESS,
+                    {
+                        input: billingAddress,
+                    },
+                ),
+                shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
+                    SET_SHIPPING_ADDRESS,
+                    {
+                        input: shippingAddress,
+                    },
+                ),
+            ]);
+
+            const { activeOrder } = await shopClient.query(gql`
+                query {
+                    activeOrder {
+                        shippingAddress {
+                            ...OrderAddress
+                        }
+                        billingAddress {
+                            ...OrderAddress
+                        }
+                    }
+                }
+                fragment OrderAddress on OrderAddress {
+                    fullName
+                    company
+                    streetLine1
+                    streetLine2
+                    city
+                    province
+                    postalCode
+                    countryCode
+                    phoneNumber
+                }
+            `);
+
+            expect(activeOrder.shippingAddress).toEqual(shippingAddress);
+            expect(activeOrder.billingAddress).toEqual(billingAddress);
+        });
+    });
 });
 
 const GET_ORDER_CUSTOM_FIELDS = gql`

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "1.7.3",
+  "version": "1.7.4",
   "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.7.3",
+    "@vendure/common": "^1.7.4",
     "apollo-server-express": "2.24.1",
     "bcrypt": "^5.0.0",
     "body-parser": "^1.19.0",

+ 5 - 22
packages/core/src/api/resolvers/admin/collection.resolver.ts

@@ -57,10 +57,7 @@ export class CollectionResolver {
         })
         relations: RelationPaths<Collection>,
     ): Promise<PaginatedList<Translated<Collection>>> {
-        return this.collectionService.findAll(ctx, args.options || undefined, relations).then(res => {
-            res.items.forEach(this.encodeFilters);
-            return res;
-        });
+        return this.collectionService.findAll(ctx, args.options || undefined, relations);
     }
 
     @Query()
@@ -85,8 +82,7 @@ export class CollectionResolver {
         } else {
             throw new UserInputError(`error.collection-id-or-slug-must-be-provided`);
         }
-
-        return this.encodeFilters(collection);
+        return collection;
     }
 
     @Query()
@@ -105,7 +101,7 @@ export class CollectionResolver {
     ): Promise<Translated<Collection>> {
         const { input } = args;
         this.configurableOperationCodec.decodeConfigurableOperationIds(CollectionFilter, input.filters);
-        return this.collectionService.create(ctx, input).then(this.encodeFilters);
+        return this.collectionService.create(ctx, input);
     }
 
     @Transaction()
@@ -117,7 +113,7 @@ export class CollectionResolver {
     ): Promise<Translated<Collection>> {
         const { input } = args;
         this.configurableOperationCodec.decodeConfigurableOperationIds(CollectionFilter, input.filters || []);
-        return this.collectionService.update(ctx, input).then(this.encodeFilters);
+        return this.collectionService.update(ctx, input);
     }
 
     @Transaction()
@@ -128,7 +124,7 @@ export class CollectionResolver {
         @Args() args: MutationMoveCollectionArgs,
     ): Promise<Translated<Collection>> {
         const { input } = args;
-        return this.collectionService.move(ctx, input).then(this.encodeFilters);
+        return this.collectionService.move(ctx, input);
     }
 
     @Transaction()
@@ -151,19 +147,6 @@ export class CollectionResolver {
         return Promise.all(args.ids.map(id => this.collectionService.delete(ctx, id)));
     }
 
-    /**
-     * Encodes any entity IDs used in the filter arguments.
-     */
-    private encodeFilters = <T extends Collection | undefined>(collection: T): T => {
-        if (collection) {
-            this.configurableOperationCodec.encodeConfigurableOperationIds(
-                CollectionFilter,
-                collection.filters,
-            );
-        }
-        return collection;
-    };
-
     @Transaction()
     @Mutation()
     @Allow(Permission.CreateCatalog, Permission.CreateCollection)

+ 24 - 1
packages/core/src/api/resolvers/entity/collection-entity.resolver.ts

@@ -1,14 +1,21 @@
+import { Logger } from '@nestjs/common';
 import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql';
-import { CollectionBreadcrumb, ProductVariantListOptions } from '@vendure/common/lib/generated-types';
+import {
+    CollectionBreadcrumb,
+    ConfigurableOperation,
+    ProductVariantListOptions,
+} from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { ListQueryOptions } from '../../../common/types/common-types';
 import { Translated } from '../../../common/types/locale-types';
+import { CollectionFilter } from '../../../config/index';
 import { Asset, Collection, Product, ProductVariant } from '../../../entity';
 import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
 import { AssetService } from '../../../service/services/asset.service';
 import { CollectionService } from '../../../service/services/collection.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
+import { ConfigurableOperationCodec } from '../../common/configurable-operation-codec';
 import { ApiType } from '../../common/get-api-type';
 import { RequestContext } from '../../common/request-context';
 import { Api } from '../../decorators/api.decorator';
@@ -22,6 +29,7 @@ export class CollectionEntityResolver {
         private collectionService: CollectionService,
         private assetService: AssetService,
         private localeStringHydrator: LocaleStringHydrator,
+        private configurableOperationCodec: ConfigurableOperationCodec,
     ) {}
 
     @ResolveField()
@@ -118,4 +126,19 @@ export class CollectionEntityResolver {
     async assets(@Ctx() ctx: RequestContext, @Parent() collection: Collection): Promise<Asset[] | undefined> {
         return this.assetService.getEntityAssets(ctx, collection);
     }
+
+    @ResolveField()
+    filters(@Ctx() ctx: RequestContext, @Parent() collection: Collection): ConfigurableOperation[] {
+        try {
+            return this.configurableOperationCodec.encodeConfigurableOperationIds(
+                CollectionFilter,
+                collection.filters,
+            );
+        } catch (e: any) {
+            Logger.error(
+                `Could not decode the collection filter arguments for "${collection.name}" (id: ${collection.id}). Error message: ${e.message}`,
+            );
+            return [];
+        }
+    }
 }

+ 1 - 1
packages/core/src/common/configurable-operation.ts

@@ -440,7 +440,7 @@ function coerceValueToType<T extends ConfigArgs>(
         try {
             return (JSON.parse(value) as string[]).map(v => coerceValueToType(v, type, false)) as any;
         } catch (err) {
-            throw new InternalServerError(err.message);
+            throw new InternalServerError(`Could not parse list value "${value}": ` + err.message);
         }
     }
     switch (type) {

+ 1 - 1
packages/core/src/config/promotion/actions/order-fixed-discount-action.ts

@@ -13,7 +13,7 @@ export const orderFixedDiscount = new PromotionOrderAction({
         },
     },
     execute(ctx, order, args) {
-        return -Math.min(args.discount, order.total);
+        return -Math.min(args.discount, order.subTotal);
     },
     description: [{ languageCode: LanguageCode.en, value: 'Discount order by fixed amount' }],
 });

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -9,6 +9,7 @@
     "cannot-transition-payment-from-to": "Cannot transition Payment from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-refund-from-to": "Cannot transition Refund from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-fulfillment-from-to": "Cannot transition Fulfillment from \"{ fromState }\" to \"{ toState }\"",
+    "channel-not-found": "No Channel with the token \"{ token }\" could be found",
     "collections-cannot-be-removed-from-default-channel": "Collections cannot be removed from the default Channel",
     "collection-id-or-slug-must-be-provided": "Either the Collection id or slug must be provided",
     "collection-id-slug-mismatch": "The provided id and slug refer to different Collections",

+ 6 - 1
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -149,7 +149,12 @@ export class MysqlSearchStrategy implements SearchStrategy {
             input;
 
         if (term && term.length > this.minTermLength) {
-            const safeTerm = term.replace(/"/g, '');
+            const safeTerm = term
+                .replace(/"/g, '')
+                .replace(/@/g, ' ')
+                .trim()
+                .replace(/[+\-*~<>]/g, ' ')
+                .trim();
             const termScoreQuery = this.connection
                 .getRepository(ctx, SearchIndexItem)
                 .createQueryBuilder('si_inner')

+ 1 - 0
packages/core/src/service/index.ts

@@ -17,6 +17,7 @@ export * from './helpers/product-price-applicator/product-price-applicator';
 export * from './helpers/refund-state-machine/refund-state';
 export * from './helpers/request-context/request-context.service';
 export * from './helpers/translatable-saver/translatable-saver';
+export * from './helpers/translator/translator.service';
 export * from './helpers/utils/patch-entity';
 export * from './helpers/utils/translate-entity';
 export * from './helpers/verification-token-generator/verification-token-generator';

+ 14 - 4
packages/core/src/service/services/collection.service.ts

@@ -124,10 +124,20 @@ export class CollectionService implements OnModuleInit {
                     }
                     completed++;
                     if (collection) {
-                        const affectedVariantIds = await this.applyCollectionFiltersInternal(
-                            collection,
-                            job.data.applyToChangedVariantsOnly,
-                        );
+                        let affectedVariantIds: ID[] = [];
+                        try {
+                            affectedVariantIds = await this.applyCollectionFiltersInternal(
+                                collection,
+                                job.data.applyToChangedVariantsOnly,
+                            );
+                        } catch (e) {
+                            const translatedCollection = await this.translator.translate(collection, ctx);
+                            Logger.error(
+                                `An error occurred when processing the filters for the collection "${translatedCollection.name}" (id: ${collection.id})`,
+                            );
+                            Logger.error(e.message);
+                            continue;
+                        }
                         job.setProgress(Math.ceil((completed / job.data.collectionIds.length) * 100));
                         this.eventBus.publish(
                             new CollectionModificationEvent(ctx, collection, affectedVariantIds),

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

@@ -751,10 +751,16 @@ export class OrderService {
     async setShippingAddress(ctx: RequestContext, orderId: ID, input: CreateAddressInput): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const country = await this.countryService.findOneByCode(ctx, input.countryCode);
-        order.shippingAddress = { ...input, countryCode: input.countryCode, country: country.name };
-        await this.connection.getRepository(ctx, Order).save(order);
+        const shippingAddress = { ...input, countryCode: input.countryCode, country: country.name };
+        await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder('order')
+            .update(Order)
+            .set({ shippingAddress })
+            .where('id = :id', { id: order.id });
+        order.shippingAddress = shippingAddress;
         // Since a changed ShippingAddress could alter the activeTaxZone,
-        // we will remove any cached activeTaxZone so it can be re-calculated
+        // we will remove any cached activeTaxZone, so it can be re-calculated
         // as needed.
         this.requestCache.set(ctx, 'activeTaxZone', undefined);
         return this.applyPriceAdjustments(ctx, order, order.lines);
@@ -767,10 +773,16 @@ export class OrderService {
     async setBillingAddress(ctx: RequestContext, orderId: ID, input: CreateAddressInput): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const country = await this.countryService.findOneByCode(ctx, input.countryCode);
-        order.billingAddress = { ...input, countryCode: input.countryCode, country: country.name };
-        await this.connection.getRepository(ctx, Order).save(order);
-        // Since a changed ShippingAddress could alter the activeTaxZone,
-        // we will remove any cached activeTaxZone so it can be re-calculated
+        const billingAddress = { ...input, countryCode: input.countryCode, country: country.name };
+        await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder('order')
+            .update(Order)
+            .set({ billingAddress })
+            .where('id = :id', { id: order.id });
+        order.billingAddress = billingAddress;
+        // Since a changed BillingAddress could alter the activeTaxZone,
+        // we will remove any cached activeTaxZone, so it can be re-calculated
         // as needed.
         this.requestCache.set(ctx, 'activeTaxZone', undefined);
         return this.applyPriceAdjustments(ctx, order, order.lines);

+ 13 - 23
packages/core/src/service/services/product.service.ts

@@ -174,42 +174,32 @@ export class ProductService {
         relations?: RelationPaths<Product>,
     ): Promise<Translated<Product> | undefined> {
         const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
-        const effectiveRelations = removeCustomFieldsWithEagerRelations(
-            qb,
-            relations ? [...new Set(this.relations.concat(relations))] : this.relations,
-        );
-        FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
-            relations: effectiveRelations,
-        });
-        // tslint:disable-next-line:no-non-null-assertion
-        FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
         const translationQb = this.connection
             .getRepository(ctx, ProductTranslation)
             .createQueryBuilder('_product_translation')
             .select('_product_translation.baseId')
             .andWhere('_product_translation.slug = :slug', { slug });
 
-        const translationsAlias = relations?.includes('translations' as any)
-            ? 'product__translations'
-            : 'product_translations';
-        qb.leftJoin('product.channels', 'channel')
+        qb.leftJoin('product.translations', 'translation')
+            .andWhere('product.deletedAt IS NULL')
             .andWhere('product.id IN (' + translationQb.getQuery() + ')')
             .setParameters(translationQb.getParameters())
-            .andWhere('product.deletedAt IS NULL')
-            .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
+            .select('product.id', 'id')
             .addSelect(
                 // tslint:disable-next-line:max-line-length
-                `CASE ${translationsAlias}.languageCode WHEN '${ctx.languageCode}' THEN 2 WHEN '${ctx.channel.defaultLanguageCode}' THEN 1 ELSE 0 END`,
+                `CASE translation.languageCode WHEN '${ctx.languageCode}' THEN 2 WHEN '${ctx.channel.defaultLanguageCode}' THEN 1 ELSE 0 END`,
                 'sort_order',
             )
             .orderBy('sort_order', 'DESC');
-        return qb
-            .getOne()
-            .then(product =>
-                product
-                    ? this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']])
-                    : undefined,
-            );
+        // We use getRawOne here to simply get the ID as efficiently as possible,
+        // which we then pass to the regular findOne() method which will handle
+        // all the joins etc.
+        const result = await qb.getRawOne();
+        if (result) {
+            return this.findOne(ctx, result.id, relations);
+        } else {
+            return undefined;
+        }
     }
 
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {

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

@@ -38,6 +38,7 @@ import { Promotion } from '../../entity/promotion/promotion.entity';
 import { EventBus } from '../../event-bus';
 import { PromotionEvent } from '../../event-bus/events/promotion-event';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
+import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -61,6 +62,7 @@ export class PromotionService {
         private channelService: ChannelService,
         private listQueryBuilder: ListQueryBuilder,
         private configArgService: ConfigArgService,
+        private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
     ) {
         this.availableConditions = this.configService.promotionOptions.promotionConditions || [];
@@ -130,7 +132,13 @@ export class PromotionService {
         }
         await this.channelService.assignToCurrentChannel(promotion, ctx);
         const newPromotion = await this.connection.getRepository(ctx, Promotion).save(promotion);
-        this.eventBus.publish(new PromotionEvent(ctx, newPromotion, 'created', input));
+        const promotionWithRelations = await this.customFieldRelationService.updateRelations(
+            ctx,
+            Promotion,
+            input,
+            newPromotion,
+        );
+        this.eventBus.publish(new PromotionEvent(ctx, promotionWithRelations, 'created', input));
         return assertFound(this.findOne(ctx, newPromotion.id));
     }
 
@@ -157,6 +165,12 @@ export class PromotionService {
         }
         promotion.priorityScore = this.calculatePriorityScore(input);
         await this.connection.getRepository(ctx, Promotion).save(updatedPromotion, { reload: false });
+        await this.customFieldRelationService.updateRelations(
+            ctx,
+            Promotion,
+            input,
+            updatedPromotion,
+        );
         this.eventBus.publish(new PromotionEvent(ctx, promotion, 'updated', input));
         return assertFound(this.findOne(ctx, updatedPromotion.id));
     }

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "1.7.3",
+  "version": "1.7.4",
   "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.7.3",
+    "@vendure/core": "^1.7.4",
     "rimraf": "^3.0.2",
     "ts-node": "^10.2.1",
     "typescript": "4.3.5"
   },
   "dependencies": {
-    "@vendure/common": "^1.7.3",
+    "@vendure/common": "^1.7.4",
     "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.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/asset-server-plugin": "^1.7.3",
-    "@vendure/common": "^1.7.3",
-    "@vendure/core": "^1.7.3",
-    "@vendure/elasticsearch-plugin": "^1.7.3",
-    "@vendure/email-plugin": "^1.7.3",
+    "@vendure/admin-ui-plugin": "^1.7.4",
+    "@vendure/asset-server-plugin": "^1.7.4",
+    "@vendure/common": "^1.7.4",
+    "@vendure/core": "^1.7.4",
+    "@vendure/elasticsearch-plugin": "^1.7.4",
+    "@vendure/email-plugin": "^1.7.4",
     "typescript": "4.3.5"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^1.7.3",
-    "@vendure/ui-devkit": "^1.7.3",
+    "@vendure/testing": "^1.7.4",
+    "@vendure/ui-devkit": "^1.7.4",
     "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.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/core": "^1.7.3",
+    "@vendure/common": "^1.7.4",
+    "@vendure/core": "^1.7.4",
     "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.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/core": "^1.7.3",
+    "@vendure/common": "^1.7.4",
+    "@vendure/core": "^1.7.4",
     "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.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/core": "^1.7.3",
+    "@vendure/common": "^1.7.4",
+    "@vendure/core": "^1.7.4",
     "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.7.3",
+    "version": "1.7.4",
     "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.7.3",
-        "@vendure/core": "^1.7.3",
-        "@vendure/testing": "^1.7.3",
+        "@vendure/common": "^1.7.4",
+        "@vendure/core": "^1.7.4",
+        "@vendure/testing": "^1.7.4",
         "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.7.3",
+  "version": "1.7.4",
   "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.7.3",
+    "@vendure/common": "^1.7.4",
     "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.7.3",
+    "@vendure/core": "^1.7.4",
     "mysql": "^2.18.1",
     "pg": "^8.4.0",
     "rimraf": "^3.0.0",

+ 4 - 0
packages/testing/src/test-server.ts

@@ -114,6 +114,10 @@ export class TestServer {
                 cors: config.apiOptions.cors,
                 logger: new Logger(),
             });
+            const earlyMiddlewares = config.apiOptions.middleware.filter(mid => mid.beforeListen);
+            earlyMiddlewares.forEach(mid => {
+                app.use(mid.route, mid.handler);
+            });
             await app.listen(config.apiOptions.port);
             await app.get(JobQueueService).start();
             DefaultLogger.restoreOriginalLogLevel();

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "1.7.3",
+  "version": "1.7.4",
   "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.7.3",
-    "@vendure/common": "^1.7.3",
+    "@vendure/admin-ui": "^1.7.4",
+    "@vendure/common": "^1.7.4",
     "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.7.3",
+    "@vendure/core": "^1.7.4",
     "rimraf": "^3.0.2",
     "rollup": "^2.40.0",
     "rollup-plugin-terser": "^7.0.2",