Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
2887e11791
42 changed files with 395 additions and 95 deletions
  1. 14 0
      CHANGELOG.md
  2. BIN
      docs/content/developer-guide/channels/channels_diagram.png
  3. 4 0
      docs/content/developer-guide/channels/index.md
  4. 1 1
      docs/content/developer-guide/customizing-the-order-process/index.md
  5. BIN
      docs/content/developer-guide/multi-tenant/channel-selector.png
  6. BIN
      docs/content/developer-guide/multi-tenant/create-admin.png
  7. BIN
      docs/content/developer-guide/multi-tenant/create-channel.png
  8. BIN
      docs/content/developer-guide/multi-tenant/create-role.png
  9. 69 0
      docs/content/developer-guide/multi-tenant/index.md
  10. 1 0
      docs/content/getting-started.md
  11. 25 0
      docs/diagrams/channels-diagram.puml
  12. 1 1
      lerna.json
  13. 3 3
      packages/admin-ui-plugin/package.json
  14. 2 2
      packages/admin-ui/package.json
  15. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  16. 1 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.html
  17. 30 0
      packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts
  18. BIN
      packages/asset-server-plugin/e2e/fixtures/assets/白飯.jpg
  19. 3 3
      packages/asset-server-plugin/package.json
  20. 6 4
      packages/asset-server-plugin/src/plugin.ts
  21. 1 1
      packages/common/package.json
  22. 111 0
      packages/core/e2e/default-search-plugin-uuids.e2e-spec.ts
  23. 2 2
      packages/core/package.json
  24. 9 4
      packages/core/src/api/middleware/exception-logger.filter.ts
  25. 1 1
      packages/core/src/common/types/entity-relation-paths.ts
  26. 6 3
      packages/core/src/config/entity-id-strategy/uuid-id-strategy.ts
  27. 35 6
      packages/core/src/config/logger/vendure-logger.ts
  28. 7 1
      packages/core/src/entity/register-custom-entity-fields.ts
  29. 4 3
      packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts
  30. 4 3
      packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts
  31. 4 0
      packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts
  32. 4 3
      packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts
  33. 3 3
      packages/create/package.json
  34. 9 9
      packages/dev-server/package.json
  35. 3 3
      packages/elasticsearch-plugin/package.json
  36. 3 3
      packages/email-plugin/package.json
  37. 11 17
      packages/email-plugin/src/default-email-handlers.ts
  38. 3 3
      packages/email-plugin/templates/order-confirmation/body.hbs
  39. 3 3
      packages/job-queue-plugin/package.json
  40. 4 4
      packages/payments-plugin/package.json
  41. 3 3
      packages/testing/package.json
  42. 4 4
      packages/ui-devkit/package.json

+ 14 - 0
CHANGELOG.md

@@ -1,3 +1,17 @@
+## <small>1.4.5 (2022-01-17)</small>
+
+
+#### Fixes
+
+* **admin-ui** Fix custom field select input ([2bbb972](https://github.com/vendure-ecommerce/vendure/commit/2bbb972)), closes [#1342](https://github.com/vendure-ecommerce/vendure/issues/1342)
+* **asset-server-plugin** Correctly handle non-latin filenames ([4cf8434](https://github.com/vendure-ecommerce/vendure/commit/4cf8434)), closes [#1343](https://github.com/vendure-ecommerce/vendure/issues/1343)
+* **core** Demote 404 errors to Verbose log level ([6b3c3fe](https://github.com/vendure-ecommerce/vendure/commit/6b3c3fe)), closes [#1335](https://github.com/vendure-ecommerce/vendure/issues/1335)
+* **core** Fix error with certain custom field config ([ba060b4](https://github.com/vendure-ecommerce/vendure/commit/ba060b4)), closes [#1337](https://github.com/vendure-ecommerce/vendure/issues/1337)
+* **core** Fix filtering by facet value uuid in DefaultSearchPlugin ([53babf6](https://github.com/vendure-ecommerce/vendure/commit/53babf6)), closes [#1341](https://github.com/vendure-ecommerce/vendure/issues/1341)
+* **core** Fix typings for EntityHydrator with nullable relations ([8f57547](https://github.com/vendure-ecommerce/vendure/commit/8f57547))
+* **core** Prevent errors being logged by Nest's ExternalExceptionFilter ([cf4f246](https://github.com/vendure-ecommerce/vendure/commit/cf4f246)), closes [#1335](https://github.com/vendure-ecommerce/vendure/issues/1335)
+* **email-plugin** Fix population of shippingLines in order handler ([4cd7ecd](https://github.com/vendure-ecommerce/vendure/commit/4cd7ecd)), closes [#1354](https://github.com/vendure-ecommerce/vendure/issues/1354)
+
 ## <small>1.4.4 (2022-01-10)</small>
 
 

BIN
docs/content/developer-guide/channels/channels_diagram.png


+ 4 - 0
docs/content/developer-guide/channels.md → docs/content/developer-guide/channels/index.md

@@ -15,6 +15,8 @@ Channels are a feature of Vendure which allows multiple sales channels to be rep
 
 Every Vendure server always has a **default Channel**, which contains _all_ entities. Subsequent channels can then contain a subset of the above entities.
 
+{{< figure src="channels_diagram.png" >}}
+
 Use-cases of Channels include:
 
 * Multi-region stores, where there is a distinct website for each territory with its own available inventory, pricing, tax and shipping rules.
@@ -28,3 +30,5 @@ To specify which channel to use when making an API call, set the `'vendure-token
 ## Multi-Tenant (Marketplace) Support
 
 Channels can also be used to implement a multi-tenant or marketplace application. In such a setup, each merchant would have their own dedicated Channel and would be granted permissions on that Channel only.
+ 
+For a detailed guide on how this would be set up, see our [Multi-Tenant guide]({{< relref "multi-tenant" >}}).

+ 1 - 1
docs/content/developer-guide/customizing-the-order-process/index.md

@@ -7,7 +7,7 @@ showtoc: true
 
 Vendure defines an order process which is based on a [finite state machine]({{< relref "fsm" >}}). This means that the [`Order.state` property]({{< relref "order" >}}#state) will be one of a set of [pre-defined states]({{< relref "order-state" >}}). From the current state, the Order can then transition (change) to another state, and the available next states depend on what the current state is.
 
-So, as an example, all orders being in the `AddingItems` state. This means that the Customer is adding items to his or her shopping cart. From there, the Order can transition to the `ArrangingPayment` state. A diagram of the default states and transitions can be found in the [Order Workflow guide]({{< relref "order-workflow" >}}).
+So, as an example, all orders begin in the `AddingItems` state. This means that the Customer is adding items to his or her shopping cart. From there, the Order can transition to the `ArrangingPayment` state. A diagram of the default states and transitions can be found in the [Order Workflow guide]({{< relref "order-workflow" >}}).
 
 ## Defining custom states and transitions
 

BIN
docs/content/developer-guide/multi-tenant/channel-selector.png


BIN
docs/content/developer-guide/multi-tenant/create-admin.png


BIN
docs/content/developer-guide/multi-tenant/create-channel.png


BIN
docs/content/developer-guide/multi-tenant/create-role.png


+ 69 - 0
docs/content/developer-guide/multi-tenant/index.md

@@ -0,0 +1,69 @@
+---
+title: 'Multi-Tenant'
+showtoc: true
+---
+
+# Multi-tenancy Support
+
+Vendure supports multi-tenant headless commerce solutions through its powerful [Channels]({{< relref "channels" >}}) feature.
+
+Channels allow us to segment our business entities per tenant. To do this, you would create a separate Channel per tenant.
+
+## Creating Tenants
+
+Let's say we want to use a single Vendure instance to run 2 separate e-commerce businesses: **Ace Parts** and **Best Choice**. First we need to create a new Channel for each:
+
+{{< figure src="create-channel.png" title="Creating a new Channel via the Admin UI" >}}
+
+Do this for both of our tenants: `ace-parts` and `best-choice`.
+
+## Defining Roles
+
+The next requirement is being able to create Administrators who have permissions only for a given tenant. This is supported by Vendure's role-based access control system.
+
+First we will create a new Role, and grant all permissions on the `ace-parts` Channel only:
+
+{{< figure src="create-role.png" title="Creating a Channel-specific Role" >}}
+
+Next we create a new Administrator, and assign the Role that was just created.
+
+{{< figure src="create-admin.png" title="Creating a Channel-specific Role" >}}
+
+Repeat the steps of creating a Role and Administrator for the `best-choice` Channel.
+
+Now you've successfully set up your two tenants and you have an admin account for each.
+
+## Mechanics of Multi-Tenancy
+
+Each tenant is modelled as a Channel, and both of our new Channels can be considered "children" of the **default Channel**. That is, the default Channel contains every Product, Customer, Order, Promotion etc., no matter which Channel they were created in.
+
+{{< figure src="../channels/channels_diagram.png" >}}
+
+In this way, the default Channel can be used by the superadmin account to get an overview of the entire instance, whereas the admins of each tenant will only have access to the entities assigned to their respective Channels.
+
+The following entities are "channel-aware", i.e. they can be assigned to a specific Channel:
+
+* Asset
+* Collection
+* Customer
+* Facet/FacetValue
+* Order
+* PaymentMethod
+* Product/ProductVariant
+* Promotion
+* Role
+* ShippingMethod
+
+In the Admin UI, you can switch between active Channels using the switcher component in the top bar:
+
+{{< figure src="channel-selector.png" title="Switching between Channels" >}}
+
+For example, switching to the `ace-parts` Channel, and then creating a new Product will assign that new Product to the `ace-parts` Channel (_and_ the default Channel, since _everything_ is assigned to the default Channel).
+
+## The Storefront
+
+Your storefront applications will need to specify which channel they are interested in. This is done by adding a query parameter or header to each API requests, with the key being `vendure-token` and the value being the target Channel's `token` property.
+
+```text
+https://my-vendure-server.com/shop-api?vendure-token=best-choice
+```

+ 1 - 0
docs/content/getting-started.md

@@ -8,6 +8,7 @@ weight: 0
 ## Requirements
  
 * [Node.js](https://nodejs.org/en/) **v12** or above, with support for **even-numbered Node.js versions**.
+* The [supported TypeScript version](https://github.com/vendure-ecommerce/vendure/blob/master/packages/create/src/constants.ts#L7) is set upon installation. Upgrading to a newer version of TypeScript might result in compilation errors.
 * If you want to use MySQL, MariaDB, or Postgres as your data store, then you'll need an instance available locally. However, if you are just testing out Vendure, we recommend using SQLite, which has no external requirements.
 * For Windows users: make sure you have **[windows build tools](https://www.npmjs.com/package/windows-build-tools) installed**
   * `npm install --global --production windows-build-tools`

+ 25 - 0
docs/diagrams/channels-diagram.puml

@@ -0,0 +1,25 @@
+@startuml
+!include theme.puml
+title Channels in Vendure
+
+node DefaultChannel as "Default Channel" {
+  [All Products\nAll Collections\nAll Customers\nAll Orders]
+}
+
+package ChannelA as "Channel A" {
+  [Products A\nCollections A\nCustomers A\nOrders A]
+}
+
+package ChannelB as "Channel B" {
+  [Products B\nCollections B\nCustomers B\nOrders B]
+}
+
+package ChannelC as "Channel C" {
+  [Products C\nCollections C\nCustomers C\nOrders C]
+}
+
+DefaultChannel -down-> ChannelA
+DefaultChannel -down-> ChannelB
+DefaultChannel -down-> ChannelC
+
+@enduml

+ 1 - 1
lerna.json

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

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

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

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

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

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

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

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

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

+ 30 - 0
packages/asset-server-plugin/e2e/asset-server-plugin.e2e-spec.ts

@@ -84,6 +84,36 @@ describe('AssetServerPlugin', () => {
         expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
     });
 
+    it('can handle non-latin filenames', async () => {
+        const FILE_NAME_ZH = '白飯';
+        const filesToUpload = [path.join(__dirname, `fixtures/assets/${FILE_NAME_ZH}.jpg`)];
+        const { createAssets }: CreateAssets.Mutation = await adminClient.fileUploadMutation({
+            mutation: CREATE_ASSETS,
+            filePaths: filesToUpload,
+            mapVariables: filePaths => ({
+                input: filePaths.map(p => ({ file: null })),
+            }),
+        });
+        expect(createAssets[0].name).toBe(`${FILE_NAME_ZH}.jpg`);
+        expect(createAssets[0].source).toContain(`${FILE_NAME_ZH}.jpg`);
+
+        const previewUrl = encodeURI(`${createAssets[0].preview}`);
+        const res = await fetch(previewUrl);
+
+        expect(res.status).toBe(200);
+
+        const previewFilePathZH = path.join(
+            __dirname,
+            TEST_ASSET_DIR,
+            `preview/3f/${FILE_NAME_ZH}__preview.jpg`,
+        );
+
+        const responseBuffer = await res.buffer();
+        const previewFile = await fs.readFile(previewFilePathZH);
+
+        expect(Buffer.compare(responseBuffer, previewFile)).toBe(0);
+    });
+
     describe('caching', () => {
         const cacheDir = path.join(__dirname, TEST_ASSET_DIR, 'cache');
         const cacheFileDir = path.join(__dirname, TEST_ASSET_DIR, 'cache', 'preview', '71');

BIN
packages/asset-server-plugin/e2e/fixtures/assets/白飯.jpg


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

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

+ 6 - 4
packages/asset-server-plugin/src/plugin.ts

@@ -231,10 +231,11 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
         return async (err: any, req: Request, res: Response, next: NextFunction) => {
             if (err && (err.status === 404 || err.statusCode === 404)) {
                 if (req.query) {
-                    Logger.debug(`Pre-cached Asset not found: ${req.path}`, loggerCtx);
+                    const decodedReqPath = decodeURIComponent(req.path);
+                    Logger.debug(`Pre-cached Asset not found: ${decodedReqPath}`, loggerCtx);
                     let file: Buffer;
                     try {
-                        file = await AssetServerPlugin.assetStorage.readFileToBuffer(req.path);
+                        file = await AssetServerPlugin.assetStorage.readFileToBuffer(decodedReqPath);
                     } catch (err) {
                         res.status(404).send('Resource not found');
                         return;
@@ -278,10 +279,11 @@ export class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
             }
         }
 
+        const decodedReqPath = decodeURIComponent(req.path);
         if (imageParamHash) {
-            return path.join(this.cacheDir, this.addSuffix(req.path, imageParamHash));
+            return path.join(this.cacheDir, this.addSuffix(decodedReqPath, imageParamHash));
         } else {
-            return req.path;
+            return decodedReqPath;
         }
     }
 

+ 1 - 1
packages/common/package.json

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

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

@@ -0,0 +1,111 @@
+/* tslint:disable:no-non-null-assertion */
+import { DefaultJobQueuePlugin, DefaultSearchPlugin, mergeConfig, UuidIdStrategy } from '@vendure/core';
+import { createTestEnvironment, registerInitializer, SqljsInitializer } from '@vendure/testing';
+import path from 'path';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+
+import { FacetValueFragment, GetFacetList } from './graphql/generated-e2e-admin-types';
+import {
+    SearchProductsShop,
+    SearchProductsShopQueryVariables,
+    SortOrder,
+} from './graphql/generated-e2e-shop-types';
+import { GET_FACET_LIST } from './graphql/shared-definitions';
+import { SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
+import { awaitRunningJobs } from './utils/await-running-jobs';
+
+registerInitializer('sqljs', new SqljsInitializer(path.join(__dirname, '__data__'), 1000));
+
+describe('Default search plugin with UUIDs', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            plugins: [DefaultSearchPlugin.init({ indexStockStatus: true }), DefaultJobQueuePlugin],
+            entityOptions: {
+                entityIdStrategy: new UuidIdStrategy(),
+            },
+        }),
+    );
+
+    let plantsFacetValue: FacetValueFragment;
+    let furnitureFacetValue: FacetValueFragment;
+    let photoFacetValue: FacetValueFragment;
+    let electronicsFacetValue: FacetValueFragment;
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-default-search.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+
+        // A precaution against a race condition in which the index
+        // rebuild is not completed in time for the first test.
+        await new Promise(resolve => setTimeout(resolve, 5000));
+
+        const { facets } = await adminClient.query<GetFacetList.Query, GetFacetList.Variables>(
+            GET_FACET_LIST,
+            {
+                options: {
+                    sort: {
+                        name: SortOrder.ASC,
+                    },
+                },
+            },
+        );
+        plantsFacetValue = facets.items[0].values.find(v => v.code === 'plants')!;
+        furnitureFacetValue = facets.items[0].values.find(v => v.code === 'furniture')!;
+        photoFacetValue = facets.items[0].values.find(v => v.code === 'photo')!;
+        electronicsFacetValue = facets.items[0].values.find(v => v.code === 'electronics')!;
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await awaitRunningJobs(adminClient);
+        await server.destroy();
+    });
+
+    it('can filter by facetValueIds', async () => {
+        const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShopQueryVariables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    facetValueIds: [plantsFacetValue.id],
+                    groupByProduct: true,
+                    sort: { name: SortOrder.ASC },
+                },
+            },
+        );
+        expect(result.search.items.map(i => i.productName)).toEqual([
+            'Bonsai Tree',
+            'Orchid',
+            'Spiky Cactus',
+        ]);
+    });
+
+    it('can filter by facetValueFilters', async () => {
+        const { facets } = await adminClient.query<GetFacetList.Query, GetFacetList.Variables>(
+            GET_FACET_LIST,
+        );
+        const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShopQueryVariables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    facetValueFilters: [
+                        { and: electronicsFacetValue.id },
+                        { or: [plantsFacetValue.id, photoFacetValue.id] },
+                    ],
+                    sort: { name: SortOrder.ASC },
+                    groupByProduct: true,
+                },
+            },
+        );
+        expect(result.search.items.map(i => i.productName)).toEqual([
+            'Camera Lens',
+            'Instant Camera',
+            'Slr Camera',
+            'Tripod',
+        ]);
+    });
+});

+ 2 - 2
packages/core/package.json

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

+ 9 - 4
packages/core/src/api/middleware/exception-logger.filter.ts

@@ -39,16 +39,21 @@ export class ExceptionLoggerFilter implements ExceptionFilter {
                     Logger.verbose(message);
                     break;
             }
+            if (exception.stack) {
+                Logger.debug(exception.stack);
+            }
+            if (isGraphQL) {
+                return exception;
+            }
         } else if (exception instanceof HttpException) {
             // Handle other Nestjs errors
             statusCode = exception.getStatus();
             message = exception.message;
-            let stack = exception.stack;
             if (statusCode === 404) {
-                message = exception.message;
-                stack = undefined;
+                Logger.verbose(exception.message);
+            } else {
+                Logger.error(message, undefined, exception.stack);
             }
-            Logger.error(message, undefined, stack);
         } else {
             Logger.error(exception.message, undefined, exception.stack);
         }

+ 1 - 1
packages/core/src/common/types/entity-relation-paths.ts

@@ -27,7 +27,7 @@ export type EntityRelationPaths<T extends VendureEntity> =
     | TripleDotPath;
 
 export type EntityRelationKeys<T extends VendureEntity> = {
-    [K in Extract<keyof T, string>]: Required<T>[K] extends VendureEntity
+    [K in Extract<keyof T, string>]: Required<T>[K] extends VendureEntity | null
         ? K
         : Required<T>[K] extends VendureEntity[]
         ? K

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

@@ -4,15 +4,18 @@ import { EntityIdStrategy } from './entity-id-strategy';
  * @description
  * An id strategy which uses string uuids as primary keys
  * for all entities. This strategy can be configured with the
- * `entityIdStrategy` property of the {@link VendureConfig}.
+ * `entityIdStrategy` property of the `entityOptions` property
+ * of {@link VendureConfig}.
  *
  * @example
  * ```TypeScript
  * import { UuidIdStrategy, VendureConfig } from '\@vendure/core';
  *
  * export const config: VendureConfig = {
- *   entityIdStrategy: new UuidIdStrategy(),
- *   // ...
+ *   entityOptions: {
+ *     entityIdStrategy: new UuidIdStrategy(),
+ *     // ...
+ *   }
  * }
  * ```
  *

+ 35 - 6
packages/core/src/config/logger/vendure-logger.ts

@@ -9,12 +9,31 @@ import { LoggerService } from '@nestjs/common';
 export enum LogLevel {
     /**
      * @description
-     * Log Errors only.
+     * Log Errors only. These are usually indicative of some potentially
+     * serious issue, so should be acted upon.
      */
     Error = 0,
+    /**
+     * @description
+     * Warnings indicate that some situation may require investigation
+     * and handling. But not as serious as an Error.
+     */
     Warn = 1,
+    /**
+     * @description
+     * Logs general information such as startup messages.
+     */
     Info = 2,
+    /**
+     * @description
+     * Logs additional information
+     */
     Verbose = 3,
+    /**
+     * @description
+     * Logs detailed info useful in debug scenarios, including stack traces for
+     * all errors. In production this would probably generate too much noise.
+     */
     Debug = 4,
 }
 
@@ -34,11 +53,21 @@ export interface VendureLogger {
 }
 
 const noopLogger: VendureLogger = {
-    error() { /* */ },
-    warn() { /* */ },
-    info() { /* */ },
-    verbose() { /* */ },
-    debug() { /* */ },
+    error() {
+        /* */
+    },
+    warn() {
+        /* */
+    },
+    info() {
+        /* */
+    },
+    verbose() {
+        /* */
+    },
+    debug() {
+        /* */
+    },
 };
 
 /**

+ 7 - 1
packages/core/src/entity/register-custom-entity-fields.ts

@@ -127,7 +127,13 @@ function registerCustomFieldsForEntity(
                 }
             }
 
-            if (customFields.filter(f => f.type === 'relation').length === customFields.length) {
+            const relationFieldsCount = customFields.filter(f => f.type === 'relation').length;
+            const nonLocaleStringFieldsCount = customFields.filter(
+                f => f.type !== 'localeString' && f.type !== 'relation',
+            ).length;
+
+            if (0 < relationFieldsCount && nonLocaleStringFieldsCount === 0) {
+                // if (customFields.filter(f => f.type === 'relation').length === customFields.length) {
                 // If there are _only_ relational customFields defined for an Entity, then TypeORM
                 // errors when attempting to load that entity ("Cannot set property <fieldName> of undefined").
                 // Therefore as a work-around we will add a "fake" column to the customFields embedded type

+ 4 - 3
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -13,6 +13,7 @@ import { getFieldsToSelect } from './search-strategy-common';
 import {
     createCollectionIdCountMap,
     createFacetIdCountMap,
+    createPlaceholderFromId,
     mapToSearchResult,
 } from './search-strategy-utils';
 
@@ -185,7 +186,7 @@ export class MysqlSearchStrategy implements SearchStrategy {
             qb.andWhere(
                 new Brackets(qb1 => {
                     for (const id of facetValueIds) {
-                        const placeholder = '_' + id;
+                        const placeholder = createPlaceholderFromId(id);
                         const clause = `FIND_IN_SET(:${placeholder}, facetValueIds)`;
                         const params = { [placeholder]: id };
                         if (facetValueOperator === LogicalOperator.AND) {
@@ -207,14 +208,14 @@ export class MysqlSearchStrategy implements SearchStrategy {
                                     throw new UserInputError('error.facetfilterinput-invalid-input');
                                 }
                                 if (facetValueFilter.and) {
-                                    const placeholder = '_' + facetValueFilter.and;
+                                    const placeholder = createPlaceholderFromId(facetValueFilter.and);
                                     const clause = `FIND_IN_SET(:${placeholder}, facetValueIds)`;
                                     const params = { [placeholder]: facetValueFilter.and };
                                     qb2.where(clause, params);
                                 }
                                 if (facetValueFilter.or?.length) {
                                     for (const id of facetValueFilter.or) {
-                                        const placeholder = '_' + id;
+                                        const placeholder = createPlaceholderFromId(id);
                                         const clause = `FIND_IN_SET(:${placeholder}, facetValueIds)`;
                                         const params = { [placeholder]: id };
                                         qb2.orWhere(clause, params);

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

@@ -13,6 +13,7 @@ import { getFieldsToSelect } from './search-strategy-common';
 import {
     createCollectionIdCountMap,
     createFacetIdCountMap,
+    createPlaceholderFromId,
     mapToSearchResult,
 } from './search-strategy-utils';
 
@@ -189,7 +190,7 @@ export class PostgresSearchStrategy implements SearchStrategy {
             qb.andWhere(
                 new Brackets(qb1 => {
                     for (const id of facetValueIds) {
-                        const placeholder = '_' + id;
+                        const placeholder = createPlaceholderFromId(id);
                         const clause = `:${placeholder} = ANY (string_to_array(si.facetValueIds, ','))`;
                         const params = { [placeholder]: id };
                         if (facetValueOperator === LogicalOperator.AND) {
@@ -211,14 +212,14 @@ export class PostgresSearchStrategy implements SearchStrategy {
                                     throw new UserInputError('error.facetfilterinput-invalid-input');
                                 }
                                 if (facetValueFilter.and) {
-                                    const placeholder = '_' + facetValueFilter.and;
+                                    const placeholder = createPlaceholderFromId(facetValueFilter.and);
                                     const clause = `:${placeholder} = ANY (string_to_array(si.facetValueIds, ','))`;
                                     const params = { [placeholder]: facetValueFilter.and };
                                     qb2.where(clause, params);
                                 }
                                 if (facetValueFilter.or?.length) {
                                     for (const id of facetValueFilter.or) {
-                                        const placeholder = '_' + id;
+                                        const placeholder = createPlaceholderFromId(id);
                                         const clause = `:${placeholder} = ANY (string_to_array(si.facetValueIds, ','))`;
                                         const params = { [placeholder]: id };
                                         qb2.orWhere(clause, params);

+ 4 - 0
packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts

@@ -106,3 +106,7 @@ function parseFocalPoint(focalPoint: any): Coordinate | undefined {
     }
     return;
 }
+
+export function createPlaceholderFromId(id: ID): string {
+    return '_' + id.toString().replace(/-/g, '_');
+}

+ 4 - 3
packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -12,6 +12,7 @@ import { SearchStrategy } from './search-strategy';
 import {
     createCollectionIdCountMap,
     createFacetIdCountMap,
+    createPlaceholderFromId,
     mapToSearchResult,
 } from './search-strategy-utils';
 
@@ -172,7 +173,7 @@ export class SqliteSearchStrategy implements SearchStrategy {
             qb.andWhere(
                 new Brackets(qb1 => {
                     for (const id of facetValueIds) {
-                        const placeholder = '_' + id;
+                        const placeholder = createPlaceholderFromId(id);
                         const clause = `(',' || facetValueIds || ',') LIKE :${placeholder}`;
                         const params = { [placeholder]: `%,${id},%` };
                         if (facetValueOperator === LogicalOperator.AND) {
@@ -194,14 +195,14 @@ export class SqliteSearchStrategy implements SearchStrategy {
                                     throw new UserInputError('error.facetfilterinput-invalid-input');
                                 }
                                 if (facetValueFilter.and) {
-                                    const placeholder = '_' + facetValueFilter.and;
+                                    const placeholder = createPlaceholderFromId(facetValueFilter.and);
                                     const clause = `(',' || facetValueIds || ',') LIKE :${placeholder}`;
                                     const params = { [placeholder]: `%,${facetValueFilter.and},%` };
                                     qb2.where(clause, params);
                                 }
                                 if (facetValueFilter.or?.length) {
                                     for (const id of facetValueFilter.or) {
-                                        const placeholder = '_' + id;
+                                        const placeholder = createPlaceholderFromId(id);
                                         const clause = `(',' || facetValueIds || ',') LIKE :${placeholder}`;
                                         const params = { [placeholder]: `%,${id},%` };
                                         qb2.orWhere(clause, params);

+ 3 - 3
packages/create/package.json

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

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

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

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

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

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

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

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

@@ -1,12 +1,12 @@
 /* tslint:disable:no-non-null-assertion */
 import {
     AccountRegistrationEvent,
+    EntityHydrator,
     IdentifierChangeRequestEvent,
     NativeAuthenticationMethod,
     OrderStateTransitionEvent,
     PasswordResetEvent,
-    ShippingMethod,
-    TransactionalConnection,
+    ShippingLine,
 } from '@vendure/core';
 
 import { EmailEventHandler } from './event-handler';
@@ -25,29 +25,23 @@ export const orderConfirmationHandler = new EmailEventListener('order-confirmati
             event.toState === 'PaymentSettled' && event.fromState !== 'Modifying' && !!event.order.customer,
     )
     .loadData(async context => {
-        const shippingMethods: ShippingMethod[] = [];
+        const shippingLines: ShippingLine[] = [];
+        const entityHydrator = context.injector.get(EntityHydrator);
 
         for (const line of context.event.order.shippingLines || []) {
-            let shippingMethod: ShippingMethod | undefined;
-            if (!line.shippingMethod && line.shippingMethodId) {
-                shippingMethod = await context.injector
-                    .get(TransactionalConnection)
-                    .getRepository(ShippingMethod)
-                    .findOne(line.shippingMethodId);
-            } else if (line.shippingMethod) {
-                shippingMethod = line.shippingMethod;
-            }
-            if (shippingMethod) {
-                shippingMethods.push(shippingMethod);
+            await entityHydrator.hydrate(context.event.ctx, line, {
+                relations: ['shippingMethod'],
+            });
+            if (line.shippingMethod) {
+                shippingLines.push(line);
             }
         }
-
-        return { shippingMethods };
+        return { shippingLines };
     })
     .setRecipient(event => event.order.customer!.emailAddress)
     .setFrom(`{{ fromAddress }}`)
     .setSubject(`Order confirmation for #{{ order.code }}`)
-    .setTemplateVars(event => ({ order: event.order, shippingMethods: event.data.shippingMethods }))
+    .setTemplateVars(event => ({ order: event.order, shippingLines: event.data.shippingLines }))
     .setMockEvent(mockOrderStateTransitionEvent);
 
 export const emailVerificationHandler = new EmailEventListener('email-verification')

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

@@ -97,7 +97,7 @@
                     <td>
                         <img alt="{{ productVariant.name }}"
                              style="width: 50px; height: 50px;"
-                             src="{{ featuredAsset.preview }}?w=50&h=50" />
+                             src="assets/{{ featuredAsset.preview }}?w=50&h=50" />
                     </td>
                     <td>{{ quantity }} x {{ productVariant.name }}</td>
                     <td>{{ productVariant.quantity }}</td>
@@ -116,9 +116,9 @@
                 <td colspan="3">Sub-total:</td>
                 <td>${{ formatMoney order.subTotalWithTax }}</td>
             </tr>
-            {{#each shippingMethods }}
+            {{#each shippingLines }}
             <tr class="order-row">
-                <td colspan="3">Shipping ({{ name }}):</td>
+                <td colspan="3">Shipping ({{ shippingMethod.name }}):</td>
                 <td>${{ formatMoney priceWithTax }}</td>
             </tr>
             {{/each}}

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/job-queue-plugin",
-  "version": "1.4.4",
+  "version": "1.4.5",
   "license": "MIT",
   "main": "package/index.js",
   "types": "package/index.d.ts",
@@ -24,8 +24,8 @@
   "devDependencies": {
     "@google-cloud/pubsub": "^2.8.0",
     "@types/redis": "^2.8.28",
-    "@vendure/common": "^1.4.4",
-    "@vendure/core": "^1.4.4",
+    "@vendure/common": "^1.4.5",
+    "@vendure/core": "^1.4.5",
     "bullmq": "^1.40.1",
     "redis": "^3.0.2",
     "rimraf": "^3.0.2",

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

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

+ 3 - 3
packages/testing/package.json

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

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

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