Ver Fonte

Merge branch 'minor' into major

Michael Bromley há 3 anos atrás
pai
commit
4d7473d8eb
26 ficheiros alterados com 226 adições e 43 exclusões
  1. 21 0
      CHANGELOG.md
  2. 6 2
      docs/content/developer-guide/customizing-models.md
  3. 6 1
      packages/admin-ui/src/lib/catalog/src/components/option-value-input/option-value-input.component.ts
  4. 22 0
      packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.spec.ts
  5. 7 2
      packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts
  6. 3 0
      packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts
  7. 9 0
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.spec.ts
  8. 8 3
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.ts
  9. 1 10
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html
  10. 3 2
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.ts
  11. 17 17
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html
  12. 4 0
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.scss
  13. 9 1
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts
  14. 1 0
      packages/admin-ui/src/lib/static/styles/_variables.scss
  15. 1 1
      packages/admin-ui/src/lib/static/styles/global/_sass-overrides.scss
  16. 33 0
      packages/core/e2e/customer-group.e2e-spec.ts
  17. 34 0
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  18. 1 1
      packages/core/src/api/config/generate-permissions.ts
  19. 11 0
      packages/core/src/api/decorators/allow.decorator.ts
  20. 13 1
      packages/core/src/api/resolvers/admin/customer.resolver.ts
  21. 1 0
      packages/core/src/api/schema/common/order.type.graphql
  22. 3 0
      packages/core/src/plugin/default-search-plugin/fulltext-search.service.ts
  23. 6 1
      packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts
  24. 1 1
      packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts
  25. 1 0
      packages/core/src/service/services/customer-group.service.ts
  26. 4 0
      packages/create/templates/vendure-config.hbs

+ 21 - 0
CHANGELOG.md

@@ -1,3 +1,24 @@
+## <small>1.7.2 (2022-09-19)</small>
+
+
+#### Fixes
+
+* **admin-ui** Correctly parse configurable args when not edited ([f753f76](https://github.com/vendure-ecommerce/vendure/commit/f753f76)), closes [#1786](https://github.com/vendure-ecommerce/vendure/issues/1786)
+* **admin-ui** Do not allow duplicate option names ([4c4ad29](https://github.com/vendure-ecommerce/vendure/commit/4c4ad29)), closes [#1747](https://github.com/vendure-ecommerce/vendure/issues/1747)
+* **admin-ui** Fix alignment of breadcrumbs ([3584ef9](https://github.com/vendure-ecommerce/vendure/commit/3584ef9))
+* **admin-ui** Fix error when modifying Order with custom field relation ([eace1c1](https://github.com/vendure-ecommerce/vendure/commit/eace1c1)), closes [#1792](https://github.com/vendure-ecommerce/vendure/issues/1792)
+* **admin-ui** Fix error when using non-standard currencyCode ([4466b24](https://github.com/vendure-ecommerce/vendure/commit/4466b24)), closes [#1768](https://github.com/vendure-ecommerce/vendure/issues/1768)
+* **admin-ui** Fix refresh issues with customer group list view ([04b431c](https://github.com/vendure-ecommerce/vendure/commit/04b431c))
+* **core** Add missing driver options in DefaultSearchPlugin ([12e2807](https://github.com/vendure-ecommerce/vendure/commit/12e2807))
+* **core** Correctly escape search term for mysql strategy ([2fa7fcf](https://github.com/vendure-ecommerce/vendure/commit/2fa7fcf)), closes [#1789](https://github.com/vendure-ecommerce/vendure/issues/1789)
+* **core** Correctly escape search term for postgres strategy ([ec70228](https://github.com/vendure-ecommerce/vendure/commit/ec70228)), closes [#1789](https://github.com/vendure-ecommerce/vendure/issues/1789)
+* **core** Correctly populate shipping/billing address for new customer ([264b326](https://github.com/vendure-ecommerce/vendure/commit/264b326))
+* **core** Handle edge-case of Collection.breadcrumbs having null values ([4a9ec5c](https://github.com/vendure-ecommerce/vendure/commit/4a9ec5c))
+* **core** Include missing id field in ShippingLine type ([481d0de](https://github.com/vendure-ecommerce/vendure/commit/481d0de)), closes [#1792](https://github.com/vendure-ecommerce/vendure/issues/1792)
+* **core** Remove deleted Customers from any CustomerGroups ([9820d9e](https://github.com/vendure-ecommerce/vendure/commit/9820d9e)), closes [#1785](https://github.com/vendure-ecommerce/vendure/issues/1785)
+* **create** Fix default migration path of scaffold (#1759) ([e1c90cc](https://github.com/vendure-ecommerce/vendure/commit/e1c90cc)), closes [#1759](https://github.com/vendure-ecommerce/vendure/issues/1759)
+* **create** Make dotenv a dependency, not devDependency ([a641beb](https://github.com/vendure-ecommerce/vendure/commit/a641beb))
+
 ## <small>1.7.1 (2022-08-29)</small>
 ## <small>1.7.1 (2022-08-29)</small>
 
 
 
 

+ 6 - 2
docs/content/developer-guide/customizing-models.md

@@ -204,9 +204,13 @@ However, this sacrifices type safety. To make our custom fields type-safe we can
 
 
 ```TypeScript
 ```TypeScript
 // types.ts
 // types.ts
-import { CustomProductFields } from '@vendure/core';
 
 
-declare module '@vendure/core' {
+// Note: we are using deep a import here, rather than importing from `@vendure/core` due to 
+// a possible bug in TypeScript (https://github.com/microsoft/TypeScript/issues/46617) which
+// causes issues when multiple plugins extend the same custom fields interface.
+import { CustomProductFields } from '@vendure/core/dist/entity/custom-entity-fields';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
   interface CustomProductFields {
   interface CustomProductFields {
     infoUrl: string;
     infoUrl: string;
     downloadable: boolean;
     downloadable: boolean;

+ 6 - 1
packages/admin-ui/src/lib/catalog/src/components/option-value-input/option-value-input.component.ts

@@ -140,7 +140,12 @@ export class OptionValueInputComponent implements ControlValueAccessor {
     }
     }
 
 
     private addOptionValue() {
     private addOptionValue() {
-        const options = this.parseInputIntoOptions(this.input);
+        const options = this.parseInputIntoOptions(this.input).filter(option => {
+            // do not add an option with the same name
+            // as an existing option
+            const existing = this.options ?? this.formValue;
+            return !existing?.find(o => o?.name === option.name);
+        });
         if (!this.formValue && this.options) {
         if (!this.formValue && this.options) {
             for (const option of options) {
             for (const option of options) {
                 this.add.emit(option);
                 this.add.emit(option);

+ 22 - 0
packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.spec.ts

@@ -143,4 +143,26 @@ describe('toConfigurableOperationInput()', () => {
             ],
             ],
         });
         });
     });
     });
+
+    it('works with array input', () => {
+        const value = toConfigurableOperationInput(configOp, {
+            args: [
+                { name: 'option2', value: 'value2-2' },
+                { name: 'option1', value: 'value1-2' },
+            ],
+        });
+        expect(value).toEqual({
+            code: 'test',
+            arguments: [
+                {
+                    name: 'option1',
+                    value: 'value1-2',
+                },
+                {
+                    name: 'option2',
+                    value: 'value2-2',
+                },
+            ],
+        });
+    });
 });
 });

+ 7 - 2
packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts

@@ -64,12 +64,17 @@ export function configurableDefinitionToInstance(
  */
  */
 export function toConfigurableOperationInput(
 export function toConfigurableOperationInput(
     operation: ConfigurableOperation,
     operation: ConfigurableOperation,
-    formValueOperations: { args: Record<string, any> },
+    formValueOperations: { args: Record<string, string> | Array<{ name: string; value: string }> },
 ): ConfigurableOperationInput {
 ): ConfigurableOperationInput {
+    const argsArray = Array.isArray(formValueOperations.args) ? formValueOperations.args : undefined;
+    const argsMap = !Array.isArray(formValueOperations.args) ? formValueOperations.args : undefined;
     return {
     return {
         code: operation.code,
         code: operation.code,
         arguments: operation.args.map(({ name, value }, j) => {
         arguments: operation.args.map(({ name, value }, j) => {
-            const formValue = formValueOperations.args[name];
+            const formValue = argsArray?.find(arg => arg.name === name)?.value ?? argsMap?.[name];
+            if (!formValue) {
+                throw new Error(`Cannot find an argument value for the key "${name}"`);
+            }
             return {
             return {
                 name,
                 name,
                 value: formValue?.hasOwnProperty('value')
                 value: formValue?.hasOwnProperty('value')

+ 3 - 0
packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.ts

@@ -32,6 +32,9 @@ export function isEntityCreateOrUpdateMutation(documentNode: DocumentNode): stri
             if (inputTypeName === 'UpdateActiveAdministratorInput') {
             if (inputTypeName === 'UpdateActiveAdministratorInput') {
                 return 'Administrator';
                 return 'Administrator';
             }
             }
+            if (inputTypeName === 'ModifyOrderInput') {
+                return 'Order';
+            }
 
 
             const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX);
             const createMatch = inputTypeName.match(CREATE_ENTITY_REGEX);
             if (createMatch) {
             if (createMatch) {

+ 9 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.spec.ts

@@ -16,4 +16,13 @@ describe('LocaleCurrencyPipe', () => {
         expect(pipe.transform(123, CurrencyCode.EUR, LanguageCode.de)).toBe('1,23 €');
         expect(pipe.transform(123, CurrencyCode.EUR, LanguageCode.de)).toBe('1,23 €');
         expect(pipe.transform(4200000, CurrencyCode.EUR, LanguageCode.de)).toBe('42.000,00 €');
         expect(pipe.transform(4200000, CurrencyCode.EUR, LanguageCode.de)).toBe('42.000,00 €');
     });
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/1768
+    it('Custom currency code in English', () => {
+        const pipe = new LocaleCurrencyPipe();
+        const customCurrencyCode = 'FLTH';
+        expect(pipe.transform(1, customCurrencyCode, LanguageCode.en)).toBe('0.01');
+        expect(pipe.transform(123, customCurrencyCode, LanguageCode.en)).toBe('1.23');
+        expect(pipe.transform(4200000, customCurrencyCode, LanguageCode.en)).toBe('42000.00');
+    });
 });
 });

+ 8 - 3
packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.ts

@@ -30,9 +30,14 @@ export class LocaleCurrencyPipe extends LocaleBasePipe implements PipeTransform
         if (typeof value === 'number' && typeof currencyCode === 'string') {
         if (typeof value === 'number' && typeof currencyCode === 'string') {
             const activeLocale = this.getActiveLocale(locale);
             const activeLocale = this.getActiveLocale(locale);
             const majorUnits = value / 100;
             const majorUnits = value / 100;
-            return new Intl.NumberFormat(activeLocale, { style: 'currency', currency: currencyCode }).format(
-                majorUnits,
-            );
+            try {
+                return new Intl.NumberFormat(activeLocale, {
+                    style: 'currency',
+                    currency: currencyCode,
+                }).format(majorUnits);
+            } catch (e: any) {
+                return majorUnits.toFixed(2);
+            }
         }
         }
         return value;
         return value;
     }
     }

+ 1 - 10
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html

@@ -50,7 +50,7 @@
                         {{ 'common.edit' | translate }}
                         {{ 'common.edit' | translate }}
                     </button>
                     </button>
                 </td>
                 </td>
-                <td [class.active]="group.id === activeGroupId">
+                <td [class.active]="group.id === activeGroupId" class="align-middle">
                     <vdr-dropdown>
                     <vdr-dropdown>
                         <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
                         <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
                             {{ 'common.actions' | translate }}
                             {{ 'common.actions' | translate }}
@@ -71,15 +71,6 @@
                 </td>
                 </td>
             </ng-template>
             </ng-template>
         </vdr-data-table>
         </vdr-data-table>
-
-        <table class="table mt0" *ngIf="!(listIsEmpty$ | async); else emptyPlaceholder">
-            <tbody>
-                <tr
-                    *ngFor="let group of items$ | async"
-                    [class.active]="group.id === (activeGroup$ | async)?.id"
-                ></tr>
-            </tbody>
-        </table>
     </div>
     </div>
     <ng-template #emptyPlaceholder>
     <ng-template #emptyPlaceholder>
         <vdr-empty-placeholder></vdr-empty-placeholder>
         <vdr-empty-placeholder></vdr-empty-placeholder>

+ 3 - 2
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.ts

@@ -143,11 +143,10 @@ export class CustomerGroupListComponent
                         ? this.dataService.customer.createCustomerGroup({ ...result, customerIds: [] })
                         ? this.dataService.customer.createCustomerGroup({ ...result, customerIds: [] })
                         : EMPTY,
                         : EMPTY,
                 ),
                 ),
-                // refresh list
-                switchMap(() => this.dataService.customer.getCustomerGroupList().single$),
             )
             )
             .subscribe(
             .subscribe(
                 () => {
                 () => {
+                    this.refresh();
                     this.notificationService.success(_('common.notify-create-success'), {
                     this.notificationService.success(_('common.notify-create-success'), {
                         entity: 'CustomerGroup',
                         entity: 'CustomerGroup',
                     });
                     });
@@ -190,6 +189,7 @@ export class CustomerGroupListComponent
                     if (typeof result.errorMessage === 'string') {
                     if (typeof result.errorMessage === 'string') {
                         this.notificationService.error(result.errorMessage);
                         this.notificationService.error(result.errorMessage);
                     } else {
                     } else {
+                        this.refresh();
                         this.notificationService.success(_('common.notify-delete-success'), {
                         this.notificationService.success(_('common.notify-delete-success'), {
                             entity: 'CustomerGroup',
                             entity: 'CustomerGroup',
                         });
                         });
@@ -215,6 +215,7 @@ export class CustomerGroupListComponent
             )
             )
             .subscribe(
             .subscribe(
                 () => {
                 () => {
+                    this.refresh();
                     this.notificationService.success(_('common.notify-update-success'), {
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'CustomerGroup',
                         entity: 'CustomerGroup',
                     });
                     });

+ 17 - 17
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html

@@ -63,14 +63,12 @@
                             ></vdr-line-fulfillment>
                             ></vdr-line-fulfillment>
                         </td>
                         </td>
                         <td *ngIf="orderLineCustomFields.length" class="order-line-custom-field align-middle">
                         <td *ngIf="orderLineCustomFields.length" class="order-line-custom-field align-middle">
-                            <ng-container *ngFor="let customField of orderLineCustomFields">
-                                <vdr-custom-field-control
-                                    [customField]="customField"
-                                    [customFieldsFormGroup]="orderLineCustomFieldsFormArray.get([i])"
-                                    entityName="OrderLine"
-                                    [compact]="true"
-                                ></vdr-custom-field-control>
-                            </ng-container>
+                            <vdr-tabbed-custom-fields
+                                entityName="OrderLine"
+                                [customFields]="orderLineCustomFields"
+                                [customFieldsFormGroup]="orderLineCustomFieldsFormArray.get([i])"
+                                [compact]="true"
+                            ></vdr-tabbed-custom-fields>
                         </td>
                         </td>
                         <td class="align-middle total">
                         <td class="align-middle total">
                             {{ line.linePriceWithTax | localeCurrency: order.currencyCode }}
                             {{ line.linePriceWithTax | localeCurrency: order.currencyCode }}
@@ -241,7 +239,8 @@
                             [formControl]="couponCodesControl"
                             [formControl]="couponCodesControl"
                         >
                         >
                             <ng-template ng-option-tmp let-item="item">
                             <ng-template ng-option-tmp let-item="item">
-                                <vdr-chip>{{ item.code }}</vdr-chip> {{ item.promotionName }}
+                                <vdr-chip>{{ item.code }}</vdr-chip>
+                                {{ item.promotionName }}
                             </ng-template>
                             </ng-template>
                         </ng-select>
                         </ng-select>
                     </clr-accordion-content>
                     </clr-accordion-content>
@@ -257,13 +256,13 @@
                             <vdr-form-field [label]="'order.product-sku' | translate" for="sku"
                             <vdr-form-field [label]="'order.product-sku' | translate" for="sku"
                                 ><input id="sku" type="text" formControlName="sku"
                                 ><input id="sku" type="text" formControlName="sku"
                             /></vdr-form-field>
                             /></vdr-form-field>
-                            <vdr-form-field [label]="'common.price' | translate" for="price"
-                                ><vdr-currency-input
+                            <vdr-form-field [label]="'common.price' | translate" for="price">
+                                <vdr-currency-input
                                     [currencyCode]="order.currencyCode"
                                     [currencyCode]="order.currencyCode"
                                     id="price"
                                     id="price"
                                     formControlName="price"
                                     formControlName="price"
-                                ></vdr-currency-input
-                            ></vdr-form-field>
+                                ></vdr-currency-input>
+                            </vdr-form-field>
                             <vdr-form-field
                             <vdr-form-field
                                 [label]="
                                 [label]="
                                     'catalog.price-includes-tax-at'
                                     'catalog.price-includes-tax-at'
@@ -276,15 +275,16 @@
                                     clrCheckbox
                                     clrCheckbox
                                     formControlName="priceIncludesTax"
                                     formControlName="priceIncludesTax"
                             /></vdr-form-field>
                             /></vdr-form-field>
-                            <vdr-form-field [label]="'order.tax-rate' | translate" for="taxRate"
-                                ><vdr-affixed-input suffix="%"
+                            <vdr-form-field [label]="'order.tax-rate' | translate" for="taxRate">
+                                <vdr-affixed-input suffix="%"
                                     ><input
                                     ><input
                                         id="taxRate"
                                         id="taxRate"
                                         type="number"
                                         type="number"
                                         min="0"
                                         min="0"
                                         max="100"
                                         max="100"
-                                        formControlName="taxRate" /></vdr-affixed-input
-                            ></vdr-form-field>
+                                        formControlName="taxRate"
+                                /></vdr-affixed-input>
+                            </vdr-form-field>
                             <vdr-form-field [label]="'order.tax-description' | translate" for="taxDescription"
                             <vdr-form-field [label]="'order.tax-description' | translate" for="taxDescription"
                                 ><input id="taxDescription" type="text" formControlName="taxDescription"
                                 ><input id="taxDescription" type="text" formControlName="taxDescription"
                             /></vdr-form-field>
                             /></vdr-form-field>

+ 4 - 0
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.scss

@@ -6,4 +6,8 @@
     tr.modified td {
     tr.modified td {
         background-color: var(--color-warning-100);
         background-color: var(--color-warning-100);
     }
     }
+
+    .order-line-custom-field {
+        text-align: start;
+    }
 }
 }

+ 9 - 1
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts

@@ -20,8 +20,10 @@ import {
     ServerConfigService,
     ServerConfigService,
     SortOrder,
     SortOrder,
     SurchargeInput,
     SurchargeInput,
+    transformRelationCustomFieldInputs,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 import { concat, EMPTY, Observable, of, Subject } from 'rxjs';
 import { concat, EMPTY, Observable, of, Subject } from 'rxjs';
 import {
 import {
     distinctUntilChanged,
     distinctUntilChanged,
@@ -379,8 +381,14 @@ export class OrderEditorComponent
     }
     }
 
 
     previewAndModify(order: OrderDetailFragment) {
     previewAndModify(order: OrderDetailFragment) {
-        const input: ModifyOrderInput = {
+        const modifyOrderInput: ModifyOrderData = {
             ...this.modifyOrderInput,
             ...this.modifyOrderInput,
+            adjustOrderLines: this.modifyOrderInput.adjustOrderLines.map(line => {
+                return transformRelationCustomFieldInputs(simpleDeepClone(line), this.orderLineCustomFields);
+            }),
+        };
+        const input: ModifyOrderInput = {
+            ...modifyOrderInput,
             ...(this.billingAddressForm.dirty ? { updateBillingAddress: this.billingAddressForm.value } : {}),
             ...(this.billingAddressForm.dirty ? { updateBillingAddress: this.billingAddressForm.value } : {}),
             ...(this.shippingAddressForm.dirty
             ...(this.shippingAddressForm.dirty
                 ? { updateShippingAddress: this.shippingAddressForm.value }
                 ? { updateShippingAddress: this.shippingAddressForm.value }

+ 1 - 0
packages/admin-ui/src/lib/static/styles/_variables.scss

@@ -1,3 +1,4 @@
+@import "./global/sass-overrides";
 @import "~@clr/ui/src/utils/_mixins";
 @import "~@clr/ui/src/utils/_mixins";
 @import "~@clr/ui/src/utils/_variables.clarity.scss";
 @import "~@clr/ui/src/utils/_variables.clarity.scss";
 
 

+ 1 - 1
packages/admin-ui/src/lib/static/styles/global/_sass-overrides.scss

@@ -1,3 +1,3 @@
-// Note: variables defined in this file should use the default! flag so they can be
+// Note: variables defined in this file should use the !default flag so they can be
 // overridden by custom Admin UI applications
 // overridden by custom Admin UI applications
 $clr-sidenav-width: 10.8rem !default;
 $clr-sidenav-width: 10.8rem !default;

+ 33 - 0
packages/core/e2e/customer-group.e2e-spec.ts

@@ -11,6 +11,7 @@ import { DeletionResult } from './graphql/generated-e2e-shop-types';
 import {
 import {
     ADD_CUSTOMERS_TO_GROUP,
     ADD_CUSTOMERS_TO_GROUP,
     CREATE_CUSTOMER_GROUP,
     CREATE_CUSTOMER_GROUP,
+    DELETE_CUSTOMER,
     DELETE_CUSTOMER_GROUP,
     DELETE_CUSTOMER_GROUP,
     GET_CUSTOMER_GROUP,
     GET_CUSTOMER_GROUP,
     GET_CUSTOMER_GROUPS,
     GET_CUSTOMER_GROUPS,
@@ -276,4 +277,36 @@ describe('CustomerGroup resolver', () => {
         );
         );
         expect(customerGroups.totalItems).toBe(0);
         expect(customerGroups.totalItems).toBe(0);
     });
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/1785
+    it('removes customer from group when customer is deleted', async () => {
+        const customer5Id = customers[4].id;
+        const { createCustomerGroup } = await adminClient.query<
+            Codegen.CreateCustomerGroupMutation,
+            Codegen.CreateCustomerGroupMutationVariables
+        >(CREATE_CUSTOMER_GROUP, {
+            input: {
+                name: 'group-1785',
+                customerIds: [customer5Id],
+            },
+        });
+
+        expect(createCustomerGroup.customers.items).toEqual([{ id: customer5Id }]);
+
+        await adminClient.query<Codegen.DeleteCustomerMutation, Codegen.DeleteCustomerMutationVariables>(
+            DELETE_CUSTOMER,
+            {
+                id: customer5Id,
+            },
+        );
+
+        const { customerGroup } = await adminClient.query<
+            Codegen.GetCustomerGroupQuery,
+            Codegen.GetCustomerGroupQueryVariables
+        >(GET_CUSTOMER_GROUP, {
+            id: createCustomerGroup.id,
+        });
+
+        expect(customerGroup?.customers.items).toEqual([]);
+    });
 });
 });

+ 34 - 0
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -1682,6 +1682,40 @@ describe('Default search plugin', () => {
                 expect(result.search.items.length).toEqual(2);
                 expect(result.search.items.length).toEqual(2);
             });
             });
         });
         });
+
+        // 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>(
+                    SEARCH_PRODUCTS,
+                    {
+                        input: {
+                            take: 10,
+                            term: 'laptop & camera',
+                        },
+                    },
+                    {
+                        languageCode: LanguageCode.de,
+                    },
+                );
+                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,
+                    },
+                );
+                expect(result.search.items).toBeDefined();
+            });
+        });
     });
     });
 });
 });
 
 

+ 1 - 1
packages/core/src/api/config/generate-permissions.ts

@@ -11,7 +11,7 @@ GraphQL resolvers via the {@link Allow} decorator.
 
 
 ## Understanding Permission.Owner
 ## Understanding Permission.Owner
 
 
-\`Permission.Owner\` is a special permission which is used in some of the Vendure resolvers to indicate that that resolver should only
+\`Permission.Owner\` is a special permission which is used in some Vendure resolvers to indicate that that resolver should only
 be accessible to the "owner" of that resource.
 be accessible to the "owner" of that resource.
 
 
 For example, the Shop API \`activeCustomer\` query resolver should only return the Customer object for the "owner" of that Customer, i.e.
 For example, the Shop API \`activeCustomer\` query resolver should only return the Customer object for the "owner" of that Customer, i.e.

+ 11 - 0
packages/core/src/api/decorators/allow.decorator.ts

@@ -12,6 +12,17 @@ export const PERMISSIONS_METADATA_KEY = '__permissions__';
  *
  *
  * For REST controllers, it can be applied to route handlers.
  * For REST controllers, it can be applied to route handlers.
  *
  *
+ * ## Allow and Sessions
+ * The `@Allow()` decorator is closely linked to the way Vendure manages sessions. For any operation or route that is decorated
+ * with `@Allow()`, there must be an authenticated session in progress, which would have been created during a prior authentication
+ * step.
+ *
+ * The exception to this is when the operation is decorated with `@Allow(Permission.Owner)`. This is a special permission which is designed
+ * to give access to certain resources to potentially un-authenticated users. For this reason, any operation decorated with this permission
+ * will always have an anonymous session created if no session is currently in progress.
+ *
+ * For more information see [Understanding Permission.Owner](/docs/typescript-api/common/permission/#understanding-permissionowner).
+ *
  * @example
  * @example
  * ```TypeScript
  * ```TypeScript
  *  \@Allow(Permission.SuperAdmin)
  *  \@Allow(Permission.SuperAdmin)

+ 13 - 1
packages/core/src/api/resolvers/admin/customer.resolver.ts

@@ -22,6 +22,7 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { ErrorResultUnion } from '../../../common/error/error-result';
 import { ErrorResultUnion } from '../../../common/error/error-result';
 import { Address } from '../../../entity/address/address.entity';
 import { Address } from '../../../entity/address/address.entity';
 import { Customer } from '../../../entity/customer/customer.entity';
 import { Customer } from '../../../entity/customer/customer.entity';
+import { CustomerGroupService } from '../../../service/index';
 import { CustomerService } from '../../../service/services/customer.service';
 import { CustomerService } from '../../../service/services/customer.service';
 import { OrderService } from '../../../service/services/order.service';
 import { OrderService } from '../../../service/services/order.service';
 import { RequestContext } from '../../common/request-context';
 import { RequestContext } from '../../common/request-context';
@@ -32,7 +33,11 @@ import { Transaction } from '../../decorators/transaction.decorator';
 
 
 @Resolver()
 @Resolver()
 export class CustomerResolver {
 export class CustomerResolver {
-    constructor(private customerService: CustomerService, private orderService: OrderService) {}
+    constructor(
+        private customerService: CustomerService,
+        private customerGroupService: CustomerGroupService,
+        private orderService: OrderService,
+    ) {}
 
 
     @Query()
     @Query()
     @Allow(Permission.ReadCustomer)
     @Allow(Permission.ReadCustomer)
@@ -117,6 +122,13 @@ export class CustomerResolver {
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationDeleteCustomerArgs,
         @Args() args: MutationDeleteCustomerArgs,
     ): Promise<DeletionResponse> {
     ): Promise<DeletionResponse> {
+        const groups = await this.customerService.getCustomerGroups(ctx, args.id);
+        for (const group of groups) {
+            await this.customerGroupService.removeCustomersFromGroup(ctx, {
+                customerGroupId: group.id,
+                customerIds: [args.id],
+            });
+        }
         return this.customerService.softDelete(ctx, args.id);
         return this.customerService.softDelete(ctx, args.id);
     }
     }
 
 

+ 1 - 0
packages/core/src/api/schema/common/order.type.graphql

@@ -93,6 +93,7 @@ type OrderList implements PaginatedList {
 }
 }
 
 
 type ShippingLine {
 type ShippingLine {
+    id: ID!
     shippingMethod: ShippingMethod!
     shippingMethod: ShippingMethod!
     price: Int!
     price: Int!
     priceWithTax: Int!
     priceWithTax: Int!

+ 3 - 0
packages/core/src/plugin/default-search-plugin/fulltext-search.service.ts

@@ -117,6 +117,7 @@ export class FulltextSearchService {
             switch (this.connection.rawConnection.options.type) {
             switch (this.connection.rawConnection.options.type) {
                 case 'mysql':
                 case 'mysql':
                 case 'mariadb':
                 case 'mariadb':
+                case 'aurora-data-api':
                     this._searchStrategy = new MysqlSearchStrategy();
                     this._searchStrategy = new MysqlSearchStrategy();
                     break;
                     break;
                 case 'sqlite':
                 case 'sqlite':
@@ -125,6 +126,8 @@ export class FulltextSearchService {
                     this._searchStrategy = new SqliteSearchStrategy();
                     this._searchStrategy = new SqliteSearchStrategy();
                     break;
                     break;
                 case 'postgres':
                 case 'postgres':
+                case 'aurora-data-api-pg':
+                case 'cockroachdb':
                     this._searchStrategy = new PostgresSearchStrategy();
                     this._searchStrategy = new PostgresSearchStrategy();
                     break;
                     break;
                 default:
                 default:

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

@@ -149,6 +149,7 @@ export class MysqlSearchStrategy implements SearchStrategy {
             input;
             input;
 
 
         if (term && term.length > this.minTermLength) {
         if (term && term.length > this.minTermLength) {
+            const safeTerm = term.replace(/"/g, '');
             const termScoreQuery = this.connection
             const termScoreQuery = this.connection
                 .getRepository(ctx, SearchIndexItem)
                 .getRepository(ctx, SearchIndexItem)
                 .createQueryBuilder('si_inner')
                 .createQueryBuilder('si_inner')
@@ -171,7 +172,11 @@ export class MysqlSearchStrategy implements SearchStrategy {
                     }),
                     }),
                 )
                 )
                 .andWhere('si_inner.channelId = :channelId')
                 .andWhere('si_inner.channelId = :channelId')
-                .setParameters({ term: `${term}*`, like_term: `%${term}%`, channelId: ctx.channelId });
+                .setParameters({
+                    term: `${safeTerm}*`,
+                    like_term: `%${safeTerm}%`,
+                    channelId: ctx.channelId,
+                });
 
 
             qb.innerJoin(`(${termScoreQuery.getQuery()})`, 'term_result', 'inner_productId = si.productId')
             qb.innerJoin(`(${termScoreQuery.getQuery()})`, 'term_result', 'inner_productId = si.productId')
                 .addSelect(input.groupByProduct ? 'MAX(term_result.score)' : 'term_result.score', 'score')
                 .addSelect(input.groupByProduct ? 'MAX(term_result.score)' : 'term_result.score', 'score')

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

@@ -155,7 +155,7 @@ export class PostgresSearchStrategy implements SearchStrategy {
             ? term
             ? term
                   .trim()
                   .trim()
                   .split(/\s+/g)
                   .split(/\s+/g)
-                  .map(t => `${t}:*`)
+                  .map(t => `'${t}':*`)
                   .join(' & ')
                   .join(' & ')
             : '';
             : '';
 
 

+ 1 - 0
packages/core/src/service/services/customer-group.service.ts

@@ -77,6 +77,7 @@ export class CustomerGroupService {
             .leftJoin('customer.groups', 'group')
             .leftJoin('customer.groups', 'group')
             .leftJoin('customer.channels', 'channel')
             .leftJoin('customer.channels', 'channel')
             .andWhere('group.id = :groupId', { groupId: customerGroupId })
             .andWhere('group.id = :groupId', { groupId: customerGroupId })
+            .andWhere('customer.deletedAt IS NULL', { groupId: customerGroupId })
             .andWhere('channel.id =:channelId', { channelId: ctx.channelId })
             .andWhere('channel.id =:channelId', { channelId: ctx.channelId })
             .getManyAndCount()
             .getManyAndCount()
             .then(([items, totalItems]) => ({ items, totalItems }));
             .then(([items, totalItems]) => ({ items, totalItems }));

+ 4 - 0
packages/create/templates/vendure-config.hbs

@@ -73,6 +73,10 @@ export const config: VendureConfig = {
         AssetServerPlugin.init({
         AssetServerPlugin.init({
             route: 'assets',
             route: 'assets',
             assetUploadDir: path.join(__dirname, '../static/assets'),
             assetUploadDir: path.join(__dirname, '../static/assets'),
+            // For local dev, the correct value for assetUrlPrefix should
+            // be guessed correctly, but for production it will usually need
+            // to be set manually to match your production url.
+            assetUrlPrefix: IS_DEV ? undefined : 'https://www.my-shop.com/assets',
         }),
         }),
         DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }),
         DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }),
         DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: true }),
         DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: true }),