diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..8691ba98bf --- /dev/null +++ b/.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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 58a379620f..3c040f1698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.15.2 (2020-09-30) + + +#### 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) + ## 0.15.1 (2020-09-09) diff --git a/docs/content/article/faq.md b/docs/content/article/faq.md index e20cf7cb40..ac509911dc 100644 --- a/docs/content/article/faq.md +++ b/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. diff --git a/docs/content/article/roadmap.md b/docs/content/article/roadmap.md index 9904e53474..ddf3abf7c8 100644 --- a/docs/content/article/roadmap.md +++ b/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 diff --git a/docs/content/docs/developer-guide/channels.md b/docs/content/docs/developer-guide/channels.md index 34d5cd8502..690ff459c9 100644 --- a/docs/content/docs/developer-guide/channels.md +++ b/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. diff --git a/docs/content/docs/developer-guide/deployment.md b/docs/content/docs/developer-guide/deployment.md index 13c40f3ccf..f716cacd6a 100644 --- a/docs/content/docs/developer-guide/deployment.md +++ b/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 diff --git a/docs/content/docs/plugins/available-plugins.md b/docs/content/docs/plugins/available-plugins.md index 45b690d98a..de2b377db0 100644 --- a/docs/content/docs/plugins/available-plugins.md +++ b/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). diff --git a/docs/layouts/partials/footer.html b/docs/layouts/partials/footer.html index 7469fc6e96..6d34d288e4 100644 --- a/docs/layouts/partials/footer.html +++ b/docs/layouts/partials/footer.html @@ -31,6 +31,12 @@ Join us on Slack +
  • + GitHub logo + Support Forum +
  • email icon diff --git a/docs/layouts/partials/top-bar.html b/docs/layouts/partials/top-bar.html index 83059bf7a2..c24a15454a 100644 --- a/docs/layouts/partials/top-bar.html +++ b/docs/layouts/partials/top-bar.html @@ -45,6 +45,10 @@ icon GitHub + + icon + Support Forum + icon Community diff --git a/lerna.json b/lerna.json index 1612e66b20..6559654c49 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "packages": [ "packages/*" ], - "version": "0.15.1", + "version": "0.15.2", "npmClient": "yarn", "useWorkspaces": true, "command": { diff --git a/packages/admin-ui-plugin/package.json b/packages/admin-ui-plugin/package.json index 600d73f95f..f7eed58dde 100644 --- a/packages/admin-ui-plugin/package.json +++ b/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" diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index ff5b00bd4d..d61892ad4b 100644 --- a/packages/admin-ui/package.json +++ b/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", diff --git a/packages/admin-ui/src/lib/core/src/common/version.ts b/packages/admin-ui/src/lib/core/src/common/version.ts index d4e975ab72..6d66801c13 100644 --- a/packages/admin-ui/src/lib/core/src/common/version.ts +++ b/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'; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.ts index 5699437de7..cda93f106e 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.ts +++ b/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(event.dataTransfer ? event.dataTransfer.items : []) .map(i => i.getAsFile()) .filter(notNullOrUndefined); this.selectFiles.emit(files); diff --git a/packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts b/packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts index e1cd3ef1d2..0cbafd7f67 100644 --- a/packages/admin-ui/src/lib/customer/src/components/address-card/address-card.component.ts +++ b/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(); @Output() setAsDefaultBilling = new EventEmitter(); + private dataDependenciesPopulated = new BehaviorSubject(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); } } diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts index 2cbb177303..ce44c826dc 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts +++ b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts @@ -161,6 +161,13 @@ export class CustomerDetailComponent extends BaseDetailComponent {{ 'order.refund-and-cancel-order' | translate }} diff --git a/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts b/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts index 08a1f17354..54e5caa322 100644 --- a/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts +++ b/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts @@ -234,7 +234,8 @@ export class OrderDetailComponent extends BaseDetailComponent p.state === 'Settled'); + } + private cancelOrder(order: OrderDetail.Fragment) { this.modalService .fromComponent(CancelOrderDialogComponent, { diff --git a/packages/asset-server-plugin/package.json b/packages/asset-server-plugin/package.json index b7b0bc4774..25f0eb96c4 100644 --- a/packages/asset-server-plugin/package.json +++ b/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", diff --git a/packages/core/package.json b/packages/core/package.json index e162cb7fbb..afecc9688c 100644 --- a/packages/core/package.json +++ b/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", diff --git a/packages/core/src/api/common/id-codec.ts b/packages/core/src/api/common/id-codec.ts index f47dae014c..fa4d8e28a7 100644 --- a/packages/core/src/api/common/id-codec.ts +++ b/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) {} /** * Decode an id from the client into the format used as the database primary key. diff --git a/packages/core/src/api/schema/type/order.type.graphql b/packages/core/src/api/schema/type/order.type.graphql index a3d77b445f..caf7a6d43a 100644 --- a/packages/core/src/api/schema/type/order.type.graphql +++ b/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! diff --git a/packages/core/src/config/config.service.mock.ts b/packages/core/src/config/config.service.mock.ts index 7bd0235a6d..a1ca0c9366 100644 --- a/packages/core/src/config/config.service.mock.ts +++ b/packages/core/src/config/config.service.mock.ts @@ -52,8 +52,8 @@ export class MockConfigService implements MockClass { 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); } diff --git a/packages/core/src/config/config.service.ts b/packages/core/src/config/config.service.ts index 192640dbc1..70aa496456 100644 --- a/packages/core/src/config/config.service.ts +++ b/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 { return this.activeConfig.entityIdStrategy; } diff --git a/packages/core/src/config/entity-id-strategy/auto-increment-id-strategy.ts b/packages/core/src/config/entity-id-strategy/auto-increment-id-strategy.ts index 4195f500aa..37b34cf73e 100644 --- a/packages/core/src/config/entity-id-strategy/auto-increment-id-strategy.ts +++ b/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; diff --git a/packages/core/src/config/entity-id-strategy/base64-id-strategy.ts b/packages/core/src/config/entity-id-strategy/base64-id-strategy.ts index d9237ab7ea..12c61a152f 100644 --- a/packages/core/src/config/entity-id-strategy/base64-id-strategy.ts +++ b/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(/=+$/, ''); } } diff --git a/packages/core/src/config/entity-id-strategy/entity-id-strategy.ts b/packages/core/src/config/entity-id-strategy/entity-id-strategy.ts index 775fef93b3..f244f4835a 100644 --- a/packages/core/src/config/entity-id-strategy/entity-id-strategy.ts +++ b/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 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 extends InjectableStrategy { - readonly primaryKeyType: PrimaryKeyType; - encodeId: (primaryKey: T) => string; - decodeId: (id: string) => T; -} - -export interface IntegerIdStrategy extends EntityIdStrategy { - readonly primaryKeyType: 'increment'; - encodeId: (primaryKey: number) => string; - decodeId: (id: string) => number; -} - -export interface StringIdStrategy extends EntityIdStrategy { - readonly primaryKeyType: 'uuid'; - encodeId: (primaryKey: string) => string; - decodeId: (id: string) => string; +export interface EntityIdStrategy 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) => string; + /** + * @description + * Reverses the transformation performed by the `encodeId` method in order to get + * back to the raw ID value. + */ + decodeId: (id: string) => PrimaryKeyType; } diff --git a/packages/core/src/config/entity-id-strategy/uuid-id-strategy.ts b/packages/core/src/config/entity-id-strategy/uuid-id-strategy.ts index c430c27a77..438a71c002 100644 --- a/packages/core/src/config/entity-id-strategy/uuid-id-strategy.ts +++ b/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; diff --git a/packages/core/src/config/vendure-config.ts b/packages/core/src/config/vendure-config.ts index bfab729a85..f6e8488041 100644 --- a/packages/core/src/config/vendure-config.ts +++ b/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; /** diff --git a/packages/core/src/entity/order/order.entity.ts b/packages/core/src/entity/order/order.entity.ts index b0789f443d..2bfa367198 100644 --- a/packages/core/src/entity/order/order.entity.ts +++ b/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) diff --git a/packages/core/src/entity/set-entity-id-strategy.ts b/packages/core/src/entity/set-entity-id-strategy.ts index 0120591f65..dd855ee9a1 100644 --- a/packages/core/src/entity/set-entity-id-strategy.ts +++ b/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>) { +export function setEntityIdStrategy(entityIdStrategy: EntityIdStrategy, entities: Array>) { setBaseEntityIdType(entityIdStrategy); setEntityIdColumnTypes(entityIdStrategy, entities); } -function setEntityIdColumnTypes(entityIdStrategy: EntityIdStrategy, entities: Array>) { +function setEntityIdColumnTypes(entityIdStrategy: EntityIdStrategy, entities: Array>) { 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) { const { entity, name } = getPrimaryGeneratedIdColumn(); PrimaryGeneratedColumn(entityIdStrategy.primaryKeyType as any)(entity, name); } diff --git a/packages/core/src/plugin/default-job-queue-plugin/sql-job-queue-strategy.ts b/packages/core/src/plugin/default-job-queue-plugin/sql-job-queue-strategy.ts index eb33507b9b..fc8f16d49e 100644 --- a/packages/core/src/plugin/default-job-queue-plugin/sql-job-queue-strategy.ts +++ b/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): JobRecord { return new JobRecord({ - id: job.id, + id: job.id || undefined, queueName: job.queueName, data: job.data, state: job.state, diff --git a/packages/create/package.json b/packages/create/package.json index e12f350fca..11cbacbef0 100644 --- a/packages/create/package.json +++ b/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" diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 60a87849ff..8c4d888f12 100644 --- a/packages/dev-server/package.json +++ b/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" } diff --git a/packages/elasticsearch-plugin/README.md b/packages/elasticsearch-plugin/README.md index 201ce9db89..1b2b16d23e 100644 --- a/packages/elasticsearch-plugin/README.md +++ b/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.** diff --git a/packages/elasticsearch-plugin/package.json b/packages/elasticsearch-plugin/package.json index 5791df3a81..e1030e5b4c 100644 --- a/packages/elasticsearch-plugin/package.json +++ b/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" } diff --git a/packages/elasticsearch-plugin/src/elasticsearch.service.ts b/packages/elasticsearch-plugin/src/elasticsearch.service.ts index 368943439c..c9c79aadc2 100644 --- a/packages/elasticsearch-plugin/src/elasticsearch.service.ts +++ b/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), }); } diff --git a/packages/elasticsearch-plugin/src/options.ts b/packages/elasticsearch-plugin/src/options.ts index 764f44cb3d..97d3144803 100644 --- a/packages/elasticsearch-plugin/src/options.ts +++ b/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 = { +export type ElasticsearchRuntimeOptions = DeepRequired> & { + clientOptions?: ClientOptions; +}; + +export const defaultOptions: ElasticsearchRuntimeOptions = { host: 'http://localhost', port: 9200, indexPrefix: 'vendure-', @@ -290,12 +307,14 @@ export const defaultOptions: DeepRequired = { sku: 1, }, priceRangeBucketInterval: 1000, - mapQuery: (query) => query, + mapQuery: query => query, }, customProductMappings: {}, customProductVariantMappings: {}, }; -export function mergeWithDefaults(userOptions: ElasticsearchOptions): DeepRequired { - return deepmerge(defaultOptions, userOptions) as DeepRequired; +export function mergeWithDefaults(userOptions: ElasticsearchOptions): ElasticsearchRuntimeOptions { + const { clientOptions, ...pluginOptions } = userOptions; + const merged = deepmerge(defaultOptions, pluginOptions) as ElasticsearchRuntimeOptions; + return { ...merged, clientOptions }; } diff --git a/packages/elasticsearch-plugin/src/plugin.ts b/packages/elasticsearch-plugin/src/plugin.ts index 248313da7d..bdff380c79 100644 --- a/packages/elasticsearch-plugin/src/plugin.ts +++ b/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; + private static options: ElasticsearchRuntimeOptions; /** @internal */ constructor( @@ -235,17 +237,18 @@ export class ElasticsearchPlugin implements OnVendureBootstrap { /** @internal */ async onVendureBootstrap(): Promise { 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}`; + } } diff --git a/packages/email-plugin/package.json b/packages/email-plugin/package.json index a8a2add8ca..f4c0916e4f 100644 --- a/packages/email-plugin/package.json +++ b/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" } diff --git a/packages/email-plugin/src/default-email-handlers.ts b/packages/email-plugin/src/default-email-handlers.ts index 6fca919f89..48e1e74235 100644 --- a/packages/email-plugin/src/default-email-handlers.ts +++ b/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') diff --git a/packages/email-plugin/src/event-handler.ts b/packages/email-plugin/src/event-handler.ts index ef559597c5..d28a7cb893 100644 --- a/packages/email-plugin/src/event-handler.ts +++ b/packages/email-plugin/src/event-handler.ts @@ -50,7 +50,7 @@ export class EmailEventHandler | undefined; + private _mockEvent: Omit | undefined; constructor(public listener: EmailEventListener, public event: Type) {} @@ -60,7 +60,7 @@ export class EmailEventHandler | undefined { + get mockEvent(): Omit | undefined { return this._mockEvent; } @@ -213,7 +213,7 @@ export class EmailEventHandler): EmailEventHandler { + setMockEvent(event: Omit): EmailEventHandler { this._mockEvent = event; return this; } @@ -225,7 +225,7 @@ export class EmailEventHandler { + const exactMatch = this.configurations.find(c => { return ( (c.channelCode === channelCode || c.channelCode === 'default') && c.languageCode === languageCode @@ -235,7 +235,7 @@ export class EmailEventHandler c.channelCode === channelCode && c.languageCode === 'default', + c => c.channelCode === channelCode && c.languageCode === 'default', ); if (channelMatch) { return channelMatch; diff --git a/packages/email-plugin/templates/order-confirmation/body.hbs b/packages/email-plugin/templates/order-confirmation/body.hbs index d9bb283d59..0b47c4898f 100644 --- a/packages/email-plugin/templates/order-confirmation/body.hbs +++ b/packages/email-plugin/templates/order-confirmation/body.hbs @@ -109,7 +109,7 @@ ${{ formatMoney order.subTotal }} - Shipping ({{ order.shippingMethod.description }}): + Shipping ({{ shippingMethod.description }}): ${{ formatMoney order.shipping }} diff --git a/packages/testing/package.json b/packages/testing/package.json index 76d56ca853..bce26fcf78 100644 --- a/packages/testing/package.json +++ b/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", diff --git a/packages/testing/src/config/testing-entity-id-strategy.ts b/packages/testing/src/config/testing-entity-id-strategy.ts index 03c31b2a8d..c866a70b08 100644 --- a/packages/testing/src/config/testing-entity-id-strategy.ts +++ b/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); diff --git a/packages/ui-devkit/package.json b/packages/ui-devkit/package.json index 2bb0e00f54..77be5a793f 100644 --- a/packages/ui-devkit/package.json +++ b/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",