Browse Source

Merge branch 'master' into next

Michael Bromley 5 years ago
parent
commit
e1fe622252
46 changed files with 347 additions and 120 deletions
  1. 71 0
      .github/workflows/codeql-analysis.yml
  2. 16 0
      CHANGELOG.md
  3. 2 2
      docs/content/article/faq.md
  4. 2 2
      docs/content/article/roadmap.md
  5. 2 1
      docs/content/docs/developer-guide/channels.md
  6. 9 0
      docs/content/docs/developer-guide/deployment.md
  7. 1 1
      docs/content/docs/plugins/available-plugins.md
  8. 6 0
      docs/layouts/partials/footer.html
  9. 4 0
      docs/layouts/partials/top-bar.html
  10. 1 1
      lerna.json
  11. 2 2
      packages/admin-ui-plugin/package.json
  12. 1 1
      packages/admin-ui/package.json
  13. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  14. 6 4
      packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.ts
  15. 22 2
      packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts
  16. 7 0
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts
  17. 1 1
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  18. 6 1
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts
  19. 2 2
      packages/asset-server-plugin/package.json
  20. 1 1
      packages/core/package.json
  21. 1 1
      packages/core/src/api/common/id-codec.ts
  22. 1 0
      packages/core/src/api/schema/type/order.type.graphql
  23. 2 2
      packages/core/src/config/config.service.mock.ts
  24. 1 1
      packages/core/src/config/config.service.ts
  25. 7 3
      packages/core/src/config/entity-id-strategy/auto-increment-id-strategy.ts
  26. 3 5
      packages/core/src/config/entity-id-strategy/base64-id-strategy.ts
  27. 33 31
      packages/core/src/config/entity-id-strategy/entity-id-strategy.ts
  28. 18 3
      packages/core/src/config/entity-id-strategy/uuid-id-strategy.ts
  29. 1 1
      packages/core/src/config/vendure-config.ts
  30. 5 0
      packages/core/src/entity/order/order.entity.ts
  31. 3 3
      packages/core/src/entity/set-entity-id-strategy.ts
  32. 1 1
      packages/core/src/plugin/default-job-queue-plugin/sql-job-queue-strategy.ts
  33. 2 2
      packages/create/package.json
  34. 8 8
      packages/dev-server/package.json
  35. 1 1
      packages/elasticsearch-plugin/README.md
  36. 2 2
      packages/elasticsearch-plugin/package.json
  37. 6 2
      packages/elasticsearch-plugin/src/elasticsearch.service.ts
  38. 27 8
      packages/elasticsearch-plugin/src/options.ts
  39. 36 8
      packages/elasticsearch-plugin/src/plugin.ts
  40. 2 2
      packages/email-plugin/package.json
  41. 11 1
      packages/email-plugin/src/default-email-handlers.ts
  42. 5 5
      packages/email-plugin/src/event-handler.ts
  43. 1 1
      packages/email-plugin/templates/order-confirmation/body.hbs
  44. 2 2
      packages/testing/package.json
  45. 2 2
      packages/testing/src/config/testing-entity-id-strategy.ts
  46. 3 3
      packages/ui-devkit/package.json

+ 71 - 0
.github/workflows/codeql-analysis.yml

@@ -0,0 +1,71 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+name: "CodeQL"
+
+on:
+  push:
+    branches: [master]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [master]
+  schedule:
+    - cron: '0 5 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+      matrix:
+        # Override automatic language detection by changing the below list
+        # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
+        language: ['javascript']
+        # Learn more...
+        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+      with:
+        # We must fetch at least the immediate parents so that if this is
+        # a pull request then we can checkout the head.
+        fetch-depth: 2
+
+    # If this run was triggered by a pull request event, then checkout
+    # the head of the pull request instead of the merge commit.
+    - run: git checkout HEAD^2
+      if: ${{ github.event_name == 'pull_request' }}
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file. 
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1

+ 16 - 0
CHANGELOG.md

@@ -1,3 +1,19 @@
+## <small>0.15.2 (2020-09-30)</small>
+
+
+#### Fixes
+
+* **admin-ui** Allow cancellation from custom Order states ([117264f](https://github.com/vendure-ecommerce/vendure/commit/117264f)), closes [#472](https://github.com/vendure-ecommerce/vendure/issues/472)
+* **admin-ui** Fix address dialog issues ([0d61f47](https://github.com/vendure-ecommerce/vendure/commit/0d61f47)), closes [#463](https://github.com/vendure-ecommerce/vendure/issues/463)
+* **admin-ui** Fix asset drag/drop support in safari ([55304c5](https://github.com/vendure-ecommerce/vendure/commit/55304c5))
+* **core** Fix handling of JobRecord ids when using UUID strategy ([30e6e70](https://github.com/vendure-ecommerce/vendure/commit/30e6e70)), closes [#478](https://github.com/vendure-ecommerce/vendure/issues/478)
+* **email-plugin** Include shipping method in order receipt handler ([ea907a4](https://github.com/vendure-ecommerce/vendure/commit/ea907a4)), closes [#473](https://github.com/vendure-ecommerce/vendure/issues/473)
+
+#### Features
+
+* **core** Add `totalQuantity` field to Order type ([829ac96](https://github.com/vendure-ecommerce/vendure/commit/829ac96)), closes [#465](https://github.com/vendure-ecommerce/vendure/issues/465)
+* **elasticsearch-plugin** Allow full client options to be passed ([c686509](https://github.com/vendure-ecommerce/vendure/commit/c686509)), closes [#474](https://github.com/vendure-ecommerce/vendure/issues/474)
+
 ## <small>0.15.1 (2020-09-09)</small>
 
 

+ 2 - 2
docs/content/article/faq.md

@@ -41,6 +41,6 @@ We're not yet offering general support packages, but if you are planning a Vendu
 
 **No**, out-of-the box Vendure does not support multi-vendor. We have a [Channels feature]({{< relref "channels" >}}) which allows a single vendor to define multiple sales channels. 
 
-It _would_ be possible to add multi-vendor support by way of a plugin, but bear in mind that this would entail a fair amount of custom development.
+Currently there is ongoing work by community contributors to put in place the internal infrastructure to support multi-vendor, but as of this writing (September 2020) it is not yet considered complete. It _would_ be possible to add multi-vendor support by way of a plugin, but bear in mind that this would entail a fair amount of custom development.
+
 
-An official multi-vendor plugin is under consideration for after the v1.0 release.

+ 2 - 2
docs/content/article/roadmap.md

@@ -11,13 +11,13 @@ Here is a list of some of the main outstanding tasks that are planned for the v1
 * Complete the Channels implementation
 * Back order handling
 * Administrator creation & editing of orders
-* Custom authentication support
+* ~~Custom authentication support~~ ✅
 * Improved promotions support
 * Improved tax calculation support
 * Improved support for running Vendure in cloud environments
 * Performance improvements
 
-We currently hope to **reach v1.0 in the latter half of 2020**. For an up-to-date overview of where we stand, refer to the [GitHub milestones page](https://github.com/vendure-ecommerce/vendure/milestones).
+We currently hope to **reach v1.0 by the end of 2020**. For an up-to-date overview of where we stand, refer to the [GitHub milestones page](https://github.com/vendure-ecommerce/vendure/milestones).
 
 ## Post v1.0
 

+ 2 - 1
docs/content/docs/developer-guide/channels.md

@@ -11,6 +11,7 @@ Channels are a feature of Vendure which allows multiple sales channels to be rep
 * Assign only specific Products to the Channel (with Channel-specific prices)
 * Create Administrator roles limited to the Channel
 * Assign only specific Promotions, Collections & ShippingMethods to the Channel (to be implemented)
+* Have Orders and Customers associated with specific Channels.
 
 Every Vendure server always has a **default Channel**, which contains _all_ entities. Subsequent channels can then contain a subset of the above entities.
 
@@ -22,6 +23,6 @@ Use-cases of Channels include:
 
 ## Multi-Tenant (Marketplace) Support
 
-In its current form, the Channels feature is not suitable for a multi-tenant or marketplace solution. This is because several entities which should be isolated in a true multi-tenant system are still shared across all Channels.
+In its current form, the Channels feature is not suitable for an out-fo-the-box multi-tenant or marketplace solution. This is because several entities which should be isolated in a true multi-tenant system are still shared across all Channels.
 
 Multi-tenancy could still be achieved through a dedicated plugin, and indeed there are some community projects underway in this direction, but would require significant custom work. An out-of-the-box solution will be considered for a future plugin offering.

+ 9 - 0
docs/content/docs/developer-guide/deployment.md

@@ -23,6 +23,15 @@ For a production Vendure server, there are a few security-related points to cons
 * Set the [Superadmin credentials]({{< relref "auth-options" >}}#superadmincredentials) to something other than the default.
 * Consider taking steps to harden your GraphQL APIs against DOS attacks. Use the [ApiOptions]({{< relref "api-options" >}}) to set up appropriate Express middleware for things like [request timeouts](https://github.com/expressjs/express/issues/3330) and [rate limits](https://www.npmjs.com/package/express-rate-limit). A tool such as [graphql-query-complexity](https://github.com/slicknode/graphql-query-complexity) can be used to mitigate resource-intensive GraphQL queries. 
 * You may wish to restrict the Admin API to only be accessed from trusted IPs. This could be achieved for instance by configuring an nginx reverse proxy that sits in front of the Vendure server.
+* By default, Vendure uses auto-increment integer IDs as entity primary keys. While easier to work with in development, sequential primary keys can leak information such as the number of orders or customers in the system. For this reason you should consider using the [UuidIdStrategy]({{< relref "entity-id-strategy" >}}#uuididstrategy) for production.
+  ```TypeScript
+  import { UuidIdStrategy, VendureConfig } from '@vendure/core';
+  
+  export const config: VendureConfig = {
+    entityIdStrategy: new UuidIdStrategy(),
+    // ...
+  }
+  ```
 
 ## Health/Readiness Checks
 

+ 1 - 1
docs/content/docs/plugins/available-plugins.md

@@ -18,5 +18,5 @@ The Vendure monorepo contains a number of "core" plugins - that is, commonly-use
 
 Have you created a Vendure plugin that you'd like to share? Contact us and we can list it here! 
 
-For now, you'll find some community plugins in [these GitHub search results](https://github.com/search?q=vendure+-user%3Avendure-ecommerce&type=Repositories).
+For now, you'll find some community plugins in [these GitHub search results](https://github.com/search?q=vendure+plugin+-user%3Avendure-ecommerce&type=Repositories).
  

+ 6 - 0
docs/layouts/partials/footer.html

@@ -31,6 +31,12 @@
                             Join us on Slack</a
                         >
                     </li>
+                    <li>
+                        <a href="https://github.com/vendure-ecommerce/vendure/discussions"
+                            ><img class="link-icon" alt="GitHub logo" src="/svg/icon-github-inverse.svg" />
+                            Support Forum</a
+                        >
+                    </li>
                     <li>
                         <a href="mailto:contact@vendure.io" class="email-link"
                             ><img src="/svg/clr-icon-email-light.svg" class="link-icon" alt="email icon" />

+ 4 - 0
docs/layouts/partials/top-bar.html

@@ -45,6 +45,10 @@
                         <img class="menu-icon" alt="icon" src="/svg/icon-github-inverse.svg" />
                         GitHub
                     </a>
+                    <a href="https://github.com/vendure-ecommerce/vendure/discussions">
+                        <img class="menu-icon" alt="icon" src="/svg/icon-github-inverse.svg" />
+                        Support Forum
+                    </a>
                     <a href="https://join.slack.com/t/vendure-ecommerce/shared_invite/enQtNzA1NTcyMDY3NTg0LTMzZGQzNDczOWJiMTU2YjAyNWJlMzdmZGE3ZDY5Y2RjMGYxZWNlYTI4NmU4Y2Q1MDNlYzE4MzQ5ODcyYTdmMGU">
                         <img class="menu-icon" alt="icon" src="/logo/slack-logo-icon-127x127.png" />
                         Community

+ 1 - 1
lerna.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -20,7 +20,7 @@
     "@types/express": "^4.0.39",
     "@types/fs-extra": "^8.0.1",
     "@vendure/common": "^0.15.0",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "express": "^4.16.4",
     "rimraf": "^3.0.0",
     "typescript": "3.8.3"

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "license": "MIT",
   "scripts": {
     "ng": "ng",

+ 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 = '0.15.1';
+export const ADMIN_UI_VERSION = '0.15.2';

+ 6 - 4
packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.ts

@@ -51,8 +51,9 @@ export class AssetFileInputComponent implements OnInit {
         this.fitDropZoneToTarget();
     }
 
+    // DragEvent is not supported in Safari, see https://github.com/vendure-ecommerce/vendure/pull/284
     @HostListener('document:dragleave', ['$event'])
-    onDragLeave(event: DragEvent) {
+    onDragLeave(event: any) {
         if (!event.clientX && !event.clientY) {
             this.dragging = false;
         }
@@ -62,15 +63,16 @@ export class AssetFileInputComponent implements OnInit {
      * Preventing this event is required to make dropping work.
      * See https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API#Define_a_drop_zone
      */
-    onDragOver(event: DragEvent) {
+    onDragOver(event: any) {
         event.preventDefault();
     }
 
-    onDrop(event: DragEvent) {
+    // DragEvent is not supported in Safari, see https://github.com/vendure-ecommerce/vendure/pull/284
+    onDrop(event: any) {
         event.preventDefault();
         this.dragging = false;
         this.overDropZone = false;
-        const files = Array.from(event.dataTransfer ? event.dataTransfer.items : [])
+        const files = Array.from<DataTransferItem>(event.dataTransfer ? event.dataTransfer.items : [])
             .map(i => i.getAsFile())
             .filter(notNullOrUndefined);
         this.selectFiles.emit(files);

+ 22 - 2
packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts

@@ -4,11 +4,15 @@ import {
     Component,
     EventEmitter,
     Input,
+    OnChanges,
     OnInit,
     Output,
+    SimpleChanges,
 } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
 import { CustomFieldConfig, GetAvailableCountries, ModalService } from '@vendure/admin-ui/core';
+import { BehaviorSubject } from 'rxjs';
+import { filter, take } from 'rxjs/operators';
 
 import { AddressDetailDialogComponent } from '../address-detail-dialog/address-detail-dialog.component';
 
@@ -18,7 +22,7 @@ import { AddressDetailDialogComponent } from '../address-detail-dialog/address-d
     styleUrls: ['./address-card.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class AddressCardComponent implements OnInit {
+export class AddressCardComponent implements OnInit, OnChanges {
     @Input() addressForm: FormGroup;
     @Input() customFields: CustomFieldConfig;
     @Input() availableCountries: GetAvailableCountries.Items[] = [];
@@ -26,13 +30,29 @@ export class AddressCardComponent implements OnInit {
     @Input() isDefaultShipping: string;
     @Output() setAsDefaultShipping = new EventEmitter<string>();
     @Output() setAsDefaultBilling = new EventEmitter<string>();
+    private dataDependenciesPopulated = new BehaviorSubject<boolean>(false);
 
     constructor(private modalService: ModalService, private changeDetector: ChangeDetectorRef) {}
 
     ngOnInit(): void {
         const streetLine1 = this.addressForm.get('streetLine1') as FormControl;
+        // Make the address dialog display automatically if there is no address line
+        // as is the case when adding a new address.
         if (!streetLine1.value) {
-            this.editAddress();
+            this.dataDependenciesPopulated
+                .pipe(
+                    filter(value => value),
+                    take(1),
+                )
+                .subscribe(() => {
+                    this.editAddress();
+                });
+        }
+    }
+
+    ngOnChanges(changes: SimpleChanges) {
+        if (this.customFields != null && this.availableCountries != null) {
+            this.dataDependenciesPopulated.next(true);
         }
     }
 

+ 7 - 0
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts

@@ -161,6 +161,13 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
             defaultShippingAddress: false,
             defaultBillingAddress: false,
         });
+        if (this.addressCustomFields.length) {
+            const customFieldsGroup = this.formBuilder.group({});
+            for (const fieldDef of this.addressCustomFields) {
+                customFieldsGroup.addControl(fieldDef.name, new FormControl(''));
+            }
+            newAddress.addControl('customFields', customFieldsGroup);
+        }
         addressFormArray.push(newAddress);
     }
 

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html

@@ -37,7 +37,7 @@
                 >
                     <clr-icon shape="error-standard" class="is-error"></clr-icon>
                     <ng-container
-                        *ngIf="order.state !== 'PaymentAuthorized' && !order.active; else cancelOnly"
+                        *ngIf="orderHasSettledPayments(order); else cancelOnly"
                     >
                         {{ 'order.refund-and-cancel-order' | translate }}
                     </ng-container>

+ 6 - 1
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -234,7 +234,8 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
     }
 
     cancelOrRefund(order: OrderDetail.Fragment) {
-        if (order.state === 'PaymentAuthorized' || order.active === true) {
+        const isRefundable = this.orderHasSettledPayments(order);
+        if (order.state === 'PaymentAuthorized' || order.active === true || !isRefundable) {
             this.cancelOrder(order);
         } else {
             this.refundOrder(order);
@@ -338,6 +339,10 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
             });
     }
 
+    orderHasSettledPayments(order: OrderDetail.Fragment): boolean {
+        return !!order.payments?.find(p => p.state === 'Settled');
+    }
+
     private cancelOrder(order: OrderDetail.Fragment) {
         this.modalService
             .fromComponent(CancelOrderDialogComponent, {

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -23,7 +23,7 @@
     "@types/node-fetch": "^2.5.4",
     "@types/sharp": "^0.24.0",
     "@vendure/common": "^0.15.0",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "aws-sdk": "^2.670.0",
     "express": "^4.16.4",
     "node-fetch": "^2.6.0",

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",

+ 1 - 1
packages/core/src/api/common/id-codec.ts

@@ -11,7 +11,7 @@ const ID_KEYS = ['id'];
  * (ProductService etc) all entity IDs are in the form used as the primary key in the database.
  */
 export class IdCodec {
-    constructor(private entityIdStrategy: EntityIdStrategy) {}
+    constructor(private entityIdStrategy: EntityIdStrategy<any>) {}
 
     /**
      * Decode an id from the client into the format used as the database primary key.

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

@@ -18,6 +18,7 @@ type Order implements Node {
     promotions: [Promotion!]!
     payments: [Payment!]
     fulfillments: [Fulfillment!]
+    totalQuantity: Int!
     subTotalBeforeTax: Int!
     "The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied."
     subTotal: Int!

+ 2 - 2
packages/core/src/config/config.service.mock.ts

@@ -52,8 +52,8 @@ export class MockConfigService implements MockClass<ConfigService> {
 export const ENCODED = 'encoded';
 export const DECODED = 'decoded';
 
-export class MockIdStrategy implements EntityIdStrategy {
-    primaryKeyType = 'integer' as any;
+export class MockIdStrategy implements EntityIdStrategy<'increment'> {
+    readonly primaryKeyType = 'increment';
     encodeId = jest.fn().mockReturnValue(ENCODED);
     decodeId = jest.fn().mockReturnValue(DECODED);
 }

+ 1 - 1
packages/core/src/config/config.service.ts

@@ -59,7 +59,7 @@ export class ConfigService implements VendureConfig {
         return this.activeConfig.defaultLanguageCode;
     }
 
-    get entityIdStrategy(): EntityIdStrategy {
+    get entityIdStrategy(): EntityIdStrategy<any> {
         return this.activeConfig.entityIdStrategy;
     }
 

+ 7 - 3
packages/core/src/config/entity-id-strategy/auto-increment-id-strategy.ts

@@ -1,10 +1,14 @@
-import { IntegerIdStrategy } from './entity-id-strategy';
+import { EntityIdStrategy } from './entity-id-strategy';
 
 /**
+ * @description
  * An id strategy which uses auto-increment integers as primary keys
- * for all entities.
+ * for all entities. This is the default strategy used by Vendure.
+ *
+ * @docsCategory configuration
+ * @docsPage EntityIdStrategy
  */
-export class AutoIncrementIdStrategy implements IntegerIdStrategy {
+export class AutoIncrementIdStrategy implements EntityIdStrategy<'increment'> {
     readonly primaryKeyType = 'increment';
     decodeId(id: string): number {
         const asNumber = +id;

+ 3 - 5
packages/core/src/config/entity-id-strategy/base64-id-strategy.ts

@@ -1,17 +1,15 @@
-import { IntegerIdStrategy } from './entity-id-strategy';
+import { EntityIdStrategy } from './entity-id-strategy';
 
 /**
  * An example custom strategy which uses base64 encoding on integer ids.
  */
-export class Base64IdStrategy implements IntegerIdStrategy {
+export class Base64IdStrategy implements EntityIdStrategy<'increment'> {
     readonly primaryKeyType = 'increment';
     decodeId(id: string): number {
         const asNumber = +Buffer.from(id, 'base64').toString();
         return Number.isNaN(asNumber) ? -1 : asNumber;
     }
     encodeId(primaryKey: number): string {
-        return Buffer.from(primaryKey.toString())
-            .toString('base64')
-            .replace(/=+$/, '');
+        return Buffer.from(primaryKey.toString()).toString('base64').replace(/=+$/, '');
     }
 }

+ 33 - 31
packages/core/src/config/entity-id-strategy/entity-id-strategy.ts

@@ -1,41 +1,43 @@
-import { ID } from '@vendure/common/lib/shared-types';
-
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 
-/**
- * @description
- * Defines the type of primary key used for all entities in the database.
- * "increment" uses an auto-incrementing integer, whereas "uuid" uses a
- * uuid string.
- *
- * @docsCategory entities
- * @docsPage Entity Configuration
- */
-export type PrimaryKeyType = 'increment' | 'uuid';
+export type PrimaryKeyType<T> = T extends 'uuid' ? string : T extends 'increment' ? number : any;
 
 /**
  * @description
  * The EntityIdStrategy determines how entity IDs are generated and stored in the
  * database, as well as how they are transformed when being passed from the API to the
- * service layer.
+ * service layer and vice versa.
+ *
+ * Vendure ships with two strategies: {@link AutoIncrementIdStrategy} and {@link UuidIdStrategy},
+ * but custom strategies can be used, e.g. to apply some custom encoding to the ID before exposing
+ * it in the GraphQL API.
  *
- * @docsCategory entities
- * @docsPage Entity Configuration
+ * @docsCategory configuration
+ * @docsPage EntityIdStrategy
  * */
-export interface EntityIdStrategy<T extends ID = ID> extends InjectableStrategy {
-    readonly primaryKeyType: PrimaryKeyType;
-    encodeId: (primaryKey: T) => string;
-    decodeId: (id: string) => T;
-}
-
-export interface IntegerIdStrategy extends EntityIdStrategy<number> {
-    readonly primaryKeyType: 'increment';
-    encodeId: (primaryKey: number) => string;
-    decodeId: (id: string) => number;
-}
-
-export interface StringIdStrategy extends EntityIdStrategy<string> {
-    readonly primaryKeyType: 'uuid';
-    encodeId: (primaryKey: string) => string;
-    decodeId: (id: string) => string;
+export interface EntityIdStrategy<T extends 'increment' | 'uuid'> extends InjectableStrategy {
+    /**
+     * @description
+     * Defines how the primary key will be stored in the database - either
+     * `'increment'` for auto-increment integer IDs, or `'uuid'` for a unique
+     * string ID.
+     */
+    readonly primaryKeyType: T;
+    /**
+     * @description
+     * Allows the raw ID from the database to be transformed in some way before exposing
+     * it in the GraphQL API.
+     *
+     * For example, you may need to use auto-increment integer IDs due to some business
+     * constraint, but you may not want to expose this data publicly in your API. In this
+     * case, you can use the encode/decode methods to obfuscate the ID with some kind of
+     * encoding scheme, such as base64 (or something more sophisticated).
+     */
+    encodeId: (primaryKey: PrimaryKeyType<T>) => string;
+    /**
+     * @description
+     * Reverses the transformation performed by the `encodeId` method in order to get
+     * back to the raw ID value.
+     */
+    decodeId: (id: string) => PrimaryKeyType<T>;
 }

+ 18 - 3
packages/core/src/config/entity-id-strategy/uuid-id-strategy.ts

@@ -1,10 +1,25 @@
-import { StringIdStrategy } from './entity-id-strategy';
+import { EntityIdStrategy } from './entity-id-strategy';
 
 /**
+ * @description
  * An id strategy which uses string uuids as primary keys
- * for all entities.
+ * for all entities. This strategy can be configured with the
+ * `entityIdStrategy` property of the {@link VendureConfig}.
+ *
+ * @example
+ * ```TypeScript
+ * import { UuidIdStrategy, VendureConfig } from '\@vendure/core';
+ *
+ * export const config: VendureConfig = {
+ *   entityIdStrategy: new UuidIdStrategy(),
+ *   // ...
+ * }
+ * ```
+ *
+ * @docsCategory configuration
+ * @docsPage EntityIdStrategy
  */
-export class UuidIdStrategy implements StringIdStrategy {
+export class UuidIdStrategy implements EntityIdStrategy<'uuid'> {
     readonly primaryKeyType = 'uuid';
     decodeId(id: string): string {
         return id;

+ 1 - 1
packages/core/src/config/vendure-config.ts

@@ -738,7 +738,7 @@ export interface VendureConfig {
      * entities via the API. The default uses a simple auto-increment integer
      * strategy.
      *
-     * @default new AutoIncrementIdStrategy()
+     * @default AutoIncrementIdStrategy
      */
     entityIdStrategy?: EntityIdStrategy<any>;
     /**

+ 5 - 0
packages/core/src/entity/order/order.entity.ts

@@ -115,6 +115,11 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
         return this.pendingAdjustments || [];
     }
 
+    @Calculated()
+    get totalQuantity(): number {
+        return (this.lines || []).reduce((total, line) => total + line.quantity, 0);
+    }
+
     get promotionAdjustmentsTotal(): number {
         return this.adjustments
             .filter(a => a.type === AdjustmentType.PROMOTION)

+ 3 - 3
packages/core/src/entity/set-entity-id-strategy.ts

@@ -5,12 +5,12 @@ import { EntityIdStrategy } from '../config/entity-id-strategy/entity-id-strateg
 
 import { getIdColumnsFor, getPrimaryGeneratedIdColumn } from './entity-id.decorator';
 
-export function setEntityIdStrategy(entityIdStrategy: EntityIdStrategy, entities: Array<Type<any>>) {
+export function setEntityIdStrategy(entityIdStrategy: EntityIdStrategy<any>, entities: Array<Type<any>>) {
     setBaseEntityIdType(entityIdStrategy);
     setEntityIdColumnTypes(entityIdStrategy, entities);
 }
 
-function setEntityIdColumnTypes(entityIdStrategy: EntityIdStrategy, entities: Array<Type<any>>) {
+function setEntityIdColumnTypes(entityIdStrategy: EntityIdStrategy<any>, entities: Array<Type<any>>) {
     const columnDataType = entityIdStrategy.primaryKeyType === 'increment' ? 'int' : 'varchar';
     for (const EntityCtor of entities) {
         const columnConfig = getIdColumnsFor(EntityCtor);
@@ -24,7 +24,7 @@ function setEntityIdColumnTypes(entityIdStrategy: EntityIdStrategy, entities: Ar
     }
 }
 
-function setBaseEntityIdType(entityIdStrategy: EntityIdStrategy) {
+function setBaseEntityIdType(entityIdStrategy: EntityIdStrategy<any>) {
     const { entity, name } = getPrimaryGeneratedIdColumn();
     PrimaryGeneratedColumn(entityIdStrategy.primaryKeyType as any)(entity, name);
 }

+ 1 - 1
packages/core/src/plugin/default-job-queue-plugin/sql-job-queue-strategy.ts

@@ -126,7 +126,7 @@ export class SqlJobQueueStrategy implements JobQueueStrategy {
 
     private toRecord(job: Job<any>): JobRecord {
         return new JobRecord({
-            id: job.id,
+            id: job.id || undefined,
             queueName: job.queueName,
             data: job.data,
             state: job.state,

+ 2 - 2
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "license": "MIT",
   "bin": {
     "create": "./index.js"
@@ -26,7 +26,7 @@
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.0",
     "@types/semver": "^6.0.0",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "rimraf": "^3.0.0",
     "ts-node": "^8.4.1",
     "typescript": "3.8.3"

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

@@ -1,6 +1,6 @@
 {
   "name": "dev-server",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "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": "^0.15.1",
-    "@vendure/asset-server-plugin": "^0.15.1",
+    "@vendure/admin-ui-plugin": "^0.15.2",
+    "@vendure/asset-server-plugin": "^0.15.2",
     "@vendure/common": "^0.15.0",
-    "@vendure/core": "^0.15.1",
-    "@vendure/elasticsearch-plugin": "^0.15.1",
-    "@vendure/email-plugin": "^0.15.1",
+    "@vendure/core": "^0.15.2",
+    "@vendure/elasticsearch-plugin": "^0.15.2",
+    "@vendure/email-plugin": "^0.15.2",
     "typescript": "3.8.3"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^0.15.1",
-    "@vendure/ui-devkit": "^0.15.1",
+    "@vendure/testing": "^0.15.2",
+    "@vendure/ui-devkit": "^0.15.2",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3"
   }

+ 1 - 1
packages/elasticsearch-plugin/README.md

@@ -1,6 +1,6 @@
 # Vendure Elasticsearch Plugin
 
-The `ElasticsearchPlugin` uses Elasticsearch to power the the Vendure product search. 
+The `ElasticsearchPlugin` uses Elasticsearch to power the Vendure product search. 
 
 **Requires Elasticsearch v7.0 or higher.** 
 

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -23,7 +23,7 @@
   },
   "devDependencies": {
     "@vendure/common": "^0.15.0",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "rimraf": "^3.0.0",
     "typescript": "3.8.3"
   }

+ 6 - 2
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -1,4 +1,4 @@
-import { Client } from '@elastic/elasticsearch';
+import { Client, ClientOptions } from '@elastic/elasticsearch';
 import { Inject, Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
 import { SearchResult, SearchResultAsset } from '@vendure/common/lib/generated-types';
 import {
@@ -50,8 +50,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
 
     onModuleInit(): any {
         const { host, port } = this.options;
+        const node = this.options.clientOptions?.node ?? `${host}:${port}`;
         this.client = new Client({
-            node: `${host}:${port}`,
+            node,
+            // `any` cast is there due to a strange error "Property '[Symbol.iterator]' is missing in type... URLSearchParams"
+            // which looks like possibly a TS/definitions bug.
+            ...(this.options.clientOptions as any),
         });
     }
 

+ 27 - 8
packages/elasticsearch-plugin/src/options.ts

@@ -1,3 +1,4 @@
+import { ClientOptions } from '@elastic/elasticsearch';
 import { DeepRequired, ID, Product, ProductVariant } from '@vendure/core';
 import deepmerge from 'deepmerge';
 
@@ -13,14 +14,26 @@ import { CustomMapping, ElasticSearchInput } from './types';
 export interface ElasticsearchOptions {
     /**
      * @description
-     * The host of the Elasticsearch server.
+     * The host of the Elasticsearch server. May also be specified in `clientOptions.node`.
+     *
+     * @default 'http://localhost'
      */
-    host: string;
+    host?: string;
     /**
      * @description
-     * The port of the Elasticsearch server.
+     * The port of the Elasticsearch server. May also be specified in `clientOptions.node`.
+     *
+     * @default 9200
      */
-    port: number;
+    port?: number;
+    /**
+     * @description
+     * Options to pass directly to the
+     * [Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
+     * set authentication or other more advanced options.
+     * Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
+     */
+    clientOptions?: ClientOptions;
     /**
      * @description
      * Prefix for the indices created by the plugin.
@@ -275,7 +288,11 @@ export interface BoostFieldsConfig {
     sku?: number;
 }
 
-export const defaultOptions: DeepRequired<ElasticsearchOptions> = {
+export type ElasticsearchRuntimeOptions = DeepRequired<Omit<ElasticsearchOptions, 'clientOptions'>> & {
+    clientOptions?: ClientOptions;
+};
+
+export const defaultOptions: ElasticsearchRuntimeOptions = {
     host: 'http://localhost',
     port: 9200,
     indexPrefix: 'vendure-',
@@ -290,12 +307,14 @@ export const defaultOptions: DeepRequired<ElasticsearchOptions> = {
             sku: 1,
         },
         priceRangeBucketInterval: 1000,
-        mapQuery: (query) => query,
+        mapQuery: query => query,
     },
     customProductMappings: {},
     customProductVariantMappings: {},
 };
 
-export function mergeWithDefaults(userOptions: ElasticsearchOptions): DeepRequired<ElasticsearchOptions> {
-    return deepmerge(defaultOptions, userOptions) as DeepRequired<ElasticsearchOptions>;
+export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions {
+    const { clientOptions, ...pluginOptions } = userOptions;
+    const merged = deepmerge(defaultOptions, pluginOptions) as ElasticsearchRuntimeOptions;
+    return { ...merged, clientOptions };
 }

+ 36 - 8
packages/elasticsearch-plugin/src/plugin.ts

@@ -1,7 +1,7 @@
+import { NodeOptions } from '@elastic/elasticsearch';
 import {
     AssetEvent,
     CollectionModificationEvent,
-    DeepRequired,
     EventBus,
     HealthCheckRegistryService,
     ID,
@@ -26,7 +26,7 @@ import { ElasticsearchHealthIndicator } from './elasticsearch.health';
 import { ElasticsearchService } from './elasticsearch.service';
 import { generateSchemaExtensions } from './graphql-schema-extensions';
 import { ElasticsearchIndexerController } from './indexer.controller';
-import { ElasticsearchOptions, mergeWithDefaults } from './options';
+import { ElasticsearchOptions, ElasticsearchRuntimeOptions, mergeWithDefaults } from './options';
 
 /**
  * @description
@@ -37,11 +37,11 @@ import { ElasticsearchOptions, mergeWithDefaults } from './options';
  *
  * **Requires Elasticsearch v7.0 or higher.**
  *
- * `yarn add \@vendure/elasticsearch-plugin`
+ * `yarn add \@elastic/elasticsearch \@vendure/elasticsearch-plugin`
  *
  * or
  *
- * `npm install \@vendure/elasticsearch-plugin`
+ * `npm install \@elastic/elasticsearch \@vendure/elasticsearch-plugin`
  *
  * Make sure to remove the `DefaultSearchPlugin` if it is still in the VendureConfig plugins array.
  *
@@ -208,12 +208,14 @@ import { ElasticsearchOptions, mergeWithDefaults } from './options';
                 ? [ShopElasticSearchResolver, CustomMappingsResolver]
                 : [ShopElasticSearchResolver];
         },
-        schema: () => generateSchemaExtensions(ElasticsearchPlugin.options),
+        // `any` cast is there due to a strange error "Property '[Symbol.iterator]' is missing in type... URLSearchParams"
+        // which looks like possibly a TS/definitions bug.
+        schema: () => generateSchemaExtensions(ElasticsearchPlugin.options as any),
     },
     workers: [ElasticsearchIndexerController],
 })
 export class ElasticsearchPlugin implements OnVendureBootstrap {
-    private static options: DeepRequired<ElasticsearchOptions>;
+    private static options: ElasticsearchRuntimeOptions;
 
     /** @internal */
     constructor(
@@ -235,17 +237,18 @@ export class ElasticsearchPlugin implements OnVendureBootstrap {
     /** @internal */
     async onVendureBootstrap(): Promise<void> {
         const { host, port } = ElasticsearchPlugin.options;
+        const nodeName = this.nodeName();
         try {
             const pingResult = await this.elasticsearchService.checkConnection();
         } catch (e) {
-            Logger.error(`Could not connect to Elasticsearch instance at "${host}:${port}"`, loggerCtx);
+            Logger.error(`Could not connect to Elasticsearch instance at "${nodeName}"`, loggerCtx);
             Logger.error(JSON.stringify(e), loggerCtx);
             this.healthCheckRegistryService.registerIndicatorFunction(() =>
                 this.elasticsearchHealthIndicator.startupCheckFailed(e.message),
             );
             return;
         }
-        Logger.info(`Sucessfully connected to Elasticsearch instance at "${host}:${port}"`, loggerCtx);
+        Logger.info(`Successfully connected to Elasticsearch instance at "${nodeName}"`, loggerCtx);
 
         await this.elasticsearchService.createIndicesIfNotExists();
         this.elasticsearchIndexService.initJobQueue();
@@ -321,4 +324,29 @@ export class ElasticsearchPlugin implements OnVendureBootstrap {
                 }
             });
     }
+
+    /**
+     * Returns a string representation of the target node(s) that the Elasticsearch
+     * client is configured to connect to.
+     */
+    private nodeName(): string {
+        const { host, port, clientOptions } = ElasticsearchPlugin.options;
+        const node = clientOptions?.node;
+        const nodes = clientOptions?.nodes;
+        if (nodes) {
+            return [...nodes].join(', ');
+        }
+        if (node) {
+            if (Array.isArray(node)) {
+                return (node as any[])
+                    .map((n: string | NodeOptions) => {
+                        return typeof n === 'string' ? n : n.url.toString();
+                    })
+                    .join(', ');
+            } else {
+                return typeof node === 'string' ? node : node.url.toString();
+            }
+        }
+        return `${host}:${port}`;
+    }
 }

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -34,7 +34,7 @@
     "@types/mjml": "^4.0.2",
     "@types/nodemailer": "^6.4.0",
     "@vendure/common": "^0.15.0",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "rimraf": "^3.0.0",
     "typescript": "3.8.3"
   }

+ 11 - 1
packages/email-plugin/src/default-email-handlers.ts

@@ -5,6 +5,7 @@ import {
     NativeAuthenticationMethod,
     OrderStateTransitionEvent,
     PasswordResetEvent,
+    ShippingMethod,
 } from '@vendure/core';
 
 import { EmailEventHandler } from './event-handler';
@@ -19,10 +20,19 @@ import {
 export const orderConfirmationHandler = new EmailEventListener('order-confirmation')
     .on(OrderStateTransitionEvent)
     .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
+    .loadData(async context => {
+        let shippingMethod: ShippingMethod | undefined;
+        if (!context.event.order.shippingMethod && context.event.order.shippingMethodId) {
+            shippingMethod = await context.connection
+                .getRepository(ShippingMethod)
+                .findOne(context.event.order.shippingMethodId);
+        }
+        return { shippingMethod };
+    })
     .setRecipient(event => event.order.customer!.emailAddress)
     .setFrom(`{{ fromAddress }}`)
     .setSubject(`Order confirmation for #{{ order.code }}`)
-    .setTemplateVars(event => ({ order: event.order }))
+    .setTemplateVars(event => ({ order: event.order, shippingMethod: event.data.shippingMethod }))
     .setMockEvent(mockOrderStateTransitionEvent);
 
 export const emailVerificationHandler = new EmailEventListener('email-verification')

+ 5 - 5
packages/email-plugin/src/event-handler.ts

@@ -50,7 +50,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     private configurations: EmailTemplateConfig[] = [];
     private defaultSubject: string;
     private from: string;
-    private _mockEvent: Omit<Event, 'ctx'> | undefined;
+    private _mockEvent: Omit<Event, 'ctx' | 'data'> | undefined;
 
     constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
 
@@ -60,7 +60,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     }
 
     /** @internal */
-    get mockEvent(): Omit<Event, 'ctx'> | undefined {
+    get mockEvent(): Omit<Event, 'ctx' | 'data'> | undefined {
         return this._mockEvent;
     }
 
@@ -213,7 +213,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
      * Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
      * from this handler, which is useful when developing the email templates.
      */
-    setMockEvent(event: Omit<Event, 'ctx'>): EmailEventHandler<T, Event> {
+    setMockEvent(event: Omit<Event, 'ctx' | 'data'>): EmailEventHandler<T, Event> {
         this._mockEvent = event;
         return this;
     }
@@ -225,7 +225,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         if (this.configurations.length === 0) {
             return;
         }
-        const exactMatch = this.configurations.find((c) => {
+        const exactMatch = this.configurations.find(c => {
             return (
                 (c.channelCode === channelCode || c.channelCode === 'default') &&
                 c.languageCode === languageCode
@@ -235,7 +235,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
             return exactMatch;
         }
         const channelMatch = this.configurations.find(
-            (c) => c.channelCode === channelCode && c.languageCode === 'default',
+            c => c.channelCode === channelCode && c.languageCode === 'default',
         );
         if (channelMatch) {
             return channelMatch;

+ 1 - 1
packages/email-plugin/templates/order-confirmation/body.hbs

@@ -109,7 +109,7 @@
                 <td>${{ formatMoney order.subTotal }}</td>
             </tr>
             <tr class="order-row">
-                <td colspan="3">Shipping ({{ order.shippingMethod.description }}):</td>
+                <td colspan="3">Shipping ({{ shippingMethod.description }}):</td>
                 <td>${{ formatMoney order.shipping }}</td>
             </tr>
             <tr class="order-row total-row">

+ 2 - 2
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/testing",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
     "vendure",
@@ -44,7 +44,7 @@
   "devDependencies": {
     "@types/mysql": "^2.15.8",
     "@types/pg": "^7.14.1",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "mysql": "^2.17.1",
     "pg": "^7.17.1",
     "rimraf": "^3.0.0",

+ 2 - 2
packages/testing/src/config/testing-entity-id-strategy.ts

@@ -1,11 +1,11 @@
-import { IntegerIdStrategy } from '@vendure/core';
+import { EntityIdStrategy } from '@vendure/core';
 
 /**
  * A testing entity id strategy which prefixes all IDs with a constant string. This is used in the
  * e2e tests to ensure that all ID properties in arguments are being
  * correctly decoded.
  */
-export class TestingEntityIdStrategy implements IntegerIdStrategy {
+export class TestingEntityIdStrategy implements EntityIdStrategy<'increment'> {
     readonly primaryKeyType = 'increment';
     decodeId(id: string): number {
         const asNumber = parseInt(id.replace('T_', ''), 10);

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "0.15.1",
+  "version": "0.15.2",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -39,7 +39,7 @@
     "@angular/cli": "^9.0.5",
     "@angular/compiler": "^9.0.6",
     "@angular/compiler-cli": "^9.0.6",
-    "@vendure/admin-ui": "^0.15.1",
+    "@vendure/admin-ui": "^0.15.2",
     "@vendure/common": "^0.15.0",
     "chalk": "^3.0.0",
     "chokidar": "^3.3.1",
@@ -51,7 +51,7 @@
     "@rollup/plugin-node-resolve": "^7.1.1",
     "@types/fs-extra": "^8.1.0",
     "@types/glob": "^7.1.1",
-    "@vendure/core": "^0.15.1",
+    "@vendure/core": "^0.15.2",
     "rimraf": "^3.0.0",
     "rollup": "^2.2.0",
     "rollup-plugin-terser": "^5.3.0",