Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
0375f787ad
48 changed files with 2618 additions and 2178 deletions
  1. 16 0
      CHANGELOG.md
  2. 1 0
      docs/content/developer-guide/customizing-models.md
  3. 83 1
      docs/content/developer-guide/deployment.md
  4. 1 1
      lerna.json
  5. 3 3
      packages/admin-ui-plugin/package.json
  6. 2 2
      packages/admin-ui/package.json
  7. 16 6
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html
  8. 1 1
      packages/admin-ui/src/lib/core/src/common/base-detail.component.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  10. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  11. 5 0
      packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss
  12. 7 2
      packages/admin-ui/src/lib/core/src/providers/i18n/i18n.service.ts
  13. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/prosemirror.service.ts
  14. 1 1
      packages/admin-ui/src/lib/core/src/shared/pipes/asset-preview.pipe.ts
  15. 489 475
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  16. 3 3
      packages/asset-server-plugin/package.json
  17. 1 1
      packages/common/package.json
  18. 2 2
      packages/common/src/generated-shop-types.ts
  19. 1 1
      packages/common/src/generated-types.ts
  20. 489 475
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  21. 95 2
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  22. 29 0
      packages/core/e2e/graphql/shop-definitions.ts
  23. 14 0
      packages/core/e2e/order.e2e-spec.ts
  24. 2 2
      packages/core/package.json
  25. 2 0
      packages/core/src/api/api-internal-modules.ts
  26. 4 1
      packages/core/src/api/config/graphql-custom-fields.ts
  27. 22 0
      packages/core/src/api/resolvers/entity/order-item-entity.resolver.ts
  28. 1 0
      packages/core/src/entity/index.ts
  29. 29 26
      packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts
  30. 21 1
      packages/core/src/service/services/channel.service.ts
  31. 14 0
      packages/core/src/service/services/fulfillment.service.ts
  32. 22 2
      packages/core/src/service/services/zone.service.ts
  33. 3 3
      packages/create/package.json
  34. 1 1
      packages/dev-server/dev-config.ts
  35. 3 4
      packages/dev-server/load-testing/init-load-test.ts
  36. 9 9
      packages/dev-server/package.json
  37. 489 475
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  38. 3 3
      packages/elasticsearch-plugin/package.json
  39. 222 177
      packages/elasticsearch-plugin/src/indexing/indexer.controller.ts
  40. 3 3
      packages/email-plugin/package.json
  41. 3 3
      packages/job-queue-plugin/package.json
  42. 489 475
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  43. 2 2
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  44. 4 4
      packages/payments-plugin/package.json
  45. 3 3
      packages/testing/package.json
  46. 4 4
      packages/ui-devkit/package.json
  47. 0 0
      schema-admin.json
  48. 0 0
      schema-shop.json

+ 16 - 0
CHANGELOG.md

@@ -1,3 +1,19 @@
+## <small>1.4.6 (2022-02-04)</small>
+
+
+#### Fixes
+
+* **admin-ui** Do not display "undefined" in rich text editor ([e80b8c5](https://github.com/vendure-ecommerce/vendure/commit/e80b8c5)), closes [#1374](https://github.com/vendure-ecommerce/vendure/issues/1374)
+* **admin-ui** Fix error when toggling product list grouping ([1427399](https://github.com/vendure-ecommerce/vendure/commit/1427399)), closes [#1384](https://github.com/vendure-ecommerce/vendure/issues/1384)
+* **admin-ui** Fix hyphenation of long words (#1390) ([671a998](https://github.com/vendure-ecommerce/vendure/commit/671a998)), closes [#1390](https://github.com/vendure-ecommerce/vendure/issues/1390)
+* **admin-ui** Fix localeString error when creating Product ([e7013d0](https://github.com/vendure-ecommerce/vendure/commit/e7013d0)), closes [#1378](https://github.com/vendure-ecommerce/vendure/issues/1378)
+* **admin-ui** Fix long nav items (#1362) ([ffc48c6](https://github.com/vendure-ecommerce/vendure/commit/ffc48c6)), closes [#1362](https://github.com/vendure-ecommerce/vendure/issues/1362) [#1361](https://github.com/vendure-ecommerce/vendure/issues/1361)
+* **core** Add missing Fulfillment entity export ([cc1e4ed](https://github.com/vendure-ecommerce/vendure/commit/cc1e4ed))
+* **core** Fix OrderAddress type AddressCustomFields error (#1394) ([b6dd5f4](https://github.com/vendure-ecommerce/vendure/commit/b6dd5f4)), closes [#1394](https://github.com/vendure-ecommerce/vendure/issues/1394) [#1377](https://github.com/vendure-ecommerce/vendure/issues/1377)
+* **core** Optimize DefaultSearchPlugin reindexing ([b9d2234](https://github.com/vendure-ecommerce/vendure/commit/b9d2234)), closes [#736](https://github.com/vendure-ecommerce/vendure/issues/736)
+* **core** Resolve OrderItem.fulfillment ([6a9efe9](https://github.com/vendure-ecommerce/vendure/commit/6a9efe9)), closes [#1381](https://github.com/vendure-ecommerce/vendure/issues/1381)
+* **elasticsearch-plugin** Fix high memory usage on reindex ([bce86f6](https://github.com/vendure-ecommerce/vendure/commit/bce86f6)), closes [#1120](https://github.com/vendure-ecommerce/vendure/issues/1120)
+
 ## <small>1.4.5 (2022-01-17)</small>
 
 

+ 1 - 0
docs/content/developer-guide/customizing-models.md

@@ -204,6 +204,7 @@ However, this sacrifices type safety. To make our custom fields type-safe we can
 
 ```TypeScript
 // types.ts
+import { CustomProductFields } from '@vendure/core';
 
 declare module '@vendure/core' {
   interface CustomProductFields {

+ 83 - 1
docs/content/developer-guide/deployment.md

@@ -16,6 +16,24 @@ A typical pattern is to run the Vendure app on the server, e.g. at `http://local
 
 Here is a good guide to setting up a production-ready server for an app such as Vendure: https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-18-04
 
+## Database Timezone
+
+Vendure internally treats all dates & times as UTC. However, you may sometimes run into issues where dates are offset by some fixed amount of hours. E.g. you place an order at 17:00, but it shows up in the Admin UI as being placed at 19:00. Typically, this is caused by the timezone of your database not being set to UTC.
+
+You can check the timezone in **MySQL/MariaDB** by executing:
+
+```SQL
+SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP);
+```
+and you should expect to see `00:00:00`.
+
+In **Postgres**, you can execute:
+```SQL
+show timezone;
+```
+and you should expect to see `UTC` or `Etc/UTC`.
+
+
 ## Security Considerations
 
 For a production Vendure server, there are a few security-related points to consider when deploying:
@@ -112,4 +130,68 @@ REQUEST: GET http://localhost:3020/health
 
 ## Admin UI
 
-If you have customized the Admin UI with extensions, it can make sense to [compile your extensions ahead-of-time as part of the deployment process]({{< relref "/docs/plugins/extending-the-admin-ui" >}}#compiling-as-a-deployment-step).
+If you have customized the Admin UI with extensions, you should [compile your extensions ahead-of-time as part of the deployment process]({{< relref "/docs/plugins/extending-the-admin-ui" >}}#compiling-as-a-deployment-step).
+
+### Deploying a stand-alone Admin UI
+
+Usually, the Admin UI is served from the Vendure server via the AdminUiPlugin. However, you may wish to deploy the Admin UI app elsewhere. Since it is just a static Angular app, it can be deployed to any static hosting service such as Vercel or Netlify.
+
+Here's an example script that can be run as part of your host's `build` command, which will generate a stand-alone app bundle and configure it to point to your remote server API.
+
+This example is for Vercel, and assumes:
+
+* A `BASE_HREF` environment variable to be set to `/`
+* A public (output) directory set to `build/dist`
+* A build command set to `npm run build` or `yarn build`
+* A `build` script defined in your package.json:
+    ```json
+    {
+      "scripts": {
+        "build": "ts-node compile.ts"
+      }
+    }
+    ```
+
+```TypeScript
+// compile.ts
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+import { DEFAULT_BASE_HREF } from '@vendure/ui-devkit/compiler/constants';
+import path from 'path';
+import { promises as fs } from 'fs';
+
+/**
+ * Compiles the Admin UI. If the BASE_HREF is defined, use that. 
+ * Otherwise, go back to the default admin route.
+ */
+compileUiExtensions({
+  outputPath: path.join(__dirname, 'build'),
+  baseHref: process.env.BASE_HREF ?? DEFAULT_BASE_HREF,
+  extensions: [
+      /* any UI extensions would go here, or leave empty */
+  ],
+})
+  .compile?.()
+  .then(() => {
+    // If building for Vercel deployment, replace the config to make 
+    // api calls to api.example.com instead of localhost.
+    if (process.env.VERCEL) {
+      console.log('Overwriting the vendure-ui-config.json for Vercel deployment.');
+      return fs.writeFile(
+        path.join(__dirname, 'build', 'dist', 'vendure-ui-config.json'),
+        JSON.stringify({
+          apiHost: 'https://api.example.com',
+          apiPort: '443',
+          adminApiPath: 'admin-api',
+          tokenMethod: 'cookie',
+          defaultLanguage: 'en',
+          availableLanguages: ['en', 'de'],
+          hideVendureBranding: false,
+          hideVersion: false,
+        }),
+      );
+    }
+  })
+  .then(() => {
+    process.exit(0);
+  });
+```

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
     "packages/*"
   ],
-  "version": "1.4.5",
+  "version": "1.4.6",
   "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.5",
+  "version": "1.4.6",
   "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.5",
-    "@vendure/core": "^1.4.5",
+    "@vendure/common": "^1.4.6",
+    "@vendure/core": "^1.4.6",
     "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.5",
+  "version": "1.4.6",
   "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.5",
+    "@vendure/common": "^1.4.6",
     "@webcomponents/custom-elements": "^1.4.3",
     "apollo-angular": "^2.6.0",
     "apollo-upload-client": "^16.0.0",

+ 16 - 6
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html

@@ -8,10 +8,17 @@
                 (facetValueChange)="setFacetValueIds($event)"
             ></vdr-product-search-input>
             <vdr-dropdown class="search-settings-menu mr3">
-                <button type="button"
-                        class="icon-button search-index-button"
-                        [title]="(pendingSearchIndexUpdates ? 'catalog.pending-search-index-updates' : 'catalog.search-index-controls') | translate "
-                        vdrDropdownTrigger>
+                <button
+                    type="button"
+                    class="icon-button search-index-button"
+                    [title]="
+                        (pendingSearchIndexUpdates
+                            ? 'catalog.pending-search-index-updates'
+                            : 'catalog.search-index-controls'
+                        ) | translate
+                    "
+                    vdrDropdownTrigger
+                >
                     <clr-icon shape="cog"></clr-icon>
                     <vdr-status-badge *ngIf="pendingSearchIndexUpdates" type="warning"> </vdr-status-badge>
                 </button>
@@ -26,7 +33,10 @@
                             [disabled]="!(['UpdateCatalog', 'UpdateProduct'] | hasPermission)"
                         >
                             <vdr-status-badge type="warning"> </vdr-status-badge>
-                            {{ 'catalog.run-pending-search-index-updates' | translate: { count: pendingSearchIndexUpdates } }}
+                            {{
+                                'catalog.run-pending-search-index-updates'
+                                    | translate: { count: pendingSearchIndexUpdates }
+                            }}
                         </button>
                         <div class="dropdown-divider"></div>
                     </ng-container>
@@ -43,7 +53,7 @@
         </div>
         <div class="flex wrap">
             <clr-checkbox-wrapper class="mt2">
-                <input type="checkbox" clrCheckbox [(ngModel)]="groupByProduct" />
+                <input type="checkbox" clrCheckbox [(ngModel)]="groupByProduct" (ngModelChange)="refresh()" />
                 <label>{{ 'catalog.group-by-product' | translate }}</label>
             </clr-checkbox-wrapper>
             <vdr-language-selector

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/base-detail.component.ts

@@ -127,7 +127,7 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt
             const key = fieldDef.name;
             const value =
                 fieldDef.type === 'localeString'
-                    ? (currentTranslation as any).customFields?.[key]
+                    ? (currentTranslation as any)?.customFields?.[key]
                     : (entity as any).customFields?.[key];
             const control = formGroup?.get(key);
             if (control) {

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

@@ -4058,7 +4058,7 @@ export type Query = {
   assets: AssetList;
   channel?: Maybe<Channel>;
   channels: Array<Channel>;
-  /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
+  /** Get a Collection either by id or slug. If neither id nor slug is specified, an error will result. */
   collection?: Maybe<Collection>;
   collectionFilters: Array<ConfigurableOperationDefinition>;
   collections: CollectionList;

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

+ 5 - 0
packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss

@@ -18,6 +18,7 @@ nav.sidenav {
     }
     .nav-group-header {
         margin: 0;
+        line-height: 1.2;
     }
     .nav-link {
         display: inline-flex;
@@ -31,6 +32,10 @@ nav.sidenav {
     margin-right: 12px;
 }
 
+.nav-group {
+    hyphens: auto;
+}
+
 .nav-group,
 .nav-link {
     position: relative;

+ 7 - 2
packages/admin-ui/src/lib/core/src/providers/i18n/i18n.service.ts

@@ -1,8 +1,10 @@
-import { Injectable } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+import { Inject, Injectable } from '@angular/core';
 import { TranslateService } from '@ngx-translate/core';
 
 import { LanguageCode } from '../../common/generated-types';
 
+/** @dynamic */
 @Injectable({
     providedIn: 'root',
 })
@@ -13,7 +15,7 @@ export class I18nService {
         return [...this._availableLanguages];
     }
 
-    constructor(private ngxTranslate: TranslateService) {}
+    constructor(private ngxTranslate: TranslateService, @Inject(DOCUMENT) private document: Document) {}
 
     /**
      * Set the default language
@@ -27,6 +29,9 @@ export class I18nService {
      */
     setLanguage(language: LanguageCode): void {
         this.ngxTranslate.use(language);
+        if (this.document?.documentElement) {
+            this.document.documentElement.lang = language;
+        }
     }
 
     /**

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/prosemirror.service.ts

@@ -81,9 +81,9 @@ export class ProsemirrorService {
         }
     }
 
-    private getStateFromText(text: string): EditorState {
+    private getStateFromText(text: string | null | undefined): EditorState {
         const div = document.createElement('div');
-        div.innerHTML = text;
+        div.innerHTML = text ?? '';
         return EditorState.create({
             doc: DOMParser.fromSchema(this.mySchema).parse(div),
             plugins: this.configurePlugins({ schema: this.mySchema, floatingMenu: false }),

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/pipes/asset-preview.pipe.ts

@@ -24,7 +24,7 @@ export class AssetPreviewPipe implements PipeTransform {
         if (!asset) {
             return '';
         }
-        if (!asset.preview || typeof asset.preview !== 'string') {
+        if (asset.preview == null || typeof asset.preview !== 'string') {
             throw new Error(`Expected an Asset, got ${JSON.stringify(asset)}`);
         }
         const fp = asset.focalPoint ? `&fpx=${asset.focalPoint.x}&fpy=${asset.focalPoint.y}` : '';

File diff suppressed because it is too large
+ 489 - 475
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "1.4.5",
+  "version": "1.4.6",
   "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.5",
-    "@vendure/core": "^1.4.5",
+    "@vendure/common": "^1.4.6",
+    "@vendure/core": "^1.4.6",
     "aws-sdk": "^2.856.0",
     "express": "^4.17.1",
     "node-fetch": "^2.6.1",

+ 1 - 1
packages/common/package.json

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

+ 2 - 2
packages/common/src/generated-shop-types.ts

@@ -2595,7 +2595,7 @@ export type Query = {
     activeCustomer?: Maybe<Customer>;
     /**
      * The active Order. Will be `null` until an Order is created via `addItemToOrder`. Once an Order reaches the
-     * state of `PaymentApproved` or `PaymentSettled`, then that Order is no longer considered "active" and this
+     * state of `PaymentAuthorized` or `PaymentSettled`, then that Order is no longer considered "active" and this
      * query will once again return `null`.
      */
     activeOrder?: Maybe<Order>;
@@ -2603,7 +2603,7 @@ export type Query = {
     availableCountries: Array<Country>;
     /** A list of Collections available to the shop */
     collections: CollectionList;
-    /** Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is speicified, an error will result. */
+    /** Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is specified, an error will result. */
     collection?: Maybe<Collection>;
     /** Returns a list of eligible shipping methods based on the current active Order */
     eligibleShippingMethods: Array<ShippingMethodQuote>;

+ 1 - 1
packages/common/src/generated-types.ts

@@ -3995,7 +3995,7 @@ export type Query = {
   channel?: Maybe<Channel>;
   activeChannel: Channel;
   collections: CollectionList;
-  /** Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result. */
+  /** Get a Collection either by id or slug. If neither id nor slug is specified, an error will result. */
   collection?: Maybe<Collection>;
   collectionFilters: Array<ConfigurableOperationDefinition>;
   countries: CountryList;

File diff suppressed because it is too large
+ 489 - 475
packages/core/e2e/graphql/generated-e2e-admin-types.ts


+ 95 - 2
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -2505,7 +2505,7 @@ export type Query = {
     activeCustomer?: Maybe<Customer>;
     /**
      * The active Order. Will be `null` until an Order is created via `addItemToOrder`. Once an Order reaches the
-     * state of `PaymentApproved` or `PaymentSettled`, then that Order is no longer considered "active" and this
+     * state of `PaymentAuthorized` or `PaymentSettled`, then that Order is no longer considered "active" and this
      * query will once again return `null`.
      */
     activeOrder?: Maybe<Order>;
@@ -2513,7 +2513,7 @@ export type Query = {
     availableCountries: Array<Country>;
     /** A list of Collections available to the shop */
     collections: CollectionList;
-    /** Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is speicified, an error will result. */
+    /** Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is specified, an error will result. */
     collection?: Maybe<Collection>;
     /** Returns a list of eligible shipping methods based on the current active Order */
     eligibleShippingMethods: Array<ShippingMethodQuote>;
@@ -3439,6 +3439,32 @@ export type GetOrderByCodeWithPaymentsQueryVariables = Exact<{
 
 export type GetOrderByCodeWithPaymentsQuery = { orderByCode?: Maybe<TestOrderWithPaymentsFragment> };
 
+export type GetActiveCustomerOrderWithItemFulfillmentsQueryVariables = Exact<{
+    code: Scalars['String'];
+}>;
+
+export type GetActiveCustomerOrderWithItemFulfillmentsQuery = {
+    activeCustomer?: Maybe<{
+        orders: Pick<OrderList, 'totalItems'> & {
+            items: Array<
+                Pick<Order, 'id' | 'code' | 'state'> & {
+                    lines: Array<
+                        Pick<OrderLine, 'id'> & {
+                            items: Array<
+                                Pick<OrderItem, 'id'> & {
+                                    fulfillment?: Maybe<
+                                        Pick<Fulfillment, 'id' | 'state' | 'method' | 'trackingCode'>
+                                    >;
+                                }
+                            >;
+                        }
+                    >;
+                }
+            >;
+        };
+    }>;
+};
+
 export type GetNextOrderStatesQueryVariables = Exact<{ [key: string]: never }>;
 
 export type GetNextOrderStatesQuery = Pick<Query, 'nextOrderStates'>;
@@ -3958,6 +3984,73 @@ export namespace GetOrderByCodeWithPayments {
     export type OrderByCode = NonNullable<GetOrderByCodeWithPaymentsQuery['orderByCode']>;
 }
 
+export namespace GetActiveCustomerOrderWithItemFulfillments {
+    export type Variables = GetActiveCustomerOrderWithItemFulfillmentsQueryVariables;
+    export type Query = GetActiveCustomerOrderWithItemFulfillmentsQuery;
+    export type ActiveCustomer = NonNullable<
+        GetActiveCustomerOrderWithItemFulfillmentsQuery['activeCustomer']
+    >;
+    export type Orders = NonNullable<
+        NonNullable<GetActiveCustomerOrderWithItemFulfillmentsQuery['activeCustomer']>['orders']
+    >;
+    export type Items = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<GetActiveCustomerOrderWithItemFulfillmentsQuery['activeCustomer']>['orders']
+            >['items']
+        >[number]
+    >;
+    export type Lines = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<
+                    NonNullable<
+                        NonNullable<
+                            GetActiveCustomerOrderWithItemFulfillmentsQuery['activeCustomer']
+                        >['orders']
+                    >['items']
+                >[number]
+            >['lines']
+        >[number]
+    >;
+    export type _Items = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<
+                    NonNullable<
+                        NonNullable<
+                            NonNullable<
+                                NonNullable<
+                                    GetActiveCustomerOrderWithItemFulfillmentsQuery['activeCustomer']
+                                >['orders']
+                            >['items']
+                        >[number]
+                    >['lines']
+                >[number]
+            >['items']
+        >[number]
+    >;
+    export type Fulfillment = NonNullable<
+        NonNullable<
+            NonNullable<
+                NonNullable<
+                    NonNullable<
+                        NonNullable<
+                            NonNullable<
+                                NonNullable<
+                                    NonNullable<
+                                        GetActiveCustomerOrderWithItemFulfillmentsQuery['activeCustomer']
+                                    >['orders']
+                                >['items']
+                            >[number]
+                        >['lines']
+                    >[number]
+                >['items']
+            >[number]
+        >['fulfillment']
+    >;
+}
+
 export namespace GetNextOrderStates {
     export type Variables = GetNextOrderStatesQueryVariables;
     export type Query = GetNextOrderStatesQuery;

+ 29 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -613,6 +613,35 @@ export const GET_ORDER_BY_CODE_WITH_PAYMENTS = gql`
     ${TEST_ORDER_WITH_PAYMENTS_FRAGMENT}
 `;
 
+export const GET_ACTIVE_ORDER_CUSTOMER_WITH_ITEM_FULFILLMENTS = gql`
+    query GetActiveCustomerOrderWithItemFulfillments {
+        activeCustomer {
+            orders(
+                options: { skip: 0, take: 5, sort: { createdAt: DESC }, filter: { active: { eq: false } } }
+            ) {
+                totalItems
+                items {
+                    id
+                    code
+                    state
+                    lines {
+                        id
+                        items {
+                            id
+                            fulfillment {
+                                id
+                                state
+                                method
+                                trackingCode
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+`;
+
 export const GET_NEXT_STATES = gql`
     query GetNextOrderStates {
         nextOrderStates

+ 14 - 0
packages/core/e2e/order.e2e-spec.ts

@@ -70,6 +70,7 @@ import {
     AddPaymentToOrder,
     ApplyCouponCode,
     DeletionResult,
+    GetActiveCustomerOrderWithItemFulfillments,
     GetActiveCustomerWithOrdersProductSlug,
     GetActiveOrder,
     GetOrderByCodeWithPayments,
@@ -102,6 +103,7 @@ import {
     APPLY_COUPON_CODE,
     GET_ACTIVE_CUSTOMER_WITH_ORDERS_PRODUCT_SLUG,
     GET_ACTIVE_ORDER,
+    GET_ACTIVE_ORDER_CUSTOMER_WITH_ITEM_FULFILLMENTS,
     GET_ORDER_BY_CODE_WITH_PAYMENTS,
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_METHOD,
@@ -983,6 +985,18 @@ describe('Orders resolver', () => {
             expect(order!.fulfillments![1].orderItems).toEqual([{ id: 'T_4' }, { id: 'T_5' }, { id: 'T_6' }]);
             expect(order!.fulfillments![2].orderItems).toEqual([{ id: 'T_4' }, { id: 'T_5' }, { id: 'T_6' }]);
         });
+
+        it('order.line.items.fulfillment resolver', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            const { activeCustomer } = await shopClient.query<
+                GetActiveCustomerOrderWithItemFulfillments.Query,
+                GetActiveCustomerOrderWithItemFulfillments.Variables
+            >(GET_ACTIVE_ORDER_CUSTOMER_WITH_ITEM_FULFILLMENTS);
+            const firstCustomerOrder = activeCustomer!.orders.items[0]!;
+            expect(firstCustomerOrder.lines[0].items[0].fulfillment).not.toBeNull();
+        });
     });
 
     describe('cancellation by orderId', () => {

+ 2 - 2
packages/core/package.json

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

+ 2 - 0
packages/core/src/api/api-internal-modules.ts

@@ -52,6 +52,7 @@ import {
 } from './resolvers/entity/fulfillment-entity.resolver';
 import { JobEntityResolver } from './resolvers/entity/job-entity.resolver';
 import { OrderAdminEntityResolver, OrderEntityResolver } from './resolvers/entity/order-entity.resolver';
+import { OrderItemEntityResolver } from './resolvers/entity/order-item-entity.resolver';
 import { OrderLineEntityResolver } from './resolvers/entity/order-line-entity.resolver';
 import {
     PaymentAdminEntityResolver,
@@ -123,6 +124,7 @@ export const entityResolvers = [
     FacetValueEntityResolver,
     FulfillmentEntityResolver,
     OrderEntityResolver,
+    OrderItemEntityResolver,
     OrderLineEntityResolver,
     PaymentEntityResolver,
     ProductEntityResolver,

+ 4 - 1
packages/core/src/api/config/graphql-custom-fields.ts

@@ -185,7 +185,10 @@ export function addGraphQLCustomFields(
         }
     }
 
-    if (customFieldConfig.Address?.length) {
+    const publicAddressFields = customFieldConfig.Address?.filter(
+        config => !config.internal && (publicOnly === true ? config.public !== false : true),
+    );
+    if (publicAddressFields?.length) {
         // For custom fields on the Address entity, we also extend the OrderAddress
         // type (which is used to store address snapshots on Orders)
         if (schema.getType('OrderAddress')) {

+ 22 - 0
packages/core/src/api/resolvers/entity/order-item-entity.resolver.ts

@@ -0,0 +1,22 @@
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+
+import { Fulfillment, OrderItem } from '../../../entity';
+import { FulfillmentService } from '../../../service';
+import { RequestContext } from '../../common/request-context';
+import { Ctx } from '../../decorators/request-context.decorator';
+
+@Resolver('OrderItem')
+export class OrderItemEntityResolver {
+    constructor(private fulfillmentService: FulfillmentService) {}
+
+    @ResolveField()
+    async fulfillment(
+        @Ctx() ctx: RequestContext,
+        @Parent() orderItem: OrderItem,
+    ): Promise<Fulfillment | undefined> {
+        if (orderItem.fulfillment) {
+            return orderItem.fulfillment;
+        }
+        return this.fulfillmentService.getFulfillmentByOrderItemId(ctx, orderItem.id);
+    }
+}

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

@@ -18,6 +18,7 @@ export * from './facet/facet.entity';
 export * from './facet/facet-translation.entity';
 export * from './facet-value/facet-value.entity';
 export * from './facet-value/facet-value-translation.entity';
+export * from './fulfillment/fulfillment.entity';
 export * from './global-settings/global-settings.entity';
 export * from './order/order.entity';
 export * from './order-item/order-item.entity';

+ 29 - 26
packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts

@@ -35,12 +35,8 @@ import {
 import { MutableRequestContext } from './mutable-request-context';
 
 export const BATCH_SIZE = 1000;
+export const productRelations = ['featuredAsset', 'facetValues', 'facetValues.facet', 'channels'];
 export const variantRelations = [
-    'product',
-    'product.featuredAsset',
-    'product.facetValues',
-    'product.facetValues.facet',
-    'product.channels',
     'featuredAsset',
     'facetValues',
     'facetValues.facet',
@@ -83,7 +79,6 @@ export class IndexerController {
                 Logger.verbose(`Processing batch ${i + 1} of ${batches}`, workerLoggerCtx);
 
                 const variants = await qb
-                    .andWhere('variants__product.deletedAt IS NULL')
                     .take(BATCH_SIZE)
                     .skip(i * BATCH_SIZE)
                     .getMany();
@@ -95,6 +90,7 @@ export class IndexerController {
                 });
             }
             Logger.verbose(`Completed reindexing`, workerLoggerCtx);
+
             return {
                 total: count,
                 completed: count,
@@ -312,7 +308,7 @@ export class IndexerController {
         qb.leftJoin('variants.product', 'product')
             .leftJoin('product.channels', 'channel')
             .where('channel.id = :channelId', { channelId })
-            .andWhere('variants__product.deletedAt IS NULL')
+            .andWhere('product.deletedAt IS NULL')
             .andWhere('variants.deletedAt IS NULL');
         return qb;
     }
@@ -321,14 +317,23 @@ export class IndexerController {
         const items: SearchIndexItem[] = [];
 
         await this.removeSyntheticVariants(variants);
+        const productMap = new Map<ID, Product>();
 
         for (const variant of variants) {
+            let product = productMap.get(variant.productId);
+            if (!product) {
+                product = await this.connection.getEntityOrThrow(ctx, Product, variant.productId, {
+                    relations: productRelations,
+                });
+                productMap.set(variant.productId, product);
+            }
+
             const languageVariants = unique([
                 ...variant.translations.map(t => t.languageCode),
-                ...variant.product.translations.map(t => t.languageCode),
+                ...product.translations.map(t => t.languageCode),
             ]);
             for (const languageCode of languageVariants) {
-                const productTranslation = this.getTranslation(variant.product, languageCode);
+                const productTranslation = this.getTranslation(product, languageCode);
                 const variantTranslation = this.getTranslation(variant, languageCode);
                 const collectionTranslations = variant.collections.map(c =>
                     this.getTranslation(c, languageCode),
@@ -344,29 +349,25 @@ export class IndexerController {
                         price: variant.price,
                         priceWithTax: variant.priceWithTax,
                         sku: variant.sku,
-                        enabled: variant.product.enabled === false ? false : variant.enabled,
+                        enabled: product.enabled === false ? false : variant.enabled,
                         slug: productTranslation.slug,
-                        productId: variant.product.id,
+                        productId: product.id,
                         productName: productTranslation.name,
                         description: this.constrainDescription(productTranslation.description),
                         productVariantName: variantTranslation.name,
-                        productAssetId: variant.product.featuredAsset
-                            ? variant.product.featuredAsset.id
-                            : null,
-                        productPreviewFocalPoint: variant.product.featuredAsset
-                            ? variant.product.featuredAsset.focalPoint
+                        productAssetId: product.featuredAsset ? product.featuredAsset.id : null,
+                        productPreviewFocalPoint: product.featuredAsset
+                            ? product.featuredAsset.focalPoint
                             : null,
                         productVariantPreviewFocalPoint: variant.featuredAsset
                             ? variant.featuredAsset.focalPoint
                             : null,
                         productVariantAssetId: variant.featuredAsset ? variant.featuredAsset.id : null,
-                        productPreview: variant.product.featuredAsset
-                            ? variant.product.featuredAsset.preview
-                            : '',
+                        productPreview: product.featuredAsset ? product.featuredAsset.preview : '',
                         productVariantPreview: variant.featuredAsset ? variant.featuredAsset.preview : '',
                         channelIds: variant.channels.map(c => c.id as string),
-                        facetIds: this.getFacetIds(variant),
-                        facetValueIds: this.getFacetValueIds(variant),
+                        facetIds: this.getFacetIds(variant, product),
+                        facetValueIds: this.getFacetValueIds(variant, product),
                         collectionIds: variant.collections.map(c => c.id.toString()),
                         collectionSlugs: collectionTranslations.map(c => c.slug),
                     });
@@ -401,7 +402,9 @@ export class IndexerController {
             }
         }
 
-        await this.queue.push(() => this.connection.getRepository(SearchIndexItem).save(items, { chunk: 2500 }));
+        await this.queue.push(() =>
+            this.connection.getRepository(SearchIndexItem).save(items, { chunk: 2500 }),
+        );
     }
 
     /**
@@ -463,17 +466,17 @@ export class IndexerController {
             translatable.translations[0]) as unknown as Translation<T>;
     }
 
-    private getFacetIds(variant: ProductVariant): string[] {
+    private getFacetIds(variant: ProductVariant, product: Product): string[] {
         const facetIds = (fv: FacetValue) => fv.facet.id.toString();
         const variantFacetIds = variant.facetValues.map(facetIds);
-        const productFacetIds = variant.product.facetValues.map(facetIds);
+        const productFacetIds = product.facetValues.map(facetIds);
         return unique([...variantFacetIds, ...productFacetIds]);
     }
 
-    private getFacetValueIds(variant: ProductVariant): string[] {
+    private getFacetValueIds(variant: ProductVariant, product: Product): string[] {
         const facetValueIds = (fv: FacetValue) => fv.id.toString();
         const variantFacetValueIds = variant.facetValues.map(facetValueIds);
-        const productFacetValueIds = variant.product.facetValues.map(facetValueIds);
+        const productFacetValueIds = product.facetValues.map(facetValueIds);
         return unique([...variantFacetValueIds, ...productFacetValueIds]);
     }
 

+ 21 - 1
packages/core/src/service/services/channel.service.ts

@@ -60,7 +60,16 @@ export class ChannelService {
      */
     async initChannels() {
         await this.ensureDefaultChannelExists();
-        this.allChannels = await createSelfRefreshingCache({
+        await this.ensureCacheExists();
+    }
+
+    /**
+     * Creates a channels cache, that can be used to reduce number of channel queries to database 
+     *
+     * @internal
+     */
+    async createCache(): Promise<SelfRefreshingCache<Channel[], [RequestContext]>> {
+        return createSelfRefreshingCache({
             name: 'ChannelService.allChannels',
             ttl: this.configService.entityOptions.channelCacheTtl,
             refresh: { fn: ctx => this.findAll(ctx), defaultArgs: [RequestContext.empty()] },
@@ -263,6 +272,17 @@ export class ChannelService {
             .relations.find(r => r.type === Channel && r.propertyName === 'channels');
     }
 
+    /**
+     * Ensures channel cache exists. If not, this method creates one.
+     */
+    private async ensureCacheExists() {
+        if (this.allChannels) {
+            return
+        }
+
+        this.allChannels = await this.createCache();
+    }
+
     /**
      * There must always be a default Channel. If none yet exists, this method creates one.
      * Also ensures the default Channel token matches the defaultChannelToken config setting.

+ 14 - 0
packages/core/src/service/services/fulfillment.service.ts

@@ -115,6 +115,20 @@ export class FulfillmentService {
         return fulfillment.orderItems;
     }
 
+    /**
+     * @description
+     * Returns the Fulfillment for the given OrderItem (if one exists).
+     */
+    async getFulfillmentByOrderItemId(
+        ctx: RequestContext,
+        orderItemId: ID,
+    ): Promise<Fulfillment | undefined> {
+        const orderItem = await this.connection
+            .getRepository(ctx, OrderItem)
+            .findOne(orderItemId, { relations: ['fulfillments'] });
+        return orderItem?.fulfillment;
+    }
+
     /**
      * @description
      * Transitions the specified Fulfillment to a new state and upon successful transition

+ 22 - 2
packages/core/src/service/services/zone.service.ts

@@ -40,11 +40,20 @@ export class ZoneService {
         private connection: TransactionalConnection,
         private configService: ConfigService,
         private eventBus: EventBus,
-    ) {}
+    ) { }
 
     /** @internal */
     async initZones() {
-        this.zones = await createSelfRefreshingCache({
+        await this.ensureCacheExists();
+    }
+
+    /**
+     * Creates a zones cache, that can be used to reduce number of zones queries to database 
+     *
+     * @internal
+     */
+    async createCache(): Promise<SelfRefreshingCache<Zone[], [RequestContext]>> {
+        return await createSelfRefreshingCache({
             name: 'ZoneService.zones',
             ttl: this.configService.entityOptions.zoneCacheTtl,
             refresh: {
@@ -177,4 +186,15 @@ export class ZoneService {
     private getCountriesFromIds(ctx: RequestContext, ids: ID[]): Promise<Country[]> {
         return this.connection.getRepository(ctx, Country).findByIds(ids);
     }
+
+    /**
+    * Ensures zones cache exists. If not, this method creates one.
+    */
+    private async ensureCacheExists() {
+        if (this.zones) {
+            return
+        }
+
+        this.zones = await this.createCache();
+    }
 }

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "1.4.5",
+  "version": "1.4.6",
   "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.5",
+    "@vendure/core": "^1.4.6",
     "rimraf": "^3.0.2",
     "ts-node": "^10.2.1",
     "typescript": "4.3.5"
   },
   "dependencies": {
-    "@vendure/common": "^1.4.5",
+    "@vendure/common": "^1.4.6",
     "chalk": "^4.1.0",
     "commander": "^7.1.0",
     "cross-spawn": "^7.0.3",

+ 1 - 1
packages/dev-server/dev-config.ts

@@ -56,7 +56,7 @@ export const devConfig: VendureConfig = {
         paymentMethodHandlers: [dummyPaymentHandler],
     },
     customFields: {},
-    logger: new DefaultLogger({ level: LogLevel.Debug }),
+    logger: new DefaultLogger({ level: LogLevel.Verbose }),
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },

+ 3 - 4
packages/dev-server/load-testing/init-load-test.ts

@@ -2,7 +2,6 @@
 /// <reference path="../../core/typings.d.ts" />
 import { bootstrap, JobQueueService, Logger } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
-import { BaseProductRecord } from '@vendure/core/dist/data-import/providers/import-parser/import-parser';
 import { clearAllTables, populateCustomers } from '@vendure/testing';
 import stringify from 'csv-stringify';
 import fs from 'fs';
@@ -132,7 +131,7 @@ async function isDatabasePopulated(): Promise<boolean> {
  * Generates a CSV file of test product data which can then be imported into Vendure.
  */
 function generateProductsCsv(productCount: number = 100): Promise<void> {
-    const result: BaseProductRecord[] = [];
+    const result: any[] = [];
 
     const stringifier = stringify({
         delimiter: ',',
@@ -166,7 +165,7 @@ function generateProductsCsv(productCount: number = 100): Promise<void> {
 }
 
 function generateMockData(productCount: number, writeFn: (row: string[]) => void) {
-    const headers: Array<keyof BaseProductRecord> = [
+    const headers: string[] = [
         'name',
         'slug',
         'description',
@@ -188,7 +187,7 @@ function generateMockData(productCount: number, writeFn: (row: string[]) => void
     const categories = getCategoryNames();
 
     for (let i = 1; i <= productCount; i++) {
-        const outputRow: BaseProductRecord = {
+        const outputRow = {
             name: `Product ${i}`,
             slug: `product-${i}`,
             description: generateProductDescription(),

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

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

File diff suppressed because it is too large
+ 489 - 475
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts


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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "1.4.5",
+  "version": "1.4.6",
   "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.5",
-    "@vendure/core": "^1.4.5",
+    "@vendure/common": "^1.4.6",
+    "@vendure/core": "^1.4.6",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
   }

+ 222 - 177
packages/elasticsearch-plugin/src/indexing/indexer.controller.ts

@@ -45,6 +45,9 @@ import {
 import { createIndices, getClient, getIndexNameByAlias } from './indexing-utils';
 import { MutableRequestContext } from './mutable-request-context';
 
+const REINDEX_CHUNK_SIZE = 2500;
+const REINDEX_OPERATION_CHUNK_SIZE = 3000;
+
 export const defaultProductRelations: Array<EntityRelationPaths<Product>> = [
     'variants',
     'featuredAsset',
@@ -232,12 +235,12 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return asyncObservable(async observer => {
             return this.asyncQueue.push(async () => {
                 const timeStart = Date.now();
-                const operations: BulkVariantOperation[] = [];
                 const ctx = MutableRequestContext.deserialize(rawContext);
 
                 const reindexTempName = new Date().getTime();
                 const variantIndexName = this.options.indexPrefix + VARIANT_INDEX_NAME;
-                const reindexVariantAliasName = variantIndexName + `-reindex-${reindexTempName}`;
+                const variantIndexNameForReindex = VARIANT_INDEX_NAME + `-reindex-${reindexTempName}`;
+                const reindexVariantAliasName = this.options.indexPrefix + variantIndexNameForReindex;
                 try {
                     await createIndices(
                         this.client,
@@ -247,79 +250,123 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         true,
                         `-reindex-${reindexTempName}`,
                     );
+                } catch (e) {
+                    Logger.error(`Could not recreate indices.`, loggerCtx);
+                    Logger.error(JSON.stringify(e), loggerCtx);
+                    throw e;
+                }
 
-                    const reindexVariantIndexName = await getIndexNameByAlias(
-                        this.client,
-                        reindexVariantAliasName,
+                const totalProductIds = await this.connection
+                    .getRepository(Product)
+                    .createQueryBuilder('product')
+                    .where('product.deletedAt IS NULL')
+                    .getCount();
+
+                Logger.verbose(`Will reindex ${totalProductIds} products`, loggerCtx);
+
+                let productIds = [];
+                let skip = 0;
+                let finishedProductsCount = 0;
+                do {
+                    const operations: BulkVariantOperation[] = [];
+
+                    productIds = await this.connection
+                        .getRepository(Product)
+                        .createQueryBuilder('product')
+                        .select('product.id')
+                        .where('product.deletedAt IS NULL')
+                        .skip(skip)
+                        .take(REINDEX_CHUNK_SIZE)
+                        .getMany();
+
+                    for (const { id: productId } of productIds) {
+                        operations.push(...(await this.updateProductsOperationsOnly(ctx, productId)));
+                        finishedProductsCount++;
+                        observer.next({
+                            total: totalProductIds,
+                            completed: Math.min(finishedProductsCount, totalProductIds),
+                            duration: +new Date() - timeStart,
+                        });
+                    }
+
+                    Logger.verbose(`Will execute ${operations.length} bulk update operations`, loggerCtx);
+
+                    // Because we can have a huge amount of variant for 1 product, we also chunk update operations
+                    await this.executeBulkOperationsByChunks(
+                        REINDEX_OPERATION_CHUNK_SIZE,
+                        operations,
+                        variantIndexNameForReindex,
                     );
-                    const originalVariantAliasExist = await this.client.indices.existsAlias({
-                        name: variantIndexName,
-                    });
-                    const originalVariantIndexExist = await this.client.indices.exists({
-                        index: variantIndexName,
+
+                    skip += REINDEX_CHUNK_SIZE;
+
+                    Logger.verbose(`Done ${finishedProductsCount} / ${totalProductIds} products`);
+                } while (productIds.length >= REINDEX_CHUNK_SIZE);
+
+                // Switch the index to the new reindexed one
+                try {
+                    const reindexVariantAliasExist = await this.client.indices.existsAlias({
+                        name: reindexVariantAliasName,
                     });
+                    if (reindexVariantAliasExist) {
+                        const reindexVariantIndexName = await getIndexNameByAlias(
+                            this.client,
+                            reindexVariantAliasName,
+                        );
+                        const originalVariantAliasExist = await this.client.indices.existsAlias({
+                            name: variantIndexName,
+                        });
+                        const originalVariantIndexExist = await this.client.indices.exists({
+                            index: variantIndexName,
+                        });
 
-                    const originalVariantIndexName = await getIndexNameByAlias(this.client, variantIndexName);
+                        const originalVariantIndexName = await getIndexNameByAlias(
+                            this.client,
+                            variantIndexName,
+                        );
 
-                    if (originalVariantAliasExist.body || originalVariantIndexExist.body) {
-                        await this.client.reindex({
-                            refresh: true,
-                            body: {
-                                source: {
-                                    index: variantIndexName,
+                        const actions = [
+                            {
+                                remove: {
+                                    index: reindexVariantIndexName,
+                                    alias: reindexVariantAliasName,
                                 },
-                                dest: {
-                                    index: reindexVariantAliasName,
+                            },
+                            {
+                                add: {
+                                    index: reindexVariantIndexName,
+                                    alias: variantIndexName,
                                 },
                             },
-                        });
-                    }
+                        ];
 
-                    const actions = [
-                        {
-                            remove: {
-                                index: reindexVariantIndexName,
-                                alias: reindexVariantAliasName,
-                            },
-                        },
-                        {
-                            add: {
-                                index: reindexVariantIndexName,
-                                alias: variantIndexName,
-                            },
-                        },
-                    ];
+                        if (originalVariantAliasExist.body) {
+                            actions.push({
+                                remove: {
+                                    index: originalVariantIndexName,
+                                    alias: variantIndexName,
+                                },
+                            });
+                        } else if (originalVariantIndexExist.body) {
+                            await this.client.indices.delete({
+                                index: [variantIndexName],
+                            });
+                        }
 
-                    if (originalVariantAliasExist.body) {
-                        actions.push({
-                            remove: {
-                                index: originalVariantIndexName,
-                                alias: variantIndexName,
+                        await this.client.indices.updateAliases({
+                            body: {
+                                actions,
                             },
                         });
-                    } else if (originalVariantIndexExist.body) {
-                        await this.client.indices.delete({
-                            index: [variantIndexName],
-                        });
-                    }
 
-                    await this.client.indices.updateAliases({
-                        body: {
-                            actions,
-                        },
-                    });
-
-                    if (originalVariantAliasExist.body) {
-                        await this.client.indices.delete({
-                            index: [originalVariantIndexName],
-                        });
+                        if (originalVariantAliasExist.body) {
+                            await this.client.indices.delete({
+                                index: [originalVariantIndexName],
+                            });
+                        }
                     }
                 } catch (e) {
-                    Logger.warn(
-                        `Could not recreate indices. Reindexing continue with existing indices.`,
-                        loggerCtx,
-                    );
-                    Logger.warn(JSON.stringify(e), loggerCtx);
+                    Logger.error('Could not switch indexes');
                 } finally {
                     const reindexVariantAliasExist = await this.client.indices.existsAlias({
                         name: reindexVariantAliasName,
@@ -335,48 +382,37 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                     }
                 }
 
-                const deletedProductIds = await this.connection
-                    .getRepository(Product)
-                    .createQueryBuilder('product')
-                    .select('product.id')
-                    .where('product.deletedAt IS NOT NULL')
-                    .getMany();
-
-                for (const { id: deletedProductId } of deletedProductIds) {
-                    operations.push(...(await this.deleteProductOperations(ctx, deletedProductId)));
-                }
-
-                const productIds = await this.connection
-                    .getRepository(Product)
-                    .createQueryBuilder('product')
-                    .select('product.id')
-                    .where('product.deletedAt IS NULL')
-                    .getMany();
-
-                Logger.verbose(`Reindexing ${productIds.length} Products`, loggerCtx);
-
-                let finishedProductsCount = 0;
-                for (const { id: productId } of productIds) {
-                    operations.push(...(await this.updateProductsOperations(ctx, [productId])));
-                    finishedProductsCount++;
-                    observer.next({
-                        total: productIds.length,
-                        completed: Math.min(finishedProductsCount, productIds.length),
-                        duration: +new Date() - timeStart,
-                    });
-                }
-                Logger.verbose(`Will execute ${operations.length} bulk update operations`, loggerCtx);
-                await this.executeBulkOperations(operations);
                 Logger.verbose(`Completed reindexing!`, loggerCtx);
+
                 return {
-                    total: productIds.length,
-                    completed: productIds.length,
+                    total: totalProductIds,
+                    completed: totalProductIds,
                     duration: +new Date() - timeStart,
                 };
             });
         });
     }
 
+    async executeBulkOperationsByChunks(
+        chunkSize: number,
+        operations: BulkVariantOperation[],
+        index = VARIANT_INDEX_NAME,
+    ): Promise<void> {
+        let i;
+        let j;
+        let processedOperation = 0;
+        for (i = 0, j = operations.length; i < j; i += chunkSize) {
+            const operationsChunks = operations.slice(i, i + chunkSize);
+            await this.executeBulkOperations(operationsChunks, index);
+            processedOperation += operationsChunks.length;
+
+            Logger.verbose(
+                `Executing operation chunks ${processedOperation}/${operations.length}`,
+                loggerCtx,
+            );
+        }
+    }
+
     async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
         const result = await this.updateAssetFocalPointForIndex(VARIANT_INDEX_NAME, data.asset);
         await this.client.indices.refresh({
@@ -461,101 +497,68 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         await this.executeBulkOperations(operations);
     }
 
-    private async updateProductsOperations(
+    private async updateProductsOperationsOnly(
         ctx: MutableRequestContext,
-        productIds: ID[],
+        productId: ID,
     ): Promise<BulkVariantOperation[]> {
-        Logger.debug(`Updating ${productIds.length} Products`, loggerCtx);
         const operations: BulkVariantOperation[] = [];
-
-        for (const productId of productIds) {
-            operations.push(...(await this.deleteProductOperations(ctx, productId)));
-
-            let product: Product | undefined;
-            try {
-                product = await this.connection.getRepository(Product).findOne(productId, {
-                    relations: this.productRelations,
+        let product: Product | undefined;
+        try {
+            product = await this.connection.getRepository(Product).findOne(productId, {
+                relations: this.productRelations,
+                where: {
+                    deletedAt: null,
+                },
+            });
+        } catch (e) {
+            Logger.error(e.message, loggerCtx, e.stack);
+            throw e;
+        }
+        if (product) {
+            const updatedProductVariants = await this.connection.getRepository(ProductVariant).findByIds(
+                product.variants.map(v => v.id),
+                {
+                    relations: this.variantRelations,
                     where: {
                         deletedAt: null,
                     },
-                });
-            } catch (e) {
-                Logger.error(e.message, loggerCtx, e.stack);
-                throw e;
-            }
-            if (product) {
-                const updatedProductVariants = await this.connection.getRepository(ProductVariant).findByIds(
-                    product.variants.map(v => v.id),
-                    {
-                        relations: this.variantRelations,
-                        where: {
-                            deletedAt: null,
-                        },
-                        order: {
-                            id: 'ASC',
-                        },
+                    order: {
+                        id: 'ASC',
                     },
-                );
-                // tslint:disable-next-line:no-non-null-assertion
-                updatedProductVariants.forEach(variant => (variant.product = product!));
-                if (!product.enabled) {
-                    updatedProductVariants.forEach(v => (v.enabled = false));
-                }
-                Logger.debug(`Updating Product (${productId})`, loggerCtx);
-                const languageVariants: LanguageCode[] = [];
-                languageVariants.push(...product.translations.map(t => t.languageCode));
-                for (const variant of product.variants) {
-                    languageVariants.push(...variant.translations.map(t => t.languageCode));
-                }
-                const uniqueLanguageVariants = unique(languageVariants);
+                },
+            );
+            // tslint:disable-next-line:no-non-null-assertion
+            updatedProductVariants.forEach(variant => (variant.product = product!));
+            if (!product.enabled) {
+                updatedProductVariants.forEach(v => (v.enabled = false));
+            }
+            Logger.debug(`Updating Product (${productId})`, loggerCtx);
+            const languageVariants: LanguageCode[] = [];
+            languageVariants.push(...product.translations.map(t => t.languageCode));
+            for (const variant of product.variants) {
+                languageVariants.push(...variant.translations.map(t => t.languageCode));
+            }
+            const uniqueLanguageVariants = unique(languageVariants);
 
-                for (const channel of product.channels) {
-                    ctx.setChannel(channel);
+            for (const channel of product.channels) {
+                ctx.setChannel(channel);
 
-                    const variantsInChannel = updatedProductVariants.filter(v =>
-                        v.channels.map(c => c.id).includes(ctx.channelId),
-                    );
-                    for (const variant of variantsInChannel) {
-                        await this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
-                    }
-                    for (const languageCode of uniqueLanguageVariants) {
-                        if (variantsInChannel.length) {
-                            for (const variant of variantsInChannel) {
-                                operations.push(
-                                    {
-                                        index: VARIANT_INDEX_NAME,
-                                        operation: {
-                                            update: {
-                                                _id: ElasticsearchIndexerController.getId(
-                                                    variant.id,
-                                                    ctx.channelId,
-                                                    languageCode,
-                                                ),
-                                            },
-                                        },
-                                    },
-                                    {
-                                        index: VARIANT_INDEX_NAME,
-                                        operation: {
-                                            doc: await this.createVariantIndexItem(
-                                                variant,
-                                                variantsInChannel,
-                                                ctx,
-                                                languageCode,
-                                            ),
-                                            doc_as_upsert: true,
-                                        },
-                                    },
-                                );
-                            }
-                        } else {
+                const variantsInChannel = updatedProductVariants.filter(v =>
+                    v.channels.map(c => c.id).includes(ctx.channelId),
+                );
+                for (const variant of variantsInChannel) {
+                    await this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
+                }
+                for (const languageCode of uniqueLanguageVariants) {
+                    if (variantsInChannel.length) {
+                        for (const variant of variantsInChannel) {
                             operations.push(
                                 {
                                     index: VARIANT_INDEX_NAME,
                                     operation: {
                                         update: {
                                             _id: ElasticsearchIndexerController.getId(
-                                                -product.id,
+                                                variant.id,
                                                 ctx.channelId,
                                                 languageCode,
                                             ),
@@ -565,16 +568,58 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                                 {
                                     index: VARIANT_INDEX_NAME,
                                     operation: {
-                                        doc: this.createSyntheticProductIndexItem(product, ctx, languageCode),
+                                        doc: await this.createVariantIndexItem(
+                                            variant,
+                                            variantsInChannel,
+                                            ctx,
+                                            languageCode,
+                                        ),
                                         doc_as_upsert: true,
                                     },
                                 },
                             );
                         }
+                    } else {
+                        operations.push(
+                            {
+                                index: VARIANT_INDEX_NAME,
+                                operation: {
+                                    update: {
+                                        _id: ElasticsearchIndexerController.getId(
+                                            -product.id,
+                                            ctx.channelId,
+                                            languageCode,
+                                        ),
+                                    },
+                                },
+                            },
+                            {
+                                index: VARIANT_INDEX_NAME,
+                                operation: {
+                                    doc: this.createSyntheticProductIndexItem(product, ctx, languageCode),
+                                    doc_as_upsert: true,
+                                },
+                            },
+                        );
                     }
                 }
             }
         }
+
+        return operations;
+    }
+
+    private async updateProductsOperations(
+        ctx: MutableRequestContext,
+        productIds: ID[],
+    ): Promise<BulkVariantOperation[]> {
+        Logger.debug(`Updating ${productIds.length} Products`, loggerCtx);
+        const operations: BulkVariantOperation[] = [];
+
+        for (const productId of productIds) {
+            operations.push(...(await this.deleteProductOperations(ctx, productId)));
+            operations.push(...(await this.updateProductsOperationsOnly(ctx, productId)));
+        }
         return operations;
     }
 
@@ -692,14 +737,14 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
         return unique(variants.map(v => v.product.id));
     }
 
-    private async executeBulkOperations(operations: BulkVariantOperation[]) {
+    private async executeBulkOperations(operations: BulkVariantOperation[], indexName = VARIANT_INDEX_NAME) {
         const variantOperations: Array<BulkOperation | BulkOperationDoc<VariantIndexItem>> = [];
 
         for (const operation of operations) {
             variantOperations.push(operation.operation);
         }
 
-        return Promise.all([this.runBulkOperationsOnIndex(VARIANT_INDEX_NAME, variantOperations)]);
+        return Promise.all([this.runBulkOperationsOnIndex(indexName, variantOperations)]);
     }
 
     private async runBulkOperationsOnIndex(

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "1.4.5",
+  "version": "1.4.6",
   "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.5",
-    "@vendure/core": "^1.4.5",
+    "@vendure/common": "^1.4.6",
+    "@vendure/core": "^1.4.6",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
   }

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/job-queue-plugin",
-  "version": "1.4.5",
+  "version": "1.4.6",
   "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.5",
-    "@vendure/core": "^1.4.5",
+    "@vendure/common": "^1.4.6",
+    "@vendure/core": "^1.4.6",
     "bullmq": "^1.40.1",
     "redis": "^3.0.2",
     "rimraf": "^3.0.2",

File diff suppressed because it is too large
+ 489 - 475
packages/payments-plugin/e2e/graphql/generated-admin-types.ts


+ 2 - 2
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -2505,7 +2505,7 @@ export type Query = {
     activeCustomer?: Maybe<Customer>;
     /**
      * The active Order. Will be `null` until an Order is created via `addItemToOrder`. Once an Order reaches the
-     * state of `PaymentApproved` or `PaymentSettled`, then that Order is no longer considered "active" and this
+     * state of `PaymentAuthorized` or `PaymentSettled`, then that Order is no longer considered "active" and this
      * query will once again return `null`.
      */
     activeOrder?: Maybe<Order>;
@@ -2513,7 +2513,7 @@ export type Query = {
     availableCountries: Array<Country>;
     /** A list of Collections available to the shop */
     collections: CollectionList;
-    /** Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is speicified, an error will result. */
+    /** Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is specified, an error will result. */
     collection?: Maybe<Collection>;
     /** Returns a list of eligible shipping methods based on the current active Order */
     eligibleShippingMethods: Array<ShippingMethodQuote>;

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "1.4.5",
+    "version": "1.4.6",
     "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.5",
-        "@vendure/core": "^1.4.5",
-        "@vendure/testing": "^1.4.5",
+        "@vendure/common": "^1.4.6",
+        "@vendure/core": "^1.4.6",
+        "@vendure/testing": "^1.4.6",
         "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.5",
+  "version": "1.4.6",
   "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.5",
+    "@vendure/common": "^1.4.6",
     "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.5",
+    "@vendure/core": "^1.4.6",
     "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.5",
+  "version": "1.4.6",
   "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.5",
-    "@vendure/common": "^1.4.5",
+    "@vendure/admin-ui": "^1.4.6",
+    "@vendure/common": "^1.4.6",
     "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.5",
+    "@vendure/core": "^1.4.6",
     "rimraf": "^3.0.2",
     "rollup": "^2.40.0",
     "rollup-plugin-terser": "^7.0.2",

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff