Преглед изворни кода

feat(elasticsearch-plugin): Add support for multiple channels

Michael Bromley пре 6 година
родитељ
комит
aacfaf4b59
21 измењених фајлова са 4805 додато и 170 уклоњено
  1. 1 0
      packages/elasticsearch-plugin/.gitignore
  2. 15 0
      packages/elasticsearch-plugin/e2e/config/jest-e2e.json
  3. 28 0
      packages/elasticsearch-plugin/e2e/config/test-config.ts
  4. 13 0
      packages/elasticsearch-plugin/e2e/config/tsconfig.e2e.json
  5. 754 0
      packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts
  6. BIN
      packages/elasticsearch-plugin/e2e/fixtures/assets/alexandru-acea-686569-unsplash.jpg
  7. BIN
      packages/elasticsearch-plugin/e2e/fixtures/assets/derick-david-409858-unsplash.jpg
  8. BIN
      packages/elasticsearch-plugin/e2e/fixtures/assets/florian-olivo-1166419-unsplash.jpg
  9. BIN
      packages/elasticsearch-plugin/e2e/fixtures/assets/vincent-botta-736919-unsplash.jpg
  10. 30 0
      packages/elasticsearch-plugin/e2e/fixtures/e2e-initial-data.ts
  11. 35 0
      packages/elasticsearch-plugin/e2e/fixtures/e2e-products-full.csv
  12. 3495 0
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  13. 39 17
      packages/elasticsearch-plugin/src/build-elastic-body.spec.ts
  14. 8 2
      packages/elasticsearch-plugin/src/build-elastic-body.ts
  15. 81 22
      packages/elasticsearch-plugin/src/elasticsearch-index.service.ts
  16. 25 4
      packages/elasticsearch-plugin/src/elasticsearch.service.ts
  17. 3 1
      packages/elasticsearch-plugin/src/graphql-schema-extensions.ts
  18. 210 114
      packages/elasticsearch-plugin/src/indexer.controller.ts
  19. 28 5
      packages/elasticsearch-plugin/src/plugin.ts
  20. 33 5
      packages/elasticsearch-plugin/src/types.ts
  21. 7 0
      scripts/codegen/generate-graphql-types.ts

+ 1 - 0
packages/elasticsearch-plugin/.gitignore

@@ -1,3 +1,4 @@
 preview/output
 yarn-error.log
 lib
+e2e/__data__

+ 15 - 0
packages/elasticsearch-plugin/e2e/config/jest-e2e.json

@@ -0,0 +1,15 @@
+{
+  "moduleFileExtensions": ["js", "json", "ts"],
+  "rootDir": "../",
+  "testRegex": ".e2e-spec.ts$",
+  "transform": {
+    "^.+\\.(t|j)s$": "ts-jest"
+  },
+  "testEnvironment": "node",
+  "globals": {
+    "ts-jest": {
+      "tsConfig": "<rootDir>/config/tsconfig.e2e.json",
+      "diagnostics": false
+    }
+  }
+}

+ 28 - 0
packages/elasticsearch-plugin/e2e/config/test-config.ts

@@ -0,0 +1,28 @@
+import { mergeConfig } from '@vendure/core';
+import { testConfig as defaultTestConfig } from '@vendure/testing';
+import path from 'path';
+
+/**
+ * We use a relatively long timeout on the initial beforeAll() function of the
+ * e2e tests because on the first run (and always in CI) the sqlite databases
+ * need to be generated, which can take a while.
+ */
+export const TEST_SETUP_TIMEOUT_MS = process.env.E2E_DEBUG ? 1800 * 1000 : 120000;
+
+/**
+ * For local debugging of the e2e tests, we set a very long timeout value otherwise tests will
+ * automatically fail for going over the 5 second default timeout.
+ */
+if (process.env.E2E_DEBUG) {
+    // tslint:disable-next-line:no-console
+    console.log('E2E_DEBUG', process.env.E2E_DEBUG, ' - setting long timeout');
+    jest.setTimeout(1800 * 1000);
+}
+
+export const dataDir = path.join(__dirname, '../__data__');
+
+export const testConfig = mergeConfig(defaultTestConfig, {
+    importExportOptions: {
+        importAssetsDir: path.join(__dirname, '..', 'fixtures/assets'),
+    },
+});

+ 13 - 0
packages/elasticsearch-plugin/e2e/config/tsconfig.e2e.json

@@ -0,0 +1,13 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "types": ["jest", "node"],
+    "lib": ["es2015"],
+    "skipLibCheck": false,
+    "inlineSourceMap": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "allowJs": true
+  },
+  "include": ["../**/*.e2e-spec.ts", "../**/*.d.ts"]
+}

+ 754 - 0
packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts

@@ -0,0 +1,754 @@
+import { SortOrder } from '@vendure/common/lib/generated-types';
+import { pick } from '@vendure/common/lib/pick';
+import { mergeConfig } from '@vendure/core';
+import { facetValueCollectionFilter } from '@vendure/core/dist/config/collection/default-collection-filters';
+import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN, SimpleGraphQLClient } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
+
+import {
+    AssignProductsToChannel,
+    CreateChannel,
+    CreateCollection,
+    CreateFacet,
+    CurrencyCode,
+    DeleteProduct,
+    DeleteProductVariant,
+    LanguageCode,
+    RemoveProductsFromChannel,
+    SearchFacetValues,
+    SearchGetPrices,
+    SearchInput,
+    UpdateCollection,
+    UpdateProduct,
+    UpdateProductVariants,
+    UpdateTaxRate,
+} from '../../core/e2e/graphql/generated-e2e-admin-types';
+import { SearchProductsShop } from '../../core/e2e/graphql/generated-e2e-shop-types';
+import {
+    ASSIGN_PRODUCT_TO_CHANNEL,
+    CREATE_CHANNEL,
+    CREATE_COLLECTION,
+    CREATE_FACET,
+    DELETE_PRODUCT,
+    DELETE_PRODUCT_VARIANT,
+    REMOVE_PRODUCT_FROM_CHANNEL,
+    UPDATE_COLLECTION,
+    UPDATE_PRODUCT,
+    UPDATE_PRODUCT_VARIANTS,
+    UPDATE_TAX_RATE,
+} from '../../core/e2e/graphql/shared-definitions';
+import { ElasticsearchPlugin } from '../src/plugin';
+
+import { SEARCH_PRODUCTS_SHOP } from './../../core/e2e/graphql/shop-definitions';
+import { awaitRunningJobs } from './../../core/e2e/utils/await-running-jobs';
+import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
+import { initialData } from './fixtures/e2e-initial-data';
+
+describe('Elasticsearch plugin', () => {
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig, {
+            plugins: [
+                ElasticsearchPlugin.init({
+                    indexPrefix: 'e2e-tests',
+                    port: 9200,
+                    host: 'http://192.168.99.100',
+                }),
+            ],
+        }),
+    );
+
+    beforeAll(async () => {
+        await server.init({
+            dataDir,
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+        await adminClient.query(gql`
+            mutation {
+                reindex {
+                    id
+                }
+            }
+        `);
+        await awaitRunningJobs(adminClient);
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    function doAdminSearchQuery(input: SearchInput) {
+        return adminClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(SEARCH_PRODUCTS, {
+            input,
+        });
+    }
+
+    async function testGroupByProduct(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    groupByProduct: true,
+                },
+            },
+        );
+        expect(result.search.totalItems).toBe(20);
+    }
+
+    async function testNoGrouping(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    groupByProduct: false,
+                },
+            },
+        );
+        expect(result.search.totalItems).toBe(34);
+    }
+
+    async function testMatchSearchTerm(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    term: 'camera',
+                    groupByProduct: true,
+                },
+            },
+        );
+        expect(result.search.items.map(i => i.productName)).toEqual([
+            'Instant Camera',
+            'Camera Lens',
+            'SLR Camera',
+        ]);
+    }
+
+    async function testMatchFacetValueIds(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    facetValueIds: ['T_1', 'T_2'],
+                    groupByProduct: true,
+                    sort: {
+                        name: SortOrder.ASC,
+                    },
+                },
+            },
+        );
+        expect(result.search.items.map(i => i.productName)).toEqual([
+            'Clacky Keyboard',
+            'Curvy Monitor',
+            'Gaming PC',
+            'Hard Drive',
+            'Laptop',
+            'USB Cable',
+        ]);
+    }
+
+    async function testMatchCollectionId(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+            SEARCH_PRODUCTS_SHOP,
+            {
+                input: {
+                    collectionId: 'T_2',
+                    groupByProduct: true,
+                },
+            },
+        );
+        expect(result.search.items.map(i => i.productName)).toEqual([
+            'Spiky Cactus',
+            'Orchid',
+            'Bonsai Tree',
+        ]);
+    }
+
+    async function testSinglePrices(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
+            SEARCH_GET_PRICES,
+            {
+                input: {
+                    groupByProduct: false,
+                    take: 3,
+                    sort: {
+                        price: SortOrder.ASC,
+                    },
+                },
+            },
+        );
+        expect(result.search.items).toEqual([
+            {
+                price: { value: 799 },
+                priceWithTax: { value: 959 },
+            },
+            {
+                price: { value: 1498 },
+                priceWithTax: { value: 1798 },
+            },
+            {
+                price: { value: 1550 },
+                priceWithTax: { value: 1860 },
+            },
+        ]);
+    }
+
+    async function testPriceRanges(client: SimpleGraphQLClient) {
+        const result = await client.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
+            SEARCH_GET_PRICES,
+            {
+                input: {
+                    groupByProduct: true,
+                    take: 3,
+                    term: 'laptop',
+                },
+            },
+        );
+        expect(result.search.items).toEqual([
+            {
+                price: { min: 129900, max: 229900 },
+                priceWithTax: { min: 155880, max: 275880 },
+            },
+        ]);
+    }
+
+    describe('shop api', () => {
+        it('group by product', () => testGroupByProduct(shopClient));
+
+        it('no grouping', () => testNoGrouping(shopClient));
+
+        it('matches search term', () => testMatchSearchTerm(shopClient));
+
+        it('matches by facetValueId', () => testMatchFacetValueIds(shopClient));
+
+        it('matches by collectionId', () => testMatchCollectionId(shopClient));
+
+        it('single prices', () => testSinglePrices(shopClient));
+
+        it('price ranges', () => testPriceRanges(shopClient));
+
+        it('returns correct facetValues when not grouped by product', async () => {
+            const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
+                SEARCH_GET_FACET_VALUES,
+                {
+                    input: {
+                        groupByProduct: false,
+                    },
+                },
+            );
+            expect(result.search.facetValues).toEqual([
+                { count: 21, facetValue: { id: 'T_1', name: 'electronics' } },
+                { count: 17, facetValue: { id: 'T_2', name: 'computers' } },
+                { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
+                { count: 10, facetValue: { id: 'T_4', name: 'sports equipment' } },
+                { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
+                { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
+            ]);
+        });
+
+        it('returns correct facetValues when grouped by product', async () => {
+            const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
+                SEARCH_GET_FACET_VALUES,
+                {
+                    input: {
+                        groupByProduct: true,
+                    },
+                },
+            );
+            expect(result.search.facetValues).toEqual([
+                { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
+                { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
+                { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
+                { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
+                { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
+                { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
+            ]);
+        });
+
+        it('omits facetValues of private facets', async () => {
+            const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
+                CREATE_FACET,
+                {
+                    input: {
+                        code: 'profit-margin',
+                        isPrivate: true,
+                        translations: [{ languageCode: LanguageCode.en, name: 'Profit Margin' }],
+                        values: [
+                            {
+                                code: 'massive',
+                                translations: [{ languageCode: LanguageCode.en, name: 'massive' }],
+                            },
+                        ],
+                    },
+                },
+            );
+            await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                input: {
+                    id: 'T_2',
+                    // T_1 & T_2 are the existing facetValues (electronics & photo)
+                    facetValueIds: ['T_1', 'T_2', createFacet.values[0].id],
+                },
+            });
+
+            const result = await shopClient.query<SearchFacetValues.Query, SearchFacetValues.Variables>(
+                SEARCH_GET_FACET_VALUES,
+                {
+                    input: {
+                        groupByProduct: true,
+                    },
+                },
+            );
+            expect(result.search.facetValues).toEqual([
+                { count: 10, facetValue: { id: 'T_1', name: 'electronics' } },
+                { count: 6, facetValue: { id: 'T_2', name: 'computers' } },
+                { count: 4, facetValue: { id: 'T_3', name: 'photo' } },
+                { count: 7, facetValue: { id: 'T_4', name: 'sports equipment' } },
+                { count: 3, facetValue: { id: 'T_5', name: 'home & garden' } },
+                { count: 3, facetValue: { id: 'T_6', name: 'plants' } },
+            ]);
+        });
+
+        it('encodes the productId and productVariantId', async () => {
+            const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+                SEARCH_PRODUCTS_SHOP,
+                {
+                    input: {
+                        groupByProduct: false,
+                        take: 1,
+                    },
+                },
+            );
+            expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual({
+                productId: 'T_1',
+                productVariantId: 'T_1',
+            });
+        });
+
+        it('omits results for disabled ProductVariants', async () => {
+            await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                UPDATE_PRODUCT_VARIANTS,
+                {
+                    input: [{ id: 'T_3', enabled: false }],
+                },
+            );
+            await awaitRunningJobs(adminClient);
+            const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+                SEARCH_PRODUCTS_SHOP,
+                {
+                    input: {
+                        groupByProduct: false,
+                        take: 3,
+                    },
+                },
+            );
+            expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']);
+        });
+
+        it('encodes collectionIds', async () => {
+            const result = await shopClient.query<SearchProductsShop.Query, SearchProductsShop.Variables>(
+                SEARCH_PRODUCTS_SHOP,
+                {
+                    input: {
+                        groupByProduct: false,
+                        term: 'cactus',
+                        take: 1,
+                    },
+                },
+            );
+
+            expect(result.search.items[0].collectionIds).toEqual(['T_2']);
+        });
+    });
+
+    describe('admin api', () => {
+        it('group by product', () => testGroupByProduct(adminClient));
+
+        it('no grouping', () => testNoGrouping(adminClient));
+
+        it('matches search term', () => testMatchSearchTerm(adminClient));
+
+        it('matches by facetValueId', () => testMatchFacetValueIds(adminClient));
+
+        it('matches by collectionId', () => testMatchCollectionId(adminClient));
+
+        it('single prices', () => testSinglePrices(adminClient));
+
+        it('price ranges', () => testPriceRanges(adminClient));
+
+        describe('updating the index', () => {
+            it('updates index when ProductVariants are changed', async () => {
+                await awaitRunningJobs(adminClient);
+                const { search } = await doAdminSearchQuery({ term: 'drive', groupByProduct: false });
+                expect(search.items.map(i => i.sku)).toEqual([
+                    'IHD455T1',
+                    'IHD455T2',
+                    'IHD455T3',
+                    'IHD455T4',
+                    'IHD455T6',
+                ]);
+
+                await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                    UPDATE_PRODUCT_VARIANTS,
+                    {
+                        input: search.items.map(i => ({
+                            id: i.productVariantId,
+                            sku: i.sku + '_updated',
+                        })),
+                    },
+                );
+
+                await awaitRunningJobs(adminClient);
+                const { search: search2 } = await doAdminSearchQuery({
+                    term: 'drive',
+                    groupByProduct: false,
+                });
+
+                expect(search2.items.map(i => i.sku)).toEqual([
+                    'IHD455T1_updated',
+                    'IHD455T2_updated',
+                    'IHD455T3_updated',
+                    'IHD455T4_updated',
+                    'IHD455T6_updated',
+                ]);
+            });
+
+            it('updates index when ProductVariants are deleted', async () => {
+                await awaitRunningJobs(adminClient);
+                const { search } = await doAdminSearchQuery({ term: 'drive', groupByProduct: false });
+
+                await adminClient.query<DeleteProductVariant.Mutation, DeleteProductVariant.Variables>(
+                    DELETE_PRODUCT_VARIANT,
+                    {
+                        id: search.items[0].productVariantId,
+                    },
+                );
+
+                await awaitRunningJobs(adminClient);
+                const { search: search2 } = await doAdminSearchQuery({
+                    term: 'drive',
+                    groupByProduct: false,
+                });
+
+                expect(search2.items.map(i => i.sku)).toEqual([
+                    'IHD455T2_updated',
+                    'IHD455T3_updated',
+                    'IHD455T4_updated',
+                    'IHD455T6_updated',
+                ]);
+            });
+
+            it('updates index when a Product is changed', async () => {
+                await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                    input: {
+                        id: 'T_1',
+                        facetValueIds: [],
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+                const result = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true });
+                expect(result.search.items.map(i => i.productName)).toEqual([
+                    'Gaming PC',
+                    'Clacky Keyboard',
+                    'USB Cable',
+                    'Curvy Monitor',
+                    'Hard Drive',
+                ]);
+            });
+
+            it('updates index when a Product is deleted', async () => {
+                const { search } = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true });
+                expect(search.items.map(i => i.productId)).toEqual(['T_3', 'T_5', 'T_6', 'T_2', 'T_4']);
+                await adminClient.query<DeleteProduct.Mutation, DeleteProduct.Variables>(DELETE_PRODUCT, {
+                    id: 'T_5',
+                });
+                await awaitRunningJobs(adminClient);
+                const { search: search2 } = await doAdminSearchQuery({
+                    facetValueIds: ['T_2'],
+                    groupByProduct: true,
+                });
+                expect(search2.items.map(i => i.productId)).toEqual(['T_3', 'T_6', 'T_2', 'T_4']);
+            });
+
+            it('updates index when a Collection is changed', async () => {
+                await adminClient.query<UpdateCollection.Mutation, UpdateCollection.Variables>(
+                    UPDATE_COLLECTION,
+                    {
+                        input: {
+                            id: 'T_2',
+                            filters: [
+                                {
+                                    code: facetValueCollectionFilter.code,
+                                    arguments: [
+                                        {
+                                            name: 'facetValueIds',
+                                            value: `["T_4"]`,
+                                            type: 'facetValueIds',
+                                        },
+                                        {
+                                            name: 'containsAny',
+                                            value: `false`,
+                                            type: 'boolean',
+                                        },
+                                    ],
+                                },
+                            ],
+                        },
+                    },
+                );
+                await awaitRunningJobs(adminClient);
+                const result = await doAdminSearchQuery({ collectionId: 'T_2', groupByProduct: true });
+
+                expect(result.search.items.map(i => i.productName)).toEqual([
+                    'Road Bike',
+                    'Skipping Rope',
+                    'Boxing Gloves',
+                    'Tent',
+                    'Cruiser Skateboard',
+                    'Football',
+                    'Running Shoe',
+                ]);
+            });
+
+            it('updates index when a Collection created', async () => {
+                const { createCollection } = await adminClient.query<
+                    CreateCollection.Mutation,
+                    CreateCollection.Variables
+                >(CREATE_COLLECTION, {
+                    input: {
+                        translations: [
+                            {
+                                languageCode: LanguageCode.en,
+                                name: 'Photo',
+                                description: '',
+                            },
+                        ],
+                        filters: [
+                            {
+                                code: facetValueCollectionFilter.code,
+                                arguments: [
+                                    {
+                                        name: 'facetValueIds',
+                                        value: `["T_3"]`,
+                                        type: 'facetValueIds',
+                                    },
+                                    {
+                                        name: 'containsAny',
+                                        value: `false`,
+                                        type: 'boolean',
+                                    },
+                                ],
+                            },
+                        ],
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+                const result = await doAdminSearchQuery({
+                    collectionId: createCollection.id,
+                    groupByProduct: true,
+                });
+                expect(result.search.items.map(i => i.productName)).toEqual([
+                    'Instant Camera',
+                    'Camera Lens',
+                    'Tripod',
+                    'SLR Camera',
+                ]);
+            });
+
+            it('updates index when a taxRate is changed', async () => {
+                await adminClient.query<UpdateTaxRate.Mutation, UpdateTaxRate.Variables>(UPDATE_TAX_RATE, {
+                    input: {
+                        // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate
+                        // to Europe is 2.
+                        id: 'T_2',
+                        value: 50,
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+                const result = await adminClient.query<SearchGetPrices.Query, SearchGetPrices.Variables>(
+                    SEARCH_GET_PRICES,
+                    {
+                        input: {
+                            groupByProduct: true,
+                            term: 'laptop',
+                        } as SearchInput,
+                    },
+                );
+                expect(result.search.items).toEqual([
+                    {
+                        price: { min: 129900, max: 229900 },
+                        priceWithTax: { min: 194850, max: 344850 },
+                    },
+                ]);
+            });
+
+            it('returns disabled field when not grouped', async () => {
+                const result = await doAdminSearchQuery({ groupByProduct: false, term: 'laptop' });
+                expect(result.search.items.map(pick(['productVariantId', 'enabled']))).toEqual([
+                    { productVariantId: 'T_1', enabled: true },
+                    { productVariantId: 'T_2', enabled: true },
+                    { productVariantId: 'T_3', enabled: false },
+                    { productVariantId: 'T_4', enabled: true },
+                ]);
+            });
+
+            it('when grouped, disabled is false if at least one variant is enabled', async () => {
+                await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                    UPDATE_PRODUCT_VARIANTS,
+                    {
+                        input: [{ id: 'T_1', enabled: false }, { id: 'T_2', enabled: false }],
+                    },
+                );
+                await awaitRunningJobs(adminClient);
+                const result = await doAdminSearchQuery({ groupByProduct: true, term: 'laptop' });
+                expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
+                    { productId: 'T_1', enabled: true },
+                ]);
+            });
+
+            it('when grouped, disabled is true if all variants are disabled', async () => {
+                await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
+                    UPDATE_PRODUCT_VARIANTS,
+                    {
+                        input: [{ id: 'T_4', enabled: false }],
+                    },
+                );
+                await awaitRunningJobs(adminClient);
+                const result = await doAdminSearchQuery({ groupByProduct: true, take: 3, term: 'laptop' });
+                expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
+                    { productId: 'T_1', enabled: false },
+                ]);
+            });
+
+            it('when grouped, disabled is true product is disabled', async () => {
+                await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                    input: {
+                        id: 'T_3',
+                        enabled: false,
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+                const result = await doAdminSearchQuery({ groupByProduct: true, term: 'gaming' });
+                expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([
+                    { productId: 'T_3', enabled: false },
+                ]);
+            });
+        });
+
+        describe('channel handling', () => {
+            const SECOND_CHANNEL_TOKEN = 'second-channel-token';
+            let secondChannel: CreateChannel.CreateChannel;
+
+            beforeAll(async () => {
+                const { createChannel } = await adminClient.query<
+                    CreateChannel.Mutation,
+                    CreateChannel.Variables
+                >(CREATE_CHANNEL, {
+                    input: {
+                        code: 'second-channel',
+                        token: SECOND_CHANNEL_TOKEN,
+                        defaultLanguageCode: LanguageCode.en,
+                        currencyCode: CurrencyCode.GBP,
+                        pricesIncludeTax: true,
+                    },
+                });
+                secondChannel = createChannel;
+            });
+
+            it('adding product to channel', async () => {
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
+                    ASSIGN_PRODUCT_TO_CHANNEL,
+                    {
+                        input: { channelId: secondChannel.id, productIds: ['T_1', 'T_2'] },
+                    },
+                );
+                await awaitRunningJobs(adminClient);
+
+                adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+                const { search } = await doAdminSearchQuery({ groupByProduct: true });
+                expect(search.items.map(i => i.productId).sort()).toEqual(['T_1', 'T_2']);
+            }, 10000);
+
+            it('removing product from channel', async () => {
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                const { removeProductsFromChannel } = await adminClient.query<
+                    RemoveProductsFromChannel.Mutation,
+                    RemoveProductsFromChannel.Variables
+                >(REMOVE_PRODUCT_FROM_CHANNEL, {
+                    input: {
+                        productIds: ['T_2'],
+                        channelId: secondChannel.id,
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+
+                adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+                const { search } = await doAdminSearchQuery({ groupByProduct: true });
+                expect(search.items.map(i => i.productId)).toEqual(['T_1']);
+            }, 10000);
+        });
+    });
+});
+
+export const SEARCH_PRODUCTS = gql`
+    query SearchProductsAdmin($input: SearchInput!) {
+        search(input: $input) {
+            totalItems
+            items {
+                enabled
+                productId
+                productName
+                productPreview
+                productVariantId
+                productVariantName
+                productVariantPreview
+                sku
+            }
+        }
+    }
+`;
+
+export const SEARCH_GET_FACET_VALUES = gql`
+    query SearchFacetValues($input: SearchInput!) {
+        search(input: $input) {
+            totalItems
+            facetValues {
+                count
+                facetValue {
+                    id
+                    name
+                }
+            }
+        }
+    }
+`;
+
+export const SEARCH_GET_PRICES = gql`
+    query SearchGetPrices($input: SearchInput!) {
+        search(input: $input) {
+            items {
+                price {
+                    ... on PriceRange {
+                        min
+                        max
+                    }
+                    ... on SinglePrice {
+                        value
+                    }
+                }
+                priceWithTax {
+                    ... on PriceRange {
+                        min
+                        max
+                    }
+                    ... on SinglePrice {
+                        value
+                    }
+                }
+            }
+        }
+    }
+`;

BIN
packages/elasticsearch-plugin/e2e/fixtures/assets/alexandru-acea-686569-unsplash.jpg


BIN
packages/elasticsearch-plugin/e2e/fixtures/assets/derick-david-409858-unsplash.jpg


BIN
packages/elasticsearch-plugin/e2e/fixtures/assets/florian-olivo-1166419-unsplash.jpg


BIN
packages/elasticsearch-plugin/e2e/fixtures/assets/vincent-botta-736919-unsplash.jpg


+ 30 - 0
packages/elasticsearch-plugin/e2e/fixtures/e2e-initial-data.ts

@@ -0,0 +1,30 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { InitialData } from '@vendure/core';
+
+export const initialData: InitialData = {
+    defaultLanguage: LanguageCode.en,
+    defaultZone: 'Europe',
+    taxRates: [
+        { name: 'Standard Tax', percentage: 20 },
+        { name: 'Reduced Tax', percentage: 10 },
+        { name: 'Zero Tax', percentage: 0 },
+    ],
+    shippingMethods: [{ name: 'Standard Shipping', price: 500 }, { name: 'Express Shipping', price: 1000 }],
+    countries: [
+        { name: 'Australia', code: 'AU', zone: 'Oceania' },
+        { name: 'Austria', code: 'AT', zone: 'Europe' },
+        { name: 'Canada', code: 'CA', zone: 'Americas' },
+        { name: 'China', code: 'CN', zone: 'Asia' },
+        { name: 'South Africa', code: 'ZA', zone: 'Africa' },
+        { name: 'United Kingdom', code: 'GB', zone: 'Europe' },
+        { name: 'United States of America', code: 'US', zone: 'Americas' },
+    ],
+    collections: [
+        {
+            name: 'Plants',
+            filters: [
+                { code: 'facet-value-filter', args: { facetValueNames: ['plants'], containsAny: false } },
+            ],
+        },
+    ],
+};

+ 35 - 0
packages/elasticsearch-plugin/e2e/fixtures/e2e-products-full.csv

@@ -0,0 +1,35 @@
+name               , slug               , description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   , assets                             , facets                                  , optionGroups      , optionValues        , sku          , price   , taxCategory , stockOnHand , trackInventory , variantAssets , variantFacets
+Laptop             , laptop             , "Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz."                                                                                                                                                                                                                  , derick-david-409858-unsplash.jpg   , category:electronics|category:computers , "screen size|RAM" , "13 inch|8GB"       , L2201308     , 1299.00 , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , "15 inch|8GB"       , L2201508     , 1399.00 , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , "13 inch|16GB"      , L2201316     , 2199.00 , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , "15 inch|16GB"      , L2201516     , 2299.00 , standard    , 100         , false          ,               ,
+Curvy Monitor      , curvy-monitor      , "Discover a truly immersive viewing experience with this monitor curved more deeply than any other. Wrapping around your field of vision the 1,800 R screencreates a wider field of view, enhances depth perception, and minimises peripheral distractions to draw you deeper in to your content."                                                                                                                                                                                            , alexandru-acea-686569-unsplash.jpg , category:electronics|category:computers , monitor size      , 24 inch             , C24F390      , 143.74  , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , 27 inch             , C27F390      , 169.94  , standard    , 100         , false          ,               ,
+Gaming PC          , gaming-pc          , "This pc is optimised for gaming, and is also VR ready. The Intel Core-i7 CPU and High Performance GPU give the computer the raw power it needs to function at a high level."                                                                                                                                                                                                                                                                                                                 , florian-olivo-1166419-unsplash.jpg , category:electronics|category:computers , "cpu|HDD"         , "i7-8700|240GB SSD" , CGS480VR1063 , 1087.20 , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , "R7-2700|240GB SSD" , CGS480VR1064 , 1099.95 , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , "i7-8700|120GB SSD" , CGS480VR1065 , 931.20  , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , "R7-2700|120GB SSD" , CGS480VR1066 , 949.20  , standard    , 100         , false          ,               ,
+Hard Drive         , hard-drive         , "Boost your PC storage with this internal hard drive, designed just for desktop and all-in-one PCs."                                                                                                                                                                                                                                                                                                                                                                                          , vincent-botta-736919-unsplash.jpg  , category:electronics|category:computers , "HDD capacity"    , 1TB                 , IHD455T1     , 37.99   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , 2TB                 , IHD455T2     , 53.74   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , 3TB                 , IHD455T3     , 78.96   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , 4TB                 , IHD455T4     , 92.99   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , 6TB                 , IHD455T6     , 134.35  , standard    , 100         , false          ,               ,
+Clacky Keyboard    , clacky-keyboard    , "Let all your colleagues know that you are typing on this exclusive, colorful klicky-klacky keyboard. Huge travel on each keypress ensures maximum klack on each and every keystroke."                                                                                                                                                                                                                                                                                                        ,                                    , category:electronics|category:computers ,                   ,                     , A4TKLA45535  , 74.89   , standard    , 100         , false          ,               ,
+USB Cable          , usb-cable          , "Solid conductors eliminate strand-interaction distortion and reduce jitter. As the surface is made of high-purity silver, the performance is very close to that of a solid silver cable, but priced much closer to solid copper cable."                                                                                                                                                                                                                                                      ,                                    , category:electronics|category:computers ,                   ,                     , USBCIN01.5MI , 69.00   , standard    , 100         , false          ,               ,
+Instant Camera     , instant-camera     , "With its nostalgic design and simple point-and-shoot functionality, the Instant Camera is the perfect pick to get started with instant photography."                                                                                                                                                                                                                                                                                                                                         ,                                    , category:electronics|category:photo     ,                   ,                     , IC22MWDD     , 174.99  , standard    , 100         , false          ,               ,
+Camera Lens        , camera-lens        , "This lens is a Di type lens using an optical system with improved multi-coating designed to function with digital SLR cameras as well as film cameras."                                                                                                                                                                                                                                                                                                                                      ,                                    , category:electronics|category:photo     ,                   ,                     , B0012UUP02   , 104.00  , standard    , 100         , false          ,               ,
+Tripod             , tripod             , "Capture vivid, professional-style photographs with help from this lightweight tripod. The adjustable-height tripod makes it easy to achieve reliable stability and score just the right angle when going after that award-winning shot."                                                                                                                                                                                                                                                     ,                                    , category:electronics|category:photo     ,                   ,                     , B00XI87KV8   , 14.98   , standard    , 100         , false          ,               ,
+SLR Camera         , slr-camera         , "Retro styled, portable in size and built around a powerful 24-megapixel APS-C CMOS sensor, this digital camera is the ideal companion for creative everyday photography. Packed full of high spec features such as an advanced hybrid autofocus system able to keep pace with even the most active subjects, a speedy 6fps continuous-shooting mode, high-resolution electronic viewfinder and intuitive swivelling touchscreen, it brings professional image making into everyone’s grasp." ,                                    , category:electronics|category:photo     ,                   ,                     , B07D75V44S   , 521.00  , standard    , 100         , false          ,               ,
+Road Bike          , road-bike          , "Featuring a full carbon chassis - complete with cyclocross-specific carbon fork - and a component setup geared for hard use on the race circuit, it's got the low weight, exceptional efficiency and brilliant handling you'll need to stay at the front of the pack."                                                                                                                                                                                                                       ,                                    , category:sports equipment               ,                   ,                     , RB000844334  , 2499.00 , standard    , 100         , false          ,               ,
+Skipping Rope      , skipping-rope      , "When you're working out you need a quality rope that doesn't tangle at every couple of jumps and with this sipping rope you won't have this problem."                                                                                                                                                                                                                                                                                                                                        ,                                    , category:sports equipment               ,                   ,                     , B07CNGXVXT   , 7.99    , standard    , 100         , false          ,               ,
+Boxing Gloves      , boxing-gloves      , "Training gloves designed for optimum training. Our gloves promote proper punching technique because they are conformed to the natural shape of your fist. Dense, innovative two-layer foam provides better shock absorbency and full padding on the front, back and wrist to promote proper punching technique."                                                                                                                                                                             ,                                    , category:sports equipment               ,                   ,                     , B000ZYLPPU   , 33.04   , standard    , 100         , false          ,               ,
+Tent               , tent               , "With tons of space inside (for max. 4 persons), full head height throughout the entire tent and an unusual and striking shape, this tent offers you everything you need."                                                                                                                                                                                                                                                                                                                    ,                                    , category:sports equipment               ,                   ,                     , 2000023510   , 214.93  , standard    , 100         , false          ,               ,
+Cruiser Skateboard , cruiser-skateboard , "Based on the 1970s iconic shape, but made to a larger 69cm size, with updated, quality component, these skateboards are great for beginners to learn the foot spacing required, and are perfect for all-day cruising."                                                                                                                                                                                                                                                                       ,                                    , category:sports equipment               ,                   ,                     , 799872520    , 24.99   , standard    , 100         , false          ,               ,
+Football           , football           , "This football features high-contrast graphics for high-visibility during play, while its machine-stitched tpu casing offers consistent performance."                                                                                                                                                                                                                                                                                                                                         ,                                    , category:sports equipment               ,                   ,                     , SC3137-056   , 57.07   , standard    , 100         , false          ,               ,
+Running Shoe       , running-shoe       , "With its ultra-light, uber-responsive magic foam and a carbon fiber plate that feels like it’s propelling you forward, the Running Shoe is ready to push you to victories both large and small"                                                                                                                                                                                                                                                                                              ,                                    , category:sports equipment               , shoe size         , Size 40             , RS0040       , 99.99   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , Size 42             , RS0042       , 99.99   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , Size 44             , RS0044       , 99.99   , standard    , 100         , false          ,               ,
+                   ,                    ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ,                                    ,                                         ,                   , Size 46             , RS0046       , 99.99   , standard    , 100         , false          ,               ,
+Spiky Cactus       , spiky-cactus       , "A spiky yet elegant house cactus - perfect for the home or office. Origin and habitat: Probably native only to the Andes of Peru"                                                                                                                                                                                                                                                                                                                                                            ,                                    , category:home & garden|category:plants  ,                   ,                     , SC011001     , 15.50   , standard    , 100         , false          ,               ,
+Orchid             , orchid             , "Gloriously elegant. It can go along with any interior as it is a neutral color and the most popular Phalaenopsis overall. 2 to 3 foot stems host large white flowers that can last for over 2 months."                                                                                                                                                                                                                                                                                       ,                                    , category:home & garden|category:plants  ,                   ,                     , ROR00221     , 65.00   , standard    , 100         , false          ,               ,
+Bonsai Tree        , bonsai-tree        , "Excellent semi-evergreen bonsai. Indoors or out but needs some winter protection. All trees sent will leave the nursery in excellent condition and will be of equal quality or better than the photograph shown."                                                                                                                                                                                                                                                                            ,                                    , category:home & garden|category:plants  ,                   ,                     , B01MXFLUSV   , 19.99   , standard    , 100         , false          ,               ,

+ 3495 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -0,0 +1,3495 @@
+// tslint:disable
+export type Maybe<T> = T | null;
+/** All built-in and custom scalars, mapped to their actual values */
+export type Scalars = {
+    ID: string;
+    String: string;
+    Boolean: boolean;
+    Int: number;
+    Float: number;
+    /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the
+     * `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO
+     * 8601 standard for representation of dates and times using the Gregorian calendar.
+     */
+    DateTime: any;
+    /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
+    JSON: any;
+    /** The `Upload` scalar type represents a file upload. */
+    Upload: any;
+};
+
+export type AddNoteToOrderInput = {
+    id: Scalars['ID'];
+    note: Scalars['String'];
+    isPublic: Scalars['Boolean'];
+};
+
+export type Address = Node & {
+    __typename?: 'Address';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    fullName?: Maybe<Scalars['String']>;
+    company?: Maybe<Scalars['String']>;
+    streetLine1: Scalars['String'];
+    streetLine2?: Maybe<Scalars['String']>;
+    city?: Maybe<Scalars['String']>;
+    province?: Maybe<Scalars['String']>;
+    postalCode?: Maybe<Scalars['String']>;
+    country: Country;
+    phoneNumber?: Maybe<Scalars['String']>;
+    defaultShippingAddress?: Maybe<Scalars['Boolean']>;
+    defaultBillingAddress?: Maybe<Scalars['Boolean']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type Adjustment = {
+    __typename?: 'Adjustment';
+    adjustmentSource: Scalars['String'];
+    type: AdjustmentType;
+    description: Scalars['String'];
+    amount: Scalars['Int'];
+};
+
+export enum AdjustmentType {
+    TAX = 'TAX',
+    PROMOTION = 'PROMOTION',
+    SHIPPING = 'SHIPPING',
+    REFUND = 'REFUND',
+    TAX_REFUND = 'TAX_REFUND',
+    PROMOTION_REFUND = 'PROMOTION_REFUND',
+    SHIPPING_REFUND = 'SHIPPING_REFUND',
+}
+
+export type Administrator = Node & {
+    __typename?: 'Administrator';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    firstName: Scalars['String'];
+    lastName: Scalars['String'];
+    emailAddress: Scalars['String'];
+    user: User;
+};
+
+export type AdministratorFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    firstName?: Maybe<StringOperators>;
+    lastName?: Maybe<StringOperators>;
+    emailAddress?: Maybe<StringOperators>;
+};
+
+export type AdministratorList = PaginatedList & {
+    __typename?: 'AdministratorList';
+    items: Array<Administrator>;
+    totalItems: Scalars['Int'];
+};
+
+export type AdministratorListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<AdministratorSortParameter>;
+    filter?: Maybe<AdministratorFilterParameter>;
+};
+
+export type AdministratorSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    firstName?: Maybe<SortOrder>;
+    lastName?: Maybe<SortOrder>;
+    emailAddress?: Maybe<SortOrder>;
+};
+
+export type Asset = Node & {
+    __typename?: 'Asset';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    name: Scalars['String'];
+    type: AssetType;
+    fileSize: Scalars['Int'];
+    mimeType: Scalars['String'];
+    width: Scalars['Int'];
+    height: Scalars['Int'];
+    source: Scalars['String'];
+    preview: Scalars['String'];
+};
+
+export type AssetFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    name?: Maybe<StringOperators>;
+    type?: Maybe<StringOperators>;
+    fileSize?: Maybe<NumberOperators>;
+    mimeType?: Maybe<StringOperators>;
+    width?: Maybe<NumberOperators>;
+    height?: Maybe<NumberOperators>;
+    source?: Maybe<StringOperators>;
+    preview?: Maybe<StringOperators>;
+};
+
+export type AssetList = PaginatedList & {
+    __typename?: 'AssetList';
+    items: Array<Asset>;
+    totalItems: Scalars['Int'];
+};
+
+export type AssetListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<AssetSortParameter>;
+    filter?: Maybe<AssetFilterParameter>;
+};
+
+export type AssetSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+    fileSize?: Maybe<SortOrder>;
+    mimeType?: Maybe<SortOrder>;
+    width?: Maybe<SortOrder>;
+    height?: Maybe<SortOrder>;
+    source?: Maybe<SortOrder>;
+    preview?: Maybe<SortOrder>;
+};
+
+export enum AssetType {
+    IMAGE = 'IMAGE',
+    VIDEO = 'VIDEO',
+    BINARY = 'BINARY',
+}
+
+export type AssignProductsToChannelInput = {
+    productIds: Array<Scalars['ID']>;
+    channelId: Scalars['ID'];
+    priceFactor?: Maybe<Scalars['Float']>;
+};
+
+export type BooleanCustomFieldConfig = CustomField & {
+    __typename?: 'BooleanCustomFieldConfig';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+};
+
+export type BooleanOperators = {
+    eq?: Maybe<Scalars['Boolean']>;
+};
+
+export type Cancellation = Node &
+    StockMovement & {
+        __typename?: 'Cancellation';
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderLine: OrderLine;
+    };
+
+export type CancelOrderInput = {
+    /** The id of the order to be cancelled */
+    orderId: Scalars['ID'];
+    /** Optionally specify which OrderLines to cancel. If not provided, all OrderLines will be cancelled */
+    lines?: Maybe<Array<OrderLineInput>>;
+    reason?: Maybe<Scalars['String']>;
+};
+
+export type Channel = Node & {
+    __typename?: 'Channel';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    code: Scalars['String'];
+    token: Scalars['String'];
+    defaultTaxZone?: Maybe<Zone>;
+    defaultShippingZone?: Maybe<Zone>;
+    defaultLanguageCode: LanguageCode;
+    currencyCode: CurrencyCode;
+    pricesIncludeTax: Scalars['Boolean'];
+};
+
+export type Collection = Node & {
+    __typename?: 'Collection';
+    isPrivate: Scalars['Boolean'];
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode?: Maybe<LanguageCode>;
+    name: Scalars['String'];
+    breadcrumbs: Array<CollectionBreadcrumb>;
+    position: Scalars['Int'];
+    description: Scalars['String'];
+    featuredAsset?: Maybe<Asset>;
+    assets: Array<Asset>;
+    parent?: Maybe<Collection>;
+    children?: Maybe<Array<Collection>>;
+    filters: Array<ConfigurableOperation>;
+    translations: Array<CollectionTranslation>;
+    productVariants: ProductVariantList;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CollectionProductVariantsArgs = {
+    options?: Maybe<ProductVariantListOptions>;
+};
+
+export type CollectionBreadcrumb = {
+    __typename?: 'CollectionBreadcrumb';
+    id: Scalars['ID'];
+    name: Scalars['String'];
+};
+
+export type CollectionFilterParameter = {
+    isPrivate?: Maybe<BooleanOperators>;
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    languageCode?: Maybe<StringOperators>;
+    name?: Maybe<StringOperators>;
+    position?: Maybe<NumberOperators>;
+    description?: Maybe<StringOperators>;
+};
+
+export type CollectionList = PaginatedList & {
+    __typename?: 'CollectionList';
+    items: Array<Collection>;
+    totalItems: Scalars['Int'];
+};
+
+export type CollectionListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<CollectionSortParameter>;
+    filter?: Maybe<CollectionFilterParameter>;
+};
+
+export type CollectionSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+    position?: Maybe<SortOrder>;
+    description?: Maybe<SortOrder>;
+};
+
+export type CollectionTranslation = {
+    __typename?: 'CollectionTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    description: Scalars['String'];
+};
+
+export type CollectionTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    description?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ConfigArg = {
+    __typename?: 'ConfigArg';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    value: Scalars['String'];
+};
+
+export type ConfigArgDefinition = {
+    __typename?: 'ConfigArgDefinition';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Scalars['String']>;
+    description?: Maybe<Scalars['String']>;
+    config?: Maybe<Scalars['JSON']>;
+};
+
+export type ConfigArgInput = {
+    name: Scalars['String'];
+    type: Scalars['String'];
+    value: Scalars['String'];
+};
+
+export type ConfigurableOperation = {
+    __typename?: 'ConfigurableOperation';
+    code: Scalars['String'];
+    args: Array<ConfigArg>;
+};
+
+export type ConfigurableOperationDefinition = {
+    __typename?: 'ConfigurableOperationDefinition';
+    code: Scalars['String'];
+    args: Array<ConfigArgDefinition>;
+    description: Scalars['String'];
+};
+
+export type ConfigurableOperationInput = {
+    code: Scalars['String'];
+    arguments: Array<ConfigArgInput>;
+};
+
+export type Country = Node & {
+    __typename?: 'Country';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    code: Scalars['String'];
+    name: Scalars['String'];
+    enabled: Scalars['Boolean'];
+    translations: Array<CountryTranslation>;
+};
+
+export type CountryFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    languageCode?: Maybe<StringOperators>;
+    code?: Maybe<StringOperators>;
+    name?: Maybe<StringOperators>;
+    enabled?: Maybe<BooleanOperators>;
+};
+
+export type CountryList = PaginatedList & {
+    __typename?: 'CountryList';
+    items: Array<Country>;
+    totalItems: Scalars['Int'];
+};
+
+export type CountryListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<CountrySortParameter>;
+    filter?: Maybe<CountryFilterParameter>;
+};
+
+export type CountrySortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    code?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+};
+
+export type CountryTranslation = {
+    __typename?: 'CountryTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+};
+
+export type CountryTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+};
+
+export type CreateAddressInput = {
+    fullName?: Maybe<Scalars['String']>;
+    company?: Maybe<Scalars['String']>;
+    streetLine1: Scalars['String'];
+    streetLine2?: Maybe<Scalars['String']>;
+    city?: Maybe<Scalars['String']>;
+    province?: Maybe<Scalars['String']>;
+    postalCode?: Maybe<Scalars['String']>;
+    countryCode: Scalars['String'];
+    phoneNumber?: Maybe<Scalars['String']>;
+    defaultShippingAddress?: Maybe<Scalars['Boolean']>;
+    defaultBillingAddress?: Maybe<Scalars['Boolean']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateAdministratorInput = {
+    firstName: Scalars['String'];
+    lastName: Scalars['String'];
+    emailAddress: Scalars['String'];
+    password: Scalars['String'];
+    roleIds: Array<Scalars['ID']>;
+};
+
+export type CreateAssetInput = {
+    file: Scalars['Upload'];
+};
+
+export type CreateChannelInput = {
+    code: Scalars['String'];
+    token: Scalars['String'];
+    defaultLanguageCode: LanguageCode;
+    pricesIncludeTax: Scalars['Boolean'];
+    currencyCode: CurrencyCode;
+    defaultTaxZoneId?: Maybe<Scalars['ID']>;
+    defaultShippingZoneId?: Maybe<Scalars['ID']>;
+};
+
+export type CreateCollectionInput = {
+    isPrivate?: Maybe<Scalars['Boolean']>;
+    featuredAssetId?: Maybe<Scalars['ID']>;
+    assetIds?: Maybe<Array<Scalars['ID']>>;
+    parentId?: Maybe<Scalars['ID']>;
+    filters: Array<ConfigurableOperationInput>;
+    translations: Array<CollectionTranslationInput>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateCountryInput = {
+    code: Scalars['String'];
+    translations: Array<CountryTranslationInput>;
+    enabled: Scalars['Boolean'];
+};
+
+export type CreateCustomerGroupInput = {
+    name: Scalars['String'];
+    customerIds?: Maybe<Array<Scalars['ID']>>;
+};
+
+export type CreateCustomerInput = {
+    title?: Maybe<Scalars['String']>;
+    firstName: Scalars['String'];
+    lastName: Scalars['String'];
+    phoneNumber?: Maybe<Scalars['String']>;
+    emailAddress: Scalars['String'];
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateFacetInput = {
+    code: Scalars['String'];
+    isPrivate: Scalars['Boolean'];
+    translations: Array<FacetTranslationInput>;
+    values?: Maybe<Array<CreateFacetValueWithFacetInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateFacetValueInput = {
+    facetId: Scalars['ID'];
+    code: Scalars['String'];
+    translations: Array<FacetValueTranslationInput>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateFacetValueWithFacetInput = {
+    code: Scalars['String'];
+    translations: Array<FacetValueTranslationInput>;
+};
+
+export type CreateGroupOptionInput = {
+    code: Scalars['String'];
+    translations: Array<ProductOptionGroupTranslationInput>;
+};
+
+export type CreateProductInput = {
+    featuredAssetId?: Maybe<Scalars['ID']>;
+    assetIds?: Maybe<Array<Scalars['ID']>>;
+    facetValueIds?: Maybe<Array<Scalars['ID']>>;
+    translations: Array<ProductTranslationInput>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateProductOptionGroupInput = {
+    code: Scalars['String'];
+    translations: Array<ProductOptionGroupTranslationInput>;
+    options: Array<CreateGroupOptionInput>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateProductOptionInput = {
+    productOptionGroupId: Scalars['ID'];
+    code: Scalars['String'];
+    translations: Array<ProductOptionGroupTranslationInput>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateProductVariantInput = {
+    productId: Scalars['ID'];
+    translations: Array<ProductVariantTranslationInput>;
+    facetValueIds?: Maybe<Array<Scalars['ID']>>;
+    sku: Scalars['String'];
+    price?: Maybe<Scalars['Int']>;
+    taxCategoryId?: Maybe<Scalars['ID']>;
+    optionIds?: Maybe<Array<Scalars['ID']>>;
+    featuredAssetId?: Maybe<Scalars['ID']>;
+    assetIds?: Maybe<Array<Scalars['ID']>>;
+    stockOnHand?: Maybe<Scalars['Int']>;
+    trackInventory?: Maybe<Scalars['Boolean']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CreateProductVariantOptionInput = {
+    optionGroupId: Scalars['ID'];
+    code: Scalars['String'];
+    translations: Array<ProductOptionTranslationInput>;
+};
+
+export type CreatePromotionInput = {
+    name: Scalars['String'];
+    enabled: Scalars['Boolean'];
+    startsAt?: Maybe<Scalars['DateTime']>;
+    endsAt?: Maybe<Scalars['DateTime']>;
+    couponCode?: Maybe<Scalars['String']>;
+    perCustomerUsageLimit?: Maybe<Scalars['Int']>;
+    conditions: Array<ConfigurableOperationInput>;
+    actions: Array<ConfigurableOperationInput>;
+};
+
+export type CreateRoleInput = {
+    code: Scalars['String'];
+    description: Scalars['String'];
+    permissions: Array<Permission>;
+    channelIds?: Maybe<Array<Scalars['ID']>>;
+};
+
+export type CreateShippingMethodInput = {
+    code: Scalars['String'];
+    description: Scalars['String'];
+    checker: ConfigurableOperationInput;
+    calculator: ConfigurableOperationInput;
+};
+
+export type CreateTaxCategoryInput = {
+    name: Scalars['String'];
+};
+
+export type CreateTaxRateInput = {
+    name: Scalars['String'];
+    enabled: Scalars['Boolean'];
+    value: Scalars['Int'];
+    categoryId: Scalars['ID'];
+    zoneId: Scalars['ID'];
+    customerGroupId?: Maybe<Scalars['ID']>;
+};
+
+export type CreateZoneInput = {
+    name: Scalars['String'];
+    memberIds?: Maybe<Array<Scalars['ID']>>;
+};
+
+/** @description
+ * ISO 4217 currency code
+ *
+ * @docsCategory common
+ */
+export enum CurrencyCode {
+    /** United Arab Emirates dirham */
+    AED = 'AED',
+    /** Afghan afghani */
+    AFN = 'AFN',
+    /** Albanian lek */
+    ALL = 'ALL',
+    /** Armenian dram */
+    AMD = 'AMD',
+    /** Netherlands Antillean guilder */
+    ANG = 'ANG',
+    /** Angolan kwanza */
+    AOA = 'AOA',
+    /** Argentine peso */
+    ARS = 'ARS',
+    /** Australian dollar */
+    AUD = 'AUD',
+    /** Aruban florin */
+    AWG = 'AWG',
+    /** Azerbaijani manat */
+    AZN = 'AZN',
+    /** Bosnia and Herzegovina convertible mark */
+    BAM = 'BAM',
+    /** Barbados dollar */
+    BBD = 'BBD',
+    /** Bangladeshi taka */
+    BDT = 'BDT',
+    /** Bulgarian lev */
+    BGN = 'BGN',
+    /** Bahraini dinar */
+    BHD = 'BHD',
+    /** Burundian franc */
+    BIF = 'BIF',
+    /** Bermudian dollar */
+    BMD = 'BMD',
+    /** Brunei dollar */
+    BND = 'BND',
+    /** Boliviano */
+    BOB = 'BOB',
+    /** Brazilian real */
+    BRL = 'BRL',
+    /** Bahamian dollar */
+    BSD = 'BSD',
+    /** Bhutanese ngultrum */
+    BTN = 'BTN',
+    /** Botswana pula */
+    BWP = 'BWP',
+    /** Belarusian ruble */
+    BYN = 'BYN',
+    /** Belize dollar */
+    BZD = 'BZD',
+    /** Canadian dollar */
+    CAD = 'CAD',
+    /** Congolese franc */
+    CHE = 'CHE',
+    /** Swiss franc */
+    CHW = 'CHW',
+    /** Chilean peso */
+    CLP = 'CLP',
+    /** Renminbi (Chinese) yuan */
+    CNY = 'CNY',
+    /** Colombian peso */
+    COP = 'COP',
+    /** Costa Rican colon */
+    CRC = 'CRC',
+    /** Cuban convertible peso */
+    CUC = 'CUC',
+    /** Cuban peso */
+    CUP = 'CUP',
+    /** Cape Verde escudo */
+    CVE = 'CVE',
+    /** Czech koruna */
+    CZK = 'CZK',
+    /** Djiboutian franc */
+    DJF = 'DJF',
+    /** Danish krone */
+    DKK = 'DKK',
+    /** Dominican peso */
+    DOP = 'DOP',
+    /** Algerian dinar */
+    DZD = 'DZD',
+    /** Egyptian pound */
+    EGP = 'EGP',
+    /** Eritrean nakfa */
+    ERN = 'ERN',
+    /** Ethiopian birr */
+    ETB = 'ETB',
+    /** Euro */
+    EUR = 'EUR',
+    /** Fiji dollar */
+    FJD = 'FJD',
+    /** Falkland Islands pound */
+    FKP = 'FKP',
+    /** Pound sterling */
+    GBP = 'GBP',
+    /** Georgian lari */
+    GEL = 'GEL',
+    /** Ghanaian cedi */
+    GHS = 'GHS',
+    /** Gibraltar pound */
+    GIP = 'GIP',
+    /** Gambian dalasi */
+    GMD = 'GMD',
+    /** Guinean franc */
+    GNF = 'GNF',
+    /** Guatemalan quetzal */
+    GTQ = 'GTQ',
+    /** Guyanese dollar */
+    GYD = 'GYD',
+    /** Hong Kong dollar */
+    HKD = 'HKD',
+    /** Honduran lempira */
+    HNL = 'HNL',
+    /** Croatian kuna */
+    HRK = 'HRK',
+    /** Haitian gourde */
+    HTG = 'HTG',
+    /** Hungarian forint */
+    HUF = 'HUF',
+    /** Indonesian rupiah */
+    IDR = 'IDR',
+    /** Israeli new shekel */
+    ILS = 'ILS',
+    /** Indian rupee */
+    INR = 'INR',
+    /** Iraqi dinar */
+    IQD = 'IQD',
+    /** Iranian rial */
+    IRR = 'IRR',
+    /** Icelandic króna */
+    ISK = 'ISK',
+    /** Jamaican dollar */
+    JMD = 'JMD',
+    /** Jordanian dinar */
+    JOD = 'JOD',
+    /** Japanese yen */
+    JPY = 'JPY',
+    /** Kenyan shilling */
+    KES = 'KES',
+    /** Kyrgyzstani som */
+    KGS = 'KGS',
+    /** Cambodian riel */
+    KHR = 'KHR',
+    /** Comoro franc */
+    KMF = 'KMF',
+    /** North Korean won */
+    KPW = 'KPW',
+    /** South Korean won */
+    KRW = 'KRW',
+    /** Kuwaiti dinar */
+    KWD = 'KWD',
+    /** Cayman Islands dollar */
+    KYD = 'KYD',
+    /** Kazakhstani tenge */
+    KZT = 'KZT',
+    /** Lao kip */
+    LAK = 'LAK',
+    /** Lebanese pound */
+    LBP = 'LBP',
+    /** Sri Lankan rupee */
+    LKR = 'LKR',
+    /** Liberian dollar */
+    LRD = 'LRD',
+    /** Lesotho loti */
+    LSL = 'LSL',
+    /** Libyan dinar */
+    LYD = 'LYD',
+    /** Moroccan dirham */
+    MAD = 'MAD',
+    /** Moldovan leu */
+    MDL = 'MDL',
+    /** Malagasy ariary */
+    MGA = 'MGA',
+    /** Macedonian denar */
+    MKD = 'MKD',
+    /** Myanmar kyat */
+    MMK = 'MMK',
+    /** Mongolian tögrög */
+    MNT = 'MNT',
+    /** Macanese pataca */
+    MOP = 'MOP',
+    /** Mauritanian ouguiya */
+    MRU = 'MRU',
+    /** Mauritian rupee */
+    MUR = 'MUR',
+    /** Maldivian rufiyaa */
+    MVR = 'MVR',
+    /** Malawian kwacha */
+    MWK = 'MWK',
+    /** Mexican peso */
+    MXN = 'MXN',
+    /** Malaysian ringgit */
+    MYR = 'MYR',
+    /** Mozambican metical */
+    MZN = 'MZN',
+    /** Namibian dollar */
+    NAD = 'NAD',
+    /** Nigerian naira */
+    NGN = 'NGN',
+    /** Nicaraguan córdoba */
+    NIO = 'NIO',
+    /** Norwegian krone */
+    NOK = 'NOK',
+    /** Nepalese rupee */
+    NPR = 'NPR',
+    /** New Zealand dollar */
+    NZD = 'NZD',
+    /** Omani rial */
+    OMR = 'OMR',
+    /** Panamanian balboa */
+    PAB = 'PAB',
+    /** Peruvian sol */
+    PEN = 'PEN',
+    /** Papua New Guinean kina */
+    PGK = 'PGK',
+    /** Philippine peso */
+    PHP = 'PHP',
+    /** Pakistani rupee */
+    PKR = 'PKR',
+    /** Polish złoty */
+    PLN = 'PLN',
+    /** Paraguayan guaraní */
+    PYG = 'PYG',
+    /** Qatari riyal */
+    QAR = 'QAR',
+    /** Romanian leu */
+    RON = 'RON',
+    /** Serbian dinar */
+    RSD = 'RSD',
+    /** Russian ruble */
+    RUB = 'RUB',
+    /** Rwandan franc */
+    RWF = 'RWF',
+    /** Saudi riyal */
+    SAR = 'SAR',
+    /** Solomon Islands dollar */
+    SBD = 'SBD',
+    /** Seychelles rupee */
+    SCR = 'SCR',
+    /** Sudanese pound */
+    SDG = 'SDG',
+    /** Swedish krona/kronor */
+    SEK = 'SEK',
+    /** Singapore dollar */
+    SGD = 'SGD',
+    /** Saint Helena pound */
+    SHP = 'SHP',
+    /** Sierra Leonean leone */
+    SLL = 'SLL',
+    /** Somali shilling */
+    SOS = 'SOS',
+    /** Surinamese dollar */
+    SRD = 'SRD',
+    /** South Sudanese pound */
+    SSP = 'SSP',
+    /** São Tomé and Príncipe dobra */
+    STN = 'STN',
+    /** Salvadoran colón */
+    SVC = 'SVC',
+    /** Syrian pound */
+    SYP = 'SYP',
+    /** Swazi lilangeni */
+    SZL = 'SZL',
+    /** Thai baht */
+    THB = 'THB',
+    /** Tajikistani somoni */
+    TJS = 'TJS',
+    /** Turkmenistan manat */
+    TMT = 'TMT',
+    /** Tunisian dinar */
+    TND = 'TND',
+    /** Tongan paʻanga */
+    TOP = 'TOP',
+    /** Turkish lira */
+    TRY = 'TRY',
+    /** Trinidad and Tobago dollar */
+    TTD = 'TTD',
+    /** New Taiwan dollar */
+    TWD = 'TWD',
+    /** Tanzanian shilling */
+    TZS = 'TZS',
+    /** Ukrainian hryvnia */
+    UAH = 'UAH',
+    /** Ugandan shilling */
+    UGX = 'UGX',
+    /** United States dollar */
+    USD = 'USD',
+    /** Uruguayan peso */
+    UYU = 'UYU',
+    /** Uzbekistan som */
+    UZS = 'UZS',
+    /** Venezuelan bolívar soberano */
+    VES = 'VES',
+    /** Vietnamese đồng */
+    VND = 'VND',
+    /** Vanuatu vatu */
+    VUV = 'VUV',
+    /** Samoan tala */
+    WST = 'WST',
+    /** CFA franc BEAC */
+    XAF = 'XAF',
+    /** East Caribbean dollar */
+    XCD = 'XCD',
+    /** CFA franc BCEAO */
+    XOF = 'XOF',
+    /** CFP franc (franc Pacifique) */
+    XPF = 'XPF',
+    /** Yemeni rial */
+    YER = 'YER',
+    /** South African rand */
+    ZAR = 'ZAR',
+    /** Zambian kwacha */
+    ZMW = 'ZMW',
+    /** Zimbabwean dollar */
+    ZWL = 'ZWL',
+}
+
+export type CurrentUser = {
+    __typename?: 'CurrentUser';
+    id: Scalars['ID'];
+    identifier: Scalars['String'];
+    channels: Array<CurrentUserChannel>;
+};
+
+export type CurrentUserChannel = {
+    __typename?: 'CurrentUserChannel';
+    id: Scalars['ID'];
+    token: Scalars['String'];
+    code: Scalars['String'];
+    permissions: Array<Permission>;
+};
+
+export type Customer = Node & {
+    __typename?: 'Customer';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    title?: Maybe<Scalars['String']>;
+    firstName: Scalars['String'];
+    lastName: Scalars['String'];
+    phoneNumber?: Maybe<Scalars['String']>;
+    emailAddress: Scalars['String'];
+    addresses?: Maybe<Array<Address>>;
+    orders: OrderList;
+    user?: Maybe<User>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type CustomerOrdersArgs = {
+    options?: Maybe<OrderListOptions>;
+};
+
+export type CustomerFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    title?: Maybe<StringOperators>;
+    firstName?: Maybe<StringOperators>;
+    lastName?: Maybe<StringOperators>;
+    phoneNumber?: Maybe<StringOperators>;
+    emailAddress?: Maybe<StringOperators>;
+};
+
+export type CustomerGroup = Node & {
+    __typename?: 'CustomerGroup';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    name: Scalars['String'];
+};
+
+export type CustomerList = PaginatedList & {
+    __typename?: 'CustomerList';
+    items: Array<Customer>;
+    totalItems: Scalars['Int'];
+};
+
+export type CustomerListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<CustomerSortParameter>;
+    filter?: Maybe<CustomerFilterParameter>;
+};
+
+export type CustomerSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    title?: Maybe<SortOrder>;
+    firstName?: Maybe<SortOrder>;
+    lastName?: Maybe<SortOrder>;
+    phoneNumber?: Maybe<SortOrder>;
+    emailAddress?: Maybe<SortOrder>;
+};
+
+export type CustomField = {
+    __typename?: 'CustomField';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+};
+
+export type CustomFieldConfig =
+    | StringCustomFieldConfig
+    | LocaleStringCustomFieldConfig
+    | IntCustomFieldConfig
+    | FloatCustomFieldConfig
+    | BooleanCustomFieldConfig
+    | DateTimeCustomFieldConfig;
+
+export type CustomFields = {
+    __typename?: 'CustomFields';
+    Address: Array<CustomFieldConfig>;
+    Collection: Array<CustomFieldConfig>;
+    Customer: Array<CustomFieldConfig>;
+    Facet: Array<CustomFieldConfig>;
+    FacetValue: Array<CustomFieldConfig>;
+    GlobalSettings: Array<CustomFieldConfig>;
+    Order: Array<CustomFieldConfig>;
+    OrderLine: Array<CustomFieldConfig>;
+    Product: Array<CustomFieldConfig>;
+    ProductOption: Array<CustomFieldConfig>;
+    ProductOptionGroup: Array<CustomFieldConfig>;
+    ProductVariant: Array<CustomFieldConfig>;
+    User: Array<CustomFieldConfig>;
+};
+
+export type DateOperators = {
+    eq?: Maybe<Scalars['DateTime']>;
+    before?: Maybe<Scalars['DateTime']>;
+    after?: Maybe<Scalars['DateTime']>;
+    between?: Maybe<DateRange>;
+};
+
+export type DateRange = {
+    start: Scalars['DateTime'];
+    end: Scalars['DateTime'];
+};
+
+/** Expects the same validation formats as the <input type="datetime-local"> HTML element.
+ * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#Additional_attributes
+ */
+export type DateTimeCustomFieldConfig = CustomField & {
+    __typename?: 'DateTimeCustomFieldConfig';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+    min?: Maybe<Scalars['String']>;
+    max?: Maybe<Scalars['String']>;
+    step?: Maybe<Scalars['Int']>;
+};
+
+export type DeletionResponse = {
+    __typename?: 'DeletionResponse';
+    result: DeletionResult;
+    message?: Maybe<Scalars['String']>;
+};
+
+export enum DeletionResult {
+    /** The entity was successfully deleted */
+    DELETED = 'DELETED',
+    /** Deletion did not take place, reason given in message */
+    NOT_DELETED = 'NOT_DELETED',
+}
+
+export type Facet = Node & {
+    __typename?: 'Facet';
+    isPrivate: Scalars['Boolean'];
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    code: Scalars['String'];
+    values: Array<FacetValue>;
+    translations: Array<FacetTranslation>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type FacetFilterParameter = {
+    isPrivate?: Maybe<BooleanOperators>;
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    languageCode?: Maybe<StringOperators>;
+    name?: Maybe<StringOperators>;
+    code?: Maybe<StringOperators>;
+};
+
+export type FacetList = PaginatedList & {
+    __typename?: 'FacetList';
+    items: Array<Facet>;
+    totalItems: Scalars['Int'];
+};
+
+export type FacetListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<FacetSortParameter>;
+    filter?: Maybe<FacetFilterParameter>;
+};
+
+export type FacetSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+    code?: Maybe<SortOrder>;
+};
+
+export type FacetTranslation = {
+    __typename?: 'FacetTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+};
+
+export type FacetTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type FacetValue = Node & {
+    __typename?: 'FacetValue';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    facet: Facet;
+    name: Scalars['String'];
+    code: Scalars['String'];
+    translations: Array<FacetValueTranslation>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+/** Which FacetValues are present in the products returned
+ * by the search, and in what quantity.
+ */
+export type FacetValueResult = {
+    __typename?: 'FacetValueResult';
+    facetValue: FacetValue;
+    count: Scalars['Int'];
+};
+
+export type FacetValueTranslation = {
+    __typename?: 'FacetValueTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+};
+
+export type FacetValueTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type FloatCustomFieldConfig = CustomField & {
+    __typename?: 'FloatCustomFieldConfig';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+    min?: Maybe<Scalars['Float']>;
+    max?: Maybe<Scalars['Float']>;
+    step?: Maybe<Scalars['Float']>;
+};
+
+export type Fulfillment = Node & {
+    __typename?: 'Fulfillment';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    orderItems: Array<OrderItem>;
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+};
+
+export type FulfillOrderInput = {
+    lines: Array<OrderLineInput>;
+    method: Scalars['String'];
+    trackingCode?: Maybe<Scalars['String']>;
+};
+
+export type GlobalSettings = {
+    __typename?: 'GlobalSettings';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    availableLanguages: Array<LanguageCode>;
+    trackInventory: Scalars['Boolean'];
+    serverConfig: ServerConfig;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type HistoryEntry = Node & {
+    __typename?: 'HistoryEntry';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    isPublic: Scalars['Boolean'];
+    type: HistoryEntryType;
+    administrator?: Maybe<Administrator>;
+    data: Scalars['JSON'];
+};
+
+export type HistoryEntryFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    isPublic?: Maybe<BooleanOperators>;
+    type?: Maybe<StringOperators>;
+};
+
+export type HistoryEntryList = PaginatedList & {
+    __typename?: 'HistoryEntryList';
+    items: Array<HistoryEntry>;
+    totalItems: Scalars['Int'];
+};
+
+export type HistoryEntryListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<HistoryEntrySortParameter>;
+    filter?: Maybe<HistoryEntryFilterParameter>;
+};
+
+export type HistoryEntrySortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+};
+
+export enum HistoryEntryType {
+    ORDER_STATE_TRANSITION = 'ORDER_STATE_TRANSITION',
+    ORDER_PAYMENT_TRANSITION = 'ORDER_PAYMENT_TRANSITION',
+    ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
+    ORDER_CANCELLATION = 'ORDER_CANCELLATION',
+    ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
+    ORDER_NOTE = 'ORDER_NOTE',
+    ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
+    ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+}
+
+export type ImportInfo = {
+    __typename?: 'ImportInfo';
+    errors?: Maybe<Array<Scalars['String']>>;
+    processed: Scalars['Int'];
+    imported: Scalars['Int'];
+};
+
+export type IntCustomFieldConfig = CustomField & {
+    __typename?: 'IntCustomFieldConfig';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+    min?: Maybe<Scalars['Int']>;
+    max?: Maybe<Scalars['Int']>;
+    step?: Maybe<Scalars['Int']>;
+};
+
+export type JobInfo = {
+    __typename?: 'JobInfo';
+    id: Scalars['String'];
+    name: Scalars['String'];
+    state: JobState;
+    progress: Scalars['Float'];
+    metadata?: Maybe<Scalars['JSON']>;
+    result?: Maybe<Scalars['JSON']>;
+    started?: Maybe<Scalars['DateTime']>;
+    ended?: Maybe<Scalars['DateTime']>;
+    duration?: Maybe<Scalars['Int']>;
+};
+
+export type JobListInput = {
+    state?: Maybe<JobState>;
+    ids?: Maybe<Array<Scalars['String']>>;
+};
+
+export enum JobState {
+    PENDING = 'PENDING',
+    RUNNING = 'RUNNING',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+/** @description
+ * ISO 639-1 language code
+ *
+ * @docsCategory common
+ */
+export enum LanguageCode {
+    /** Afar */
+    aa = 'aa',
+    /** Abkhazian */
+    ab = 'ab',
+    /** Afrikaans */
+    af = 'af',
+    /** Akan */
+    ak = 'ak',
+    /** Albanian */
+    sq = 'sq',
+    /** Amharic */
+    am = 'am',
+    /** Arabic */
+    ar = 'ar',
+    /** Aragonese */
+    an = 'an',
+    /** Armenian */
+    hy = 'hy',
+    /** Assamese */
+    as = 'as',
+    /** Avaric */
+    av = 'av',
+    /** Avestan */
+    ae = 'ae',
+    /** Aymara */
+    ay = 'ay',
+    /** Azerbaijani */
+    az = 'az',
+    /** Bashkir */
+    ba = 'ba',
+    /** Bambara */
+    bm = 'bm',
+    /** Basque */
+    eu = 'eu',
+    /** Belarusian */
+    be = 'be',
+    /** Bengali */
+    bn = 'bn',
+    /** Bihari languages */
+    bh = 'bh',
+    /** Bislama */
+    bi = 'bi',
+    /** Bosnian */
+    bs = 'bs',
+    /** Breton */
+    br = 'br',
+    /** Bulgarian */
+    bg = 'bg',
+    /** Burmese */
+    my = 'my',
+    /** Catalan; Valencian */
+    ca = 'ca',
+    /** Chamorro */
+    ch = 'ch',
+    /** Chechen */
+    ce = 'ce',
+    /** Chinese */
+    zh = 'zh',
+    /** Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic */
+    cu = 'cu',
+    /** Chuvash */
+    cv = 'cv',
+    /** Cornish */
+    kw = 'kw',
+    /** Corsican */
+    co = 'co',
+    /** Cree */
+    cr = 'cr',
+    /** Czech */
+    cs = 'cs',
+    /** Danish */
+    da = 'da',
+    /** Divehi; Dhivehi; Maldivian */
+    dv = 'dv',
+    /** Dutch; Flemish */
+    nl = 'nl',
+    /** Dzongkha */
+    dz = 'dz',
+    /** English */
+    en = 'en',
+    /** Esperanto */
+    eo = 'eo',
+    /** Estonian */
+    et = 'et',
+    /** Ewe */
+    ee = 'ee',
+    /** Faroese */
+    fo = 'fo',
+    /** Fijian */
+    fj = 'fj',
+    /** Finnish */
+    fi = 'fi',
+    /** French */
+    fr = 'fr',
+    /** Western Frisian */
+    fy = 'fy',
+    /** Fulah */
+    ff = 'ff',
+    /** Georgian */
+    ka = 'ka',
+    /** German */
+    de = 'de',
+    /** Gaelic; Scottish Gaelic */
+    gd = 'gd',
+    /** Irish */
+    ga = 'ga',
+    /** Galician */
+    gl = 'gl',
+    /** Manx */
+    gv = 'gv',
+    /** Greek, Modern (1453-) */
+    el = 'el',
+    /** Guarani */
+    gn = 'gn',
+    /** Gujarati */
+    gu = 'gu',
+    /** Haitian; Haitian Creole */
+    ht = 'ht',
+    /** Hausa */
+    ha = 'ha',
+    /** Hebrew */
+    he = 'he',
+    /** Herero */
+    hz = 'hz',
+    /** Hindi */
+    hi = 'hi',
+    /** Hiri Motu */
+    ho = 'ho',
+    /** Croatian */
+    hr = 'hr',
+    /** Hungarian */
+    hu = 'hu',
+    /** Igbo */
+    ig = 'ig',
+    /** Icelandic */
+    is = 'is',
+    /** Ido */
+    io = 'io',
+    /** Sichuan Yi; Nuosu */
+    ii = 'ii',
+    /** Inuktitut */
+    iu = 'iu',
+    /** Interlingue; Occidental */
+    ie = 'ie',
+    /** Interlingua (International Auxiliary Language Association) */
+    ia = 'ia',
+    /** Indonesian */
+    id = 'id',
+    /** Inupiaq */
+    ik = 'ik',
+    /** Italian */
+    it = 'it',
+    /** Javanese */
+    jv = 'jv',
+    /** Japanese */
+    ja = 'ja',
+    /** Kalaallisut; Greenlandic */
+    kl = 'kl',
+    /** Kannada */
+    kn = 'kn',
+    /** Kashmiri */
+    ks = 'ks',
+    /** Kanuri */
+    kr = 'kr',
+    /** Kazakh */
+    kk = 'kk',
+    /** Central Khmer */
+    km = 'km',
+    /** Kikuyu; Gikuyu */
+    ki = 'ki',
+    /** Kinyarwanda */
+    rw = 'rw',
+    /** Kirghiz; Kyrgyz */
+    ky = 'ky',
+    /** Komi */
+    kv = 'kv',
+    /** Kongo */
+    kg = 'kg',
+    /** Korean */
+    ko = 'ko',
+    /** Kuanyama; Kwanyama */
+    kj = 'kj',
+    /** Kurdish */
+    ku = 'ku',
+    /** Lao */
+    lo = 'lo',
+    /** Latin */
+    la = 'la',
+    /** Latvian */
+    lv = 'lv',
+    /** Limburgan; Limburger; Limburgish */
+    li = 'li',
+    /** Lingala */
+    ln = 'ln',
+    /** Lithuanian */
+    lt = 'lt',
+    /** Luxembourgish; Letzeburgesch */
+    lb = 'lb',
+    /** Luba-Katanga */
+    lu = 'lu',
+    /** Ganda */
+    lg = 'lg',
+    /** Macedonian */
+    mk = 'mk',
+    /** Marshallese */
+    mh = 'mh',
+    /** Malayalam */
+    ml = 'ml',
+    /** Maori */
+    mi = 'mi',
+    /** Marathi */
+    mr = 'mr',
+    /** Malay */
+    ms = 'ms',
+    /** Malagasy */
+    mg = 'mg',
+    /** Maltese */
+    mt = 'mt',
+    /** Mongolian */
+    mn = 'mn',
+    /** Nauru */
+    na = 'na',
+    /** Navajo; Navaho */
+    nv = 'nv',
+    /** Ndebele, South; South Ndebele */
+    nr = 'nr',
+    /** Ndebele, North; North Ndebele */
+    nd = 'nd',
+    /** Ndonga */
+    ng = 'ng',
+    /** Nepali */
+    ne = 'ne',
+    /** Norwegian Nynorsk; Nynorsk, Norwegian */
+    nn = 'nn',
+    /** Bokmål, Norwegian; Norwegian Bokmål */
+    nb = 'nb',
+    /** Norwegian */
+    no = 'no',
+    /** Chichewa; Chewa; Nyanja */
+    ny = 'ny',
+    /** Occitan (post 1500); Provençal */
+    oc = 'oc',
+    /** Ojibwa */
+    oj = 'oj',
+    /** Oriya */
+    or = 'or',
+    /** Oromo */
+    om = 'om',
+    /** Ossetian; Ossetic */
+    os = 'os',
+    /** Panjabi; Punjabi */
+    pa = 'pa',
+    /** Persian */
+    fa = 'fa',
+    /** Pali */
+    pi = 'pi',
+    /** Polish */
+    pl = 'pl',
+    /** Portuguese */
+    pt = 'pt',
+    /** Pushto; Pashto */
+    ps = 'ps',
+    /** Quechua */
+    qu = 'qu',
+    /** Romansh */
+    rm = 'rm',
+    /** Romanian; Moldavian; Moldovan */
+    ro = 'ro',
+    /** Rundi */
+    rn = 'rn',
+    /** Russian */
+    ru = 'ru',
+    /** Sango */
+    sg = 'sg',
+    /** Sanskrit */
+    sa = 'sa',
+    /** Sinhala; Sinhalese */
+    si = 'si',
+    /** Slovak */
+    sk = 'sk',
+    /** Slovenian */
+    sl = 'sl',
+    /** Northern Sami */
+    se = 'se',
+    /** Samoan */
+    sm = 'sm',
+    /** Shona */
+    sn = 'sn',
+    /** Sindhi */
+    sd = 'sd',
+    /** Somali */
+    so = 'so',
+    /** Sotho, Southern */
+    st = 'st',
+    /** Spanish; Castilian */
+    es = 'es',
+    /** Sardinian */
+    sc = 'sc',
+    /** Serbian */
+    sr = 'sr',
+    /** Swati */
+    ss = 'ss',
+    /** Sundanese */
+    su = 'su',
+    /** Swahili */
+    sw = 'sw',
+    /** Swedish */
+    sv = 'sv',
+    /** Tahitian */
+    ty = 'ty',
+    /** Tamil */
+    ta = 'ta',
+    /** Tatar */
+    tt = 'tt',
+    /** Telugu */
+    te = 'te',
+    /** Tajik */
+    tg = 'tg',
+    /** Tagalog */
+    tl = 'tl',
+    /** Thai */
+    th = 'th',
+    /** Tibetan */
+    bo = 'bo',
+    /** Tigrinya */
+    ti = 'ti',
+    /** Tonga (Tonga Islands) */
+    to = 'to',
+    /** Tswana */
+    tn = 'tn',
+    /** Tsonga */
+    ts = 'ts',
+    /** Turkmen */
+    tk = 'tk',
+    /** Turkish */
+    tr = 'tr',
+    /** Twi */
+    tw = 'tw',
+    /** Uighur; Uyghur */
+    ug = 'ug',
+    /** Ukrainian */
+    uk = 'uk',
+    /** Urdu */
+    ur = 'ur',
+    /** Uzbek */
+    uz = 'uz',
+    /** Venda */
+    ve = 've',
+    /** Vietnamese */
+    vi = 'vi',
+    /** Volapük */
+    vo = 'vo',
+    /** Welsh */
+    cy = 'cy',
+    /** Walloon */
+    wa = 'wa',
+    /** Wolof */
+    wo = 'wo',
+    /** Xhosa */
+    xh = 'xh',
+    /** Yiddish */
+    yi = 'yi',
+    /** Yoruba */
+    yo = 'yo',
+    /** Zhuang; Chuang */
+    za = 'za',
+    /** Zulu */
+    zu = 'zu',
+}
+
+export type LocaleStringCustomFieldConfig = CustomField & {
+    __typename?: 'LocaleStringCustomFieldConfig';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+    pattern?: Maybe<Scalars['String']>;
+};
+
+export type LocalizedString = {
+    __typename?: 'LocalizedString';
+    languageCode: LanguageCode;
+    value: Scalars['String'];
+};
+
+export type LoginResult = {
+    __typename?: 'LoginResult';
+    user: CurrentUser;
+};
+
+export type MoveCollectionInput = {
+    collectionId: Scalars['ID'];
+    parentId: Scalars['ID'];
+    index: Scalars['Int'];
+};
+
+export type Mutation = {
+    __typename?: 'Mutation';
+    /** Create a new Administrator */
+    createAdministrator: Administrator;
+    /** Update an existing Administrator */
+    updateAdministrator: Administrator;
+    /** Assign a Role to an Administrator */
+    assignRoleToAdministrator: Administrator;
+    /** Create a new Asset */
+    createAssets: Array<Asset>;
+    login: LoginResult;
+    logout: Scalars['Boolean'];
+    /** Create a new Channel */
+    createChannel: Channel;
+    /** Update an existing Channel */
+    updateChannel: Channel;
+    /** Delete a Channel */
+    deleteChannel: DeletionResponse;
+    /** Create a new Collection */
+    createCollection: Collection;
+    /** Update an existing Collection */
+    updateCollection: Collection;
+    /** Delete a Collection and all of its descendants */
+    deleteCollection: DeletionResponse;
+    /** Move a Collection to a different parent or index */
+    moveCollection: Collection;
+    /** Create a new Country */
+    createCountry: Country;
+    /** Update an existing Country */
+    updateCountry: Country;
+    /** Delete a Country */
+    deleteCountry: DeletionResponse;
+    /** Create a new CustomerGroup */
+    createCustomerGroup: CustomerGroup;
+    /** Update an existing CustomerGroup */
+    updateCustomerGroup: CustomerGroup;
+    /** Add Customers to a CustomerGroup */
+    addCustomersToGroup: CustomerGroup;
+    /** Remove Customers from a CustomerGroup */
+    removeCustomersFromGroup: CustomerGroup;
+    /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
+    createCustomer: Customer;
+    /** Update an existing Customer */
+    updateCustomer: Customer;
+    /** Delete a Customer */
+    deleteCustomer: DeletionResponse;
+    /** Create a new Address and associate it with the Customer specified by customerId */
+    createCustomerAddress: Address;
+    /** Update an existing Address */
+    updateCustomerAddress: Address;
+    /** Update an existing Address */
+    deleteCustomerAddress: Scalars['Boolean'];
+    /** Create a new Facet */
+    createFacet: Facet;
+    /** Update an existing Facet */
+    updateFacet: Facet;
+    /** Delete an existing Facet */
+    deleteFacet: DeletionResponse;
+    /** Create one or more FacetValues */
+    createFacetValues: Array<FacetValue>;
+    /** Update one or more FacetValues */
+    updateFacetValues: Array<FacetValue>;
+    /** Delete one or more FacetValues */
+    deleteFacetValues: Array<DeletionResponse>;
+    updateGlobalSettings: GlobalSettings;
+    importProducts?: Maybe<ImportInfo>;
+    settlePayment: Payment;
+    fulfillOrder: Fulfillment;
+    cancelOrder: Order;
+    refundOrder: Refund;
+    settleRefund: Refund;
+    addNoteToOrder: Order;
+    /** Update an existing PaymentMethod */
+    updatePaymentMethod: PaymentMethod;
+    /** Create a new ProductOptionGroup */
+    createProductOptionGroup: ProductOptionGroup;
+    /** Update an existing ProductOptionGroup */
+    updateProductOptionGroup: ProductOptionGroup;
+    /** Create a new ProductOption within a ProductOptionGroup */
+    createProductOption: ProductOption;
+    /** Create a new ProductOption within a ProductOptionGroup */
+    updateProductOption: ProductOption;
+    reindex: JobInfo;
+    /** Create a new Product */
+    createProduct: Product;
+    /** Update an existing Product */
+    updateProduct: Product;
+    /** Delete a Product */
+    deleteProduct: DeletionResponse;
+    /** Add an OptionGroup to a Product */
+    addOptionGroupToProduct: Product;
+    /** Remove an OptionGroup from a Product */
+    removeOptionGroupFromProduct: Product;
+    /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
+    createProductVariants: Array<Maybe<ProductVariant>>;
+    /** Update existing ProductVariants */
+    updateProductVariants: Array<Maybe<ProductVariant>>;
+    /** Delete a ProductVariant */
+    deleteProductVariant: DeletionResponse;
+    /** Assigns Products to the specified Channel */
+    assignProductsToChannel: Array<Product>;
+    /** Removes Products from the specified Channel */
+    removeProductsFromChannel: Array<Product>;
+    createPromotion: Promotion;
+    updatePromotion: Promotion;
+    deletePromotion: DeletionResponse;
+    /** Create a new Role */
+    createRole: Role;
+    /** Update an existing Role */
+    updateRole: Role;
+    /** Create a new ShippingMethod */
+    createShippingMethod: ShippingMethod;
+    /** Update an existing ShippingMethod */
+    updateShippingMethod: ShippingMethod;
+    /** Delete a ShippingMethod */
+    deleteShippingMethod: DeletionResponse;
+    /** Create a new TaxCategory */
+    createTaxCategory: TaxCategory;
+    /** Update an existing TaxCategory */
+    updateTaxCategory: TaxCategory;
+    /** Create a new TaxRate */
+    createTaxRate: TaxRate;
+    /** Update an existing TaxRate */
+    updateTaxRate: TaxRate;
+    /** Create a new Zone */
+    createZone: Zone;
+    /** Update an existing Zone */
+    updateZone: Zone;
+    /** Delete a Zone */
+    deleteZone: DeletionResponse;
+    /** Add members to a Zone */
+    addMembersToZone: Zone;
+    /** Remove members from a Zone */
+    removeMembersFromZone: Zone;
+};
+
+export type MutationCreateAdministratorArgs = {
+    input: CreateAdministratorInput;
+};
+
+export type MutationUpdateAdministratorArgs = {
+    input: UpdateAdministratorInput;
+};
+
+export type MutationAssignRoleToAdministratorArgs = {
+    administratorId: Scalars['ID'];
+    roleId: Scalars['ID'];
+};
+
+export type MutationCreateAssetsArgs = {
+    input: Array<CreateAssetInput>;
+};
+
+export type MutationLoginArgs = {
+    username: Scalars['String'];
+    password: Scalars['String'];
+    rememberMe?: Maybe<Scalars['Boolean']>;
+};
+
+export type MutationCreateChannelArgs = {
+    input: CreateChannelInput;
+};
+
+export type MutationUpdateChannelArgs = {
+    input: UpdateChannelInput;
+};
+
+export type MutationDeleteChannelArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateCollectionArgs = {
+    input: CreateCollectionInput;
+};
+
+export type MutationUpdateCollectionArgs = {
+    input: UpdateCollectionInput;
+};
+
+export type MutationDeleteCollectionArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationMoveCollectionArgs = {
+    input: MoveCollectionInput;
+};
+
+export type MutationCreateCountryArgs = {
+    input: CreateCountryInput;
+};
+
+export type MutationUpdateCountryArgs = {
+    input: UpdateCountryInput;
+};
+
+export type MutationDeleteCountryArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateCustomerGroupArgs = {
+    input: CreateCustomerGroupInput;
+};
+
+export type MutationUpdateCustomerGroupArgs = {
+    input: UpdateCustomerGroupInput;
+};
+
+export type MutationAddCustomersToGroupArgs = {
+    customerGroupId: Scalars['ID'];
+    customerIds: Array<Scalars['ID']>;
+};
+
+export type MutationRemoveCustomersFromGroupArgs = {
+    customerGroupId: Scalars['ID'];
+    customerIds: Array<Scalars['ID']>;
+};
+
+export type MutationCreateCustomerArgs = {
+    input: CreateCustomerInput;
+    password?: Maybe<Scalars['String']>;
+};
+
+export type MutationUpdateCustomerArgs = {
+    input: UpdateCustomerInput;
+};
+
+export type MutationDeleteCustomerArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateCustomerAddressArgs = {
+    customerId: Scalars['ID'];
+    input: CreateAddressInput;
+};
+
+export type MutationUpdateCustomerAddressArgs = {
+    input: UpdateAddressInput;
+};
+
+export type MutationDeleteCustomerAddressArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateFacetArgs = {
+    input: CreateFacetInput;
+};
+
+export type MutationUpdateFacetArgs = {
+    input: UpdateFacetInput;
+};
+
+export type MutationDeleteFacetArgs = {
+    id: Scalars['ID'];
+    force?: Maybe<Scalars['Boolean']>;
+};
+
+export type MutationCreateFacetValuesArgs = {
+    input: Array<CreateFacetValueInput>;
+};
+
+export type MutationUpdateFacetValuesArgs = {
+    input: Array<UpdateFacetValueInput>;
+};
+
+export type MutationDeleteFacetValuesArgs = {
+    ids: Array<Scalars['ID']>;
+    force?: Maybe<Scalars['Boolean']>;
+};
+
+export type MutationUpdateGlobalSettingsArgs = {
+    input: UpdateGlobalSettingsInput;
+};
+
+export type MutationImportProductsArgs = {
+    csvFile: Scalars['Upload'];
+};
+
+export type MutationSettlePaymentArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationFulfillOrderArgs = {
+    input: FulfillOrderInput;
+};
+
+export type MutationCancelOrderArgs = {
+    input: CancelOrderInput;
+};
+
+export type MutationRefundOrderArgs = {
+    input: RefundOrderInput;
+};
+
+export type MutationSettleRefundArgs = {
+    input: SettleRefundInput;
+};
+
+export type MutationAddNoteToOrderArgs = {
+    input: AddNoteToOrderInput;
+};
+
+export type MutationUpdatePaymentMethodArgs = {
+    input: UpdatePaymentMethodInput;
+};
+
+export type MutationCreateProductOptionGroupArgs = {
+    input: CreateProductOptionGroupInput;
+};
+
+export type MutationUpdateProductOptionGroupArgs = {
+    input: UpdateProductOptionGroupInput;
+};
+
+export type MutationCreateProductOptionArgs = {
+    input: CreateProductOptionInput;
+};
+
+export type MutationUpdateProductOptionArgs = {
+    input: UpdateProductOptionInput;
+};
+
+export type MutationCreateProductArgs = {
+    input: CreateProductInput;
+};
+
+export type MutationUpdateProductArgs = {
+    input: UpdateProductInput;
+};
+
+export type MutationDeleteProductArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationAddOptionGroupToProductArgs = {
+    productId: Scalars['ID'];
+    optionGroupId: Scalars['ID'];
+};
+
+export type MutationRemoveOptionGroupFromProductArgs = {
+    productId: Scalars['ID'];
+    optionGroupId: Scalars['ID'];
+};
+
+export type MutationCreateProductVariantsArgs = {
+    input: Array<CreateProductVariantInput>;
+};
+
+export type MutationUpdateProductVariantsArgs = {
+    input: Array<UpdateProductVariantInput>;
+};
+
+export type MutationDeleteProductVariantArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationAssignProductsToChannelArgs = {
+    input: AssignProductsToChannelInput;
+};
+
+export type MutationRemoveProductsFromChannelArgs = {
+    input: RemoveProductsFromChannelInput;
+};
+
+export type MutationCreatePromotionArgs = {
+    input: CreatePromotionInput;
+};
+
+export type MutationUpdatePromotionArgs = {
+    input: UpdatePromotionInput;
+};
+
+export type MutationDeletePromotionArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateRoleArgs = {
+    input: CreateRoleInput;
+};
+
+export type MutationUpdateRoleArgs = {
+    input: UpdateRoleInput;
+};
+
+export type MutationCreateShippingMethodArgs = {
+    input: CreateShippingMethodInput;
+};
+
+export type MutationUpdateShippingMethodArgs = {
+    input: UpdateShippingMethodInput;
+};
+
+export type MutationDeleteShippingMethodArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationCreateTaxCategoryArgs = {
+    input: CreateTaxCategoryInput;
+};
+
+export type MutationUpdateTaxCategoryArgs = {
+    input: UpdateTaxCategoryInput;
+};
+
+export type MutationCreateTaxRateArgs = {
+    input: CreateTaxRateInput;
+};
+
+export type MutationUpdateTaxRateArgs = {
+    input: UpdateTaxRateInput;
+};
+
+export type MutationCreateZoneArgs = {
+    input: CreateZoneInput;
+};
+
+export type MutationUpdateZoneArgs = {
+    input: UpdateZoneInput;
+};
+
+export type MutationDeleteZoneArgs = {
+    id: Scalars['ID'];
+};
+
+export type MutationAddMembersToZoneArgs = {
+    zoneId: Scalars['ID'];
+    memberIds: Array<Scalars['ID']>;
+};
+
+export type MutationRemoveMembersFromZoneArgs = {
+    zoneId: Scalars['ID'];
+    memberIds: Array<Scalars['ID']>;
+};
+
+export type Node = {
+    __typename?: 'Node';
+    id: Scalars['ID'];
+};
+
+export type NumberOperators = {
+    eq?: Maybe<Scalars['Float']>;
+    lt?: Maybe<Scalars['Float']>;
+    lte?: Maybe<Scalars['Float']>;
+    gt?: Maybe<Scalars['Float']>;
+    gte?: Maybe<Scalars['Float']>;
+    between?: Maybe<NumberRange>;
+};
+
+export type NumberRange = {
+    start: Scalars['Float'];
+    end: Scalars['Float'];
+};
+
+export type Order = Node & {
+    __typename?: 'Order';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    /** A unique code for the Order */
+    code: Scalars['String'];
+    state: Scalars['String'];
+    /** An order is active as long as the payment process has not been completed */
+    active: Scalars['Boolean'];
+    customer?: Maybe<Customer>;
+    shippingAddress?: Maybe<OrderAddress>;
+    billingAddress?: Maybe<OrderAddress>;
+    lines: Array<OrderLine>;
+    /** Order-level adjustments to the order total, such as discounts from promotions */
+    adjustments: Array<Adjustment>;
+    couponCodes: Array<Scalars['String']>;
+    promotions: Array<Promotion>;
+    payments?: Maybe<Array<Payment>>;
+    fulfillments?: Maybe<Array<Fulfillment>>;
+    subTotalBeforeTax: Scalars['Int'];
+    /** The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied. */
+    subTotal: Scalars['Int'];
+    currencyCode: CurrencyCode;
+    shipping: Scalars['Int'];
+    shippingWithTax: Scalars['Int'];
+    shippingMethod?: Maybe<ShippingMethod>;
+    totalBeforeTax: Scalars['Int'];
+    total: Scalars['Int'];
+    history: HistoryEntryList;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type OrderHistoryArgs = {
+    options?: Maybe<HistoryEntryListOptions>;
+};
+
+export type OrderAddress = {
+    __typename?: 'OrderAddress';
+    fullName?: Maybe<Scalars['String']>;
+    company?: Maybe<Scalars['String']>;
+    streetLine1?: Maybe<Scalars['String']>;
+    streetLine2?: Maybe<Scalars['String']>;
+    city?: Maybe<Scalars['String']>;
+    province?: Maybe<Scalars['String']>;
+    postalCode?: Maybe<Scalars['String']>;
+    country?: Maybe<Scalars['String']>;
+    countryCode?: Maybe<Scalars['String']>;
+    phoneNumber?: Maybe<Scalars['String']>;
+};
+
+export type OrderFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    code?: Maybe<StringOperators>;
+    state?: Maybe<StringOperators>;
+    active?: Maybe<BooleanOperators>;
+    subTotalBeforeTax?: Maybe<NumberOperators>;
+    subTotal?: Maybe<NumberOperators>;
+    currencyCode?: Maybe<StringOperators>;
+    shipping?: Maybe<NumberOperators>;
+    shippingWithTax?: Maybe<NumberOperators>;
+    totalBeforeTax?: Maybe<NumberOperators>;
+    total?: Maybe<NumberOperators>;
+};
+
+export type OrderItem = Node & {
+    __typename?: 'OrderItem';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    cancelled: Scalars['Boolean'];
+    unitPrice: Scalars['Int'];
+    unitPriceWithTax: Scalars['Int'];
+    unitPriceIncludesTax: Scalars['Boolean'];
+    taxRate: Scalars['Float'];
+    adjustments: Array<Adjustment>;
+    fulfillment?: Maybe<Fulfillment>;
+    refundId?: Maybe<Scalars['ID']>;
+};
+
+export type OrderLine = Node & {
+    __typename?: 'OrderLine';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    productVariant: ProductVariant;
+    featuredAsset?: Maybe<Asset>;
+    unitPrice: Scalars['Int'];
+    unitPriceWithTax: Scalars['Int'];
+    quantity: Scalars['Int'];
+    items: Array<OrderItem>;
+    totalPrice: Scalars['Int'];
+    adjustments: Array<Adjustment>;
+    order: Order;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type OrderLineInput = {
+    orderLineId: Scalars['ID'];
+    quantity: Scalars['Int'];
+};
+
+export type OrderList = PaginatedList & {
+    __typename?: 'OrderList';
+    items: Array<Order>;
+    totalItems: Scalars['Int'];
+};
+
+export type OrderListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<OrderSortParameter>;
+    filter?: Maybe<OrderFilterParameter>;
+};
+
+export type OrderSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    code?: Maybe<SortOrder>;
+    state?: Maybe<SortOrder>;
+    subTotalBeforeTax?: Maybe<SortOrder>;
+    subTotal?: Maybe<SortOrder>;
+    shipping?: Maybe<SortOrder>;
+    shippingWithTax?: Maybe<SortOrder>;
+    totalBeforeTax?: Maybe<SortOrder>;
+    total?: Maybe<SortOrder>;
+};
+
+export type PaginatedList = {
+    __typename?: 'PaginatedList';
+    items: Array<Node>;
+    totalItems: Scalars['Int'];
+};
+
+export type Payment = Node & {
+    __typename?: 'Payment';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    method: Scalars['String'];
+    amount: Scalars['Int'];
+    state: Scalars['String'];
+    transactionId?: Maybe<Scalars['String']>;
+    errorMessage?: Maybe<Scalars['String']>;
+    refunds: Array<Refund>;
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
+export type PaymentMethod = Node & {
+    __typename?: 'PaymentMethod';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    code: Scalars['String'];
+    enabled: Scalars['Boolean'];
+    configArgs: Array<ConfigArg>;
+};
+
+export type PaymentMethodFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    code?: Maybe<StringOperators>;
+    enabled?: Maybe<BooleanOperators>;
+};
+
+export type PaymentMethodList = PaginatedList & {
+    __typename?: 'PaymentMethodList';
+    items: Array<PaymentMethod>;
+    totalItems: Scalars['Int'];
+};
+
+export type PaymentMethodListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<PaymentMethodSortParameter>;
+    filter?: Maybe<PaymentMethodFilterParameter>;
+};
+
+export type PaymentMethodSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    code?: Maybe<SortOrder>;
+};
+
+/** "
+ * @description
+ * Permissions for administrators and customers. Used to control access to
+ * GraphQL resolvers via the {@link Allow} decorator.
+ *
+ * @docsCategory common
+ */
+export enum Permission {
+    /**  The Authenticated role means simply that the user is logged in  */
+    Authenticated = 'Authenticated',
+    /**  SuperAdmin can perform the most sensitive tasks */
+    SuperAdmin = 'SuperAdmin',
+    /**  Owner means the user owns this entity, e.g. a Customer's own Order */
+    Owner = 'Owner',
+    /**  Public means any unauthenticated user may perform the operation  */
+    Public = 'Public',
+    CreateCatalog = 'CreateCatalog',
+    ReadCatalog = 'ReadCatalog',
+    UpdateCatalog = 'UpdateCatalog',
+    DeleteCatalog = 'DeleteCatalog',
+    CreateCustomer = 'CreateCustomer',
+    ReadCustomer = 'ReadCustomer',
+    UpdateCustomer = 'UpdateCustomer',
+    DeleteCustomer = 'DeleteCustomer',
+    CreateAdministrator = 'CreateAdministrator',
+    ReadAdministrator = 'ReadAdministrator',
+    UpdateAdministrator = 'UpdateAdministrator',
+    DeleteAdministrator = 'DeleteAdministrator',
+    CreateOrder = 'CreateOrder',
+    ReadOrder = 'ReadOrder',
+    UpdateOrder = 'UpdateOrder',
+    DeleteOrder = 'DeleteOrder',
+    CreatePromotion = 'CreatePromotion',
+    ReadPromotion = 'ReadPromotion',
+    UpdatePromotion = 'UpdatePromotion',
+    DeletePromotion = 'DeletePromotion',
+    CreateSettings = 'CreateSettings',
+    ReadSettings = 'ReadSettings',
+    UpdateSettings = 'UpdateSettings',
+    DeleteSettings = 'DeleteSettings',
+}
+
+/** The price range where the result has more than one price */
+export type PriceRange = {
+    __typename?: 'PriceRange';
+    min: Scalars['Int'];
+    max: Scalars['Int'];
+};
+
+export type Product = Node & {
+    __typename?: 'Product';
+    enabled: Scalars['Boolean'];
+    channels: Array<Channel>;
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    slug: Scalars['String'];
+    description: Scalars['String'];
+    featuredAsset?: Maybe<Asset>;
+    assets: Array<Asset>;
+    variants: Array<ProductVariant>;
+    optionGroups: Array<ProductOptionGroup>;
+    facetValues: Array<FacetValue>;
+    translations: Array<ProductTranslation>;
+    collections: Array<Collection>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductFilterParameter = {
+    enabled?: Maybe<BooleanOperators>;
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    languageCode?: Maybe<StringOperators>;
+    name?: Maybe<StringOperators>;
+    slug?: Maybe<StringOperators>;
+    description?: Maybe<StringOperators>;
+};
+
+export type ProductList = PaginatedList & {
+    __typename?: 'ProductList';
+    items: Array<Product>;
+    totalItems: Scalars['Int'];
+};
+
+export type ProductListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<ProductSortParameter>;
+    filter?: Maybe<ProductFilterParameter>;
+};
+
+export type ProductOption = Node & {
+    __typename?: 'ProductOption';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    code: Scalars['String'];
+    name: Scalars['String'];
+    groupId: Scalars['ID'];
+    translations: Array<ProductOptionTranslation>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductOptionGroup = Node & {
+    __typename?: 'ProductOptionGroup';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    code: Scalars['String'];
+    name: Scalars['String'];
+    options: Array<ProductOption>;
+    translations: Array<ProductOptionGroupTranslation>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductOptionGroupTranslation = {
+    __typename?: 'ProductOptionGroupTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+};
+
+export type ProductOptionGroupTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductOptionTranslation = {
+    __typename?: 'ProductOptionTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+};
+
+export type ProductOptionTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+    slug?: Maybe<SortOrder>;
+    description?: Maybe<SortOrder>;
+};
+
+export type ProductTranslation = {
+    __typename?: 'ProductTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    slug: Scalars['String'];
+    description: Scalars['String'];
+};
+
+export type ProductTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    slug?: Maybe<Scalars['String']>;
+    description?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductVariant = Node & {
+    __typename?: 'ProductVariant';
+    enabled: Scalars['Boolean'];
+    stockOnHand: Scalars['Int'];
+    trackInventory: Scalars['Boolean'];
+    stockMovements: StockMovementList;
+    id: Scalars['ID'];
+    productId: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    sku: Scalars['String'];
+    name: Scalars['String'];
+    featuredAsset?: Maybe<Asset>;
+    assets: Array<Asset>;
+    price: Scalars['Int'];
+    currencyCode: CurrencyCode;
+    priceIncludesTax: Scalars['Boolean'];
+    priceWithTax: Scalars['Int'];
+    taxRateApplied: TaxRate;
+    taxCategory: TaxCategory;
+    options: Array<ProductOption>;
+    facetValues: Array<FacetValue>;
+    translations: Array<ProductVariantTranslation>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type ProductVariantStockMovementsArgs = {
+    options?: Maybe<StockMovementListOptions>;
+};
+
+export type ProductVariantFilterParameter = {
+    enabled?: Maybe<BooleanOperators>;
+    stockOnHand?: Maybe<NumberOperators>;
+    trackInventory?: Maybe<BooleanOperators>;
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    languageCode?: Maybe<StringOperators>;
+    sku?: Maybe<StringOperators>;
+    name?: Maybe<StringOperators>;
+    price?: Maybe<NumberOperators>;
+    currencyCode?: Maybe<StringOperators>;
+    priceIncludesTax?: Maybe<BooleanOperators>;
+    priceWithTax?: Maybe<NumberOperators>;
+};
+
+export type ProductVariantList = PaginatedList & {
+    __typename?: 'ProductVariantList';
+    items: Array<ProductVariant>;
+    totalItems: Scalars['Int'];
+};
+
+export type ProductVariantListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<ProductVariantSortParameter>;
+    filter?: Maybe<ProductVariantFilterParameter>;
+};
+
+export type ProductVariantSortParameter = {
+    stockOnHand?: Maybe<SortOrder>;
+    id?: Maybe<SortOrder>;
+    productId?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    sku?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+    price?: Maybe<SortOrder>;
+    priceWithTax?: Maybe<SortOrder>;
+};
+
+export type ProductVariantTranslation = {
+    __typename?: 'ProductVariantTranslation';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+};
+
+export type ProductVariantTranslationInput = {
+    id?: Maybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type Promotion = Node & {
+    __typename?: 'Promotion';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    startsAt?: Maybe<Scalars['DateTime']>;
+    endsAt?: Maybe<Scalars['DateTime']>;
+    couponCode?: Maybe<Scalars['String']>;
+    perCustomerUsageLimit?: Maybe<Scalars['Int']>;
+    name: Scalars['String'];
+    enabled: Scalars['Boolean'];
+    conditions: Array<ConfigurableOperation>;
+    actions: Array<ConfigurableOperation>;
+};
+
+export type PromotionFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    startsAt?: Maybe<DateOperators>;
+    endsAt?: Maybe<DateOperators>;
+    couponCode?: Maybe<StringOperators>;
+    perCustomerUsageLimit?: Maybe<NumberOperators>;
+    name?: Maybe<StringOperators>;
+    enabled?: Maybe<BooleanOperators>;
+};
+
+export type PromotionList = PaginatedList & {
+    __typename?: 'PromotionList';
+    items: Array<Promotion>;
+    totalItems: Scalars['Int'];
+};
+
+export type PromotionListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<PromotionSortParameter>;
+    filter?: Maybe<PromotionFilterParameter>;
+};
+
+export type PromotionSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    startsAt?: Maybe<SortOrder>;
+    endsAt?: Maybe<SortOrder>;
+    couponCode?: Maybe<SortOrder>;
+    perCustomerUsageLimit?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+};
+
+export type Query = {
+    __typename?: 'Query';
+    administrators: AdministratorList;
+    administrator?: Maybe<Administrator>;
+    assets: AssetList;
+    asset?: Maybe<Asset>;
+    me?: Maybe<CurrentUser>;
+    channels: Array<Channel>;
+    channel?: Maybe<Channel>;
+    activeChannel: Channel;
+    collections: CollectionList;
+    collection?: Maybe<Collection>;
+    collectionFilters: Array<ConfigurableOperationDefinition>;
+    countries: CountryList;
+    country?: Maybe<Country>;
+    customerGroups: Array<CustomerGroup>;
+    customerGroup?: Maybe<CustomerGroup>;
+    customers: CustomerList;
+    customer?: Maybe<Customer>;
+    facets: FacetList;
+    facet?: Maybe<Facet>;
+    globalSettings: GlobalSettings;
+    job?: Maybe<JobInfo>;
+    jobs: Array<JobInfo>;
+    order?: Maybe<Order>;
+    orders: OrderList;
+    paymentMethods: PaymentMethodList;
+    paymentMethod?: Maybe<PaymentMethod>;
+    productOptionGroups: Array<ProductOptionGroup>;
+    productOptionGroup?: Maybe<ProductOptionGroup>;
+    search: SearchResponse;
+    products: ProductList;
+    /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
+    product?: Maybe<Product>;
+    promotion?: Maybe<Promotion>;
+    promotions: PromotionList;
+    promotionConditions: Array<ConfigurableOperationDefinition>;
+    promotionActions: Array<ConfigurableOperationDefinition>;
+    roles: RoleList;
+    role?: Maybe<Role>;
+    shippingMethods: ShippingMethodList;
+    shippingMethod?: Maybe<ShippingMethod>;
+    shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
+    shippingCalculators: Array<ConfigurableOperationDefinition>;
+    testShippingMethod: TestShippingMethodResult;
+    testEligibleShippingMethods: Array<ShippingMethodQuote>;
+    taxCategories: Array<TaxCategory>;
+    taxCategory?: Maybe<TaxCategory>;
+    taxRates: TaxRateList;
+    taxRate?: Maybe<TaxRate>;
+    zones: Array<Zone>;
+    zone?: Maybe<Zone>;
+};
+
+export type QueryAdministratorsArgs = {
+    options?: Maybe<AdministratorListOptions>;
+};
+
+export type QueryAdministratorArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryAssetsArgs = {
+    options?: Maybe<AssetListOptions>;
+};
+
+export type QueryAssetArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryChannelArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryCollectionsArgs = {
+    options?: Maybe<CollectionListOptions>;
+};
+
+export type QueryCollectionArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryCountriesArgs = {
+    options?: Maybe<CountryListOptions>;
+};
+
+export type QueryCountryArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryCustomerGroupArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryCustomersArgs = {
+    options?: Maybe<CustomerListOptions>;
+};
+
+export type QueryCustomerArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryFacetsArgs = {
+    options?: Maybe<FacetListOptions>;
+};
+
+export type QueryFacetArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryJobArgs = {
+    jobId: Scalars['String'];
+};
+
+export type QueryJobsArgs = {
+    input?: Maybe<JobListInput>;
+};
+
+export type QueryOrderArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryOrdersArgs = {
+    options?: Maybe<OrderListOptions>;
+};
+
+export type QueryPaymentMethodsArgs = {
+    options?: Maybe<PaymentMethodListOptions>;
+};
+
+export type QueryPaymentMethodArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryProductOptionGroupsArgs = {
+    filterTerm?: Maybe<Scalars['String']>;
+};
+
+export type QueryProductOptionGroupArgs = {
+    id: Scalars['ID'];
+};
+
+export type QuerySearchArgs = {
+    input: SearchInput;
+};
+
+export type QueryProductsArgs = {
+    options?: Maybe<ProductListOptions>;
+};
+
+export type QueryProductArgs = {
+    id?: Maybe<Scalars['ID']>;
+    slug?: Maybe<Scalars['String']>;
+};
+
+export type QueryPromotionArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryPromotionsArgs = {
+    options?: Maybe<PromotionListOptions>;
+};
+
+export type QueryRolesArgs = {
+    options?: Maybe<RoleListOptions>;
+};
+
+export type QueryRoleArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryShippingMethodsArgs = {
+    options?: Maybe<ShippingMethodListOptions>;
+};
+
+export type QueryShippingMethodArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryTestShippingMethodArgs = {
+    input: TestShippingMethodInput;
+};
+
+export type QueryTestEligibleShippingMethodsArgs = {
+    input: TestEligibleShippingMethodsInput;
+};
+
+export type QueryTaxCategoryArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryTaxRatesArgs = {
+    options?: Maybe<TaxRateListOptions>;
+};
+
+export type QueryTaxRateArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryZoneArgs = {
+    id: Scalars['ID'];
+};
+
+export type Refund = Node & {
+    __typename?: 'Refund';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    items: Scalars['Int'];
+    shipping: Scalars['Int'];
+    adjustment: Scalars['Int'];
+    total: Scalars['Int'];
+    method?: Maybe<Scalars['String']>;
+    state: Scalars['String'];
+    transactionId?: Maybe<Scalars['String']>;
+    reason?: Maybe<Scalars['String']>;
+    orderItems: Array<OrderItem>;
+    paymentId: Scalars['ID'];
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
+export type RefundOrderInput = {
+    lines: Array<OrderLineInput>;
+    shipping: Scalars['Int'];
+    adjustment: Scalars['Int'];
+    paymentId: Scalars['ID'];
+    reason?: Maybe<Scalars['String']>;
+};
+
+export type RemoveProductsFromChannelInput = {
+    productIds: Array<Scalars['ID']>;
+    channelId: Scalars['ID'];
+};
+
+export type Return = Node &
+    StockMovement & {
+        __typename?: 'Return';
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderItem: OrderItem;
+    };
+
+export type Role = Node & {
+    __typename?: 'Role';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    code: Scalars['String'];
+    description: Scalars['String'];
+    permissions: Array<Permission>;
+    channels: Array<Channel>;
+};
+
+export type RoleFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    code?: Maybe<StringOperators>;
+    description?: Maybe<StringOperators>;
+};
+
+export type RoleList = PaginatedList & {
+    __typename?: 'RoleList';
+    items: Array<Role>;
+    totalItems: Scalars['Int'];
+};
+
+export type RoleListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<RoleSortParameter>;
+    filter?: Maybe<RoleFilterParameter>;
+};
+
+export type RoleSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    code?: Maybe<SortOrder>;
+    description?: Maybe<SortOrder>;
+};
+
+export type Sale = Node &
+    StockMovement & {
+        __typename?: 'Sale';
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+        orderLine: OrderLine;
+    };
+
+export type SearchInput = {
+    term?: Maybe<Scalars['String']>;
+    facetValueIds?: Maybe<Array<Scalars['ID']>>;
+    collectionId?: Maybe<Scalars['ID']>;
+    groupByProduct?: Maybe<Scalars['Boolean']>;
+    take?: Maybe<Scalars['Int']>;
+    skip?: Maybe<Scalars['Int']>;
+    sort?: Maybe<SearchResultSortParameter>;
+};
+
+export type SearchReindexResponse = {
+    __typename?: 'SearchReindexResponse';
+    success: Scalars['Boolean'];
+};
+
+export type SearchResponse = {
+    __typename?: 'SearchResponse';
+    items: Array<SearchResult>;
+    totalItems: Scalars['Int'];
+    facetValues: Array<FacetValueResult>;
+};
+
+export type SearchResult = {
+    __typename?: 'SearchResult';
+    enabled: Scalars['Boolean'];
+    /** An array of ids of the Collections in which this result appears */
+    channelIds: Array<Scalars['ID']>;
+    sku: Scalars['String'];
+    slug: Scalars['String'];
+    productId: Scalars['ID'];
+    productName: Scalars['String'];
+    productPreview: Scalars['String'];
+    productVariantId: Scalars['ID'];
+    productVariantName: Scalars['String'];
+    productVariantPreview: Scalars['String'];
+    price: SearchResultPrice;
+    priceWithTax: SearchResultPrice;
+    currencyCode: CurrencyCode;
+    description: Scalars['String'];
+    facetIds: Array<Scalars['ID']>;
+    facetValueIds: Array<Scalars['ID']>;
+    /** An array of ids of the Collections in which this result appears */
+    collectionIds: Array<Scalars['ID']>;
+    /** A relevence score for the result. Differs between database implementations */
+    score: Scalars['Float'];
+};
+
+/** The price of a search result product, either as a range or as a single price */
+export type SearchResultPrice = PriceRange | SinglePrice;
+
+export type SearchResultSortParameter = {
+    name?: Maybe<SortOrder>;
+    price?: Maybe<SortOrder>;
+};
+
+export type ServerConfig = {
+    __typename?: 'ServerConfig';
+    customFieldConfig: CustomFields;
+};
+
+export type SettleRefundInput = {
+    id: Scalars['ID'];
+    transactionId: Scalars['String'];
+};
+
+export type ShippingMethod = Node & {
+    __typename?: 'ShippingMethod';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    code: Scalars['String'];
+    description: Scalars['String'];
+    checker: ConfigurableOperation;
+    calculator: ConfigurableOperation;
+};
+
+export type ShippingMethodFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    code?: Maybe<StringOperators>;
+    description?: Maybe<StringOperators>;
+};
+
+export type ShippingMethodList = PaginatedList & {
+    __typename?: 'ShippingMethodList';
+    items: Array<ShippingMethod>;
+    totalItems: Scalars['Int'];
+};
+
+export type ShippingMethodListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<ShippingMethodSortParameter>;
+    filter?: Maybe<ShippingMethodFilterParameter>;
+};
+
+export type ShippingMethodQuote = {
+    __typename?: 'ShippingMethodQuote';
+    id: Scalars['ID'];
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    description: Scalars['String'];
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
+export type ShippingMethodSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    code?: Maybe<SortOrder>;
+    description?: Maybe<SortOrder>;
+};
+
+/** The price value where the result has a single price */
+export type SinglePrice = {
+    __typename?: 'SinglePrice';
+    value: Scalars['Int'];
+};
+
+export enum SortOrder {
+    ASC = 'ASC',
+    DESC = 'DESC',
+}
+
+export type StockAdjustment = Node &
+    StockMovement & {
+        __typename?: 'StockAdjustment';
+        id: Scalars['ID'];
+        createdAt: Scalars['DateTime'];
+        updatedAt: Scalars['DateTime'];
+        productVariant: ProductVariant;
+        type: StockMovementType;
+        quantity: Scalars['Int'];
+    };
+
+export type StockMovement = {
+    __typename?: 'StockMovement';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    productVariant: ProductVariant;
+    type: StockMovementType;
+    quantity: Scalars['Int'];
+};
+
+export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
+
+export type StockMovementList = {
+    __typename?: 'StockMovementList';
+    items: Array<StockMovementItem>;
+    totalItems: Scalars['Int'];
+};
+
+export type StockMovementListOptions = {
+    type?: Maybe<StockMovementType>;
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+};
+
+export enum StockMovementType {
+    ADJUSTMENT = 'ADJUSTMENT',
+    SALE = 'SALE',
+    CANCELLATION = 'CANCELLATION',
+    RETURN = 'RETURN',
+}
+
+export type StringCustomFieldConfig = CustomField & {
+    __typename?: 'StringCustomFieldConfig';
+    name: Scalars['String'];
+    type: Scalars['String'];
+    length?: Maybe<Scalars['Int']>;
+    label?: Maybe<Array<LocalizedString>>;
+    description?: Maybe<Array<LocalizedString>>;
+    pattern?: Maybe<Scalars['String']>;
+    options?: Maybe<Array<StringFieldOption>>;
+};
+
+export type StringFieldOption = {
+    __typename?: 'StringFieldOption';
+    value: Scalars['String'];
+    label?: Maybe<Array<LocalizedString>>;
+};
+
+export type StringOperators = {
+    eq?: Maybe<Scalars['String']>;
+    contains?: Maybe<Scalars['String']>;
+};
+
+export type TaxCategory = Node & {
+    __typename?: 'TaxCategory';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    name: Scalars['String'];
+};
+
+export type TaxRate = Node & {
+    __typename?: 'TaxRate';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    name: Scalars['String'];
+    enabled: Scalars['Boolean'];
+    value: Scalars['Int'];
+    category: TaxCategory;
+    zone: Zone;
+    customerGroup?: Maybe<CustomerGroup>;
+};
+
+export type TaxRateFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    name?: Maybe<StringOperators>;
+    enabled?: Maybe<BooleanOperators>;
+    value?: Maybe<NumberOperators>;
+};
+
+export type TaxRateList = PaginatedList & {
+    __typename?: 'TaxRateList';
+    items: Array<TaxRate>;
+    totalItems: Scalars['Int'];
+};
+
+export type TaxRateListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<TaxRateSortParameter>;
+    filter?: Maybe<TaxRateFilterParameter>;
+};
+
+export type TaxRateSortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+    name?: Maybe<SortOrder>;
+    value?: Maybe<SortOrder>;
+};
+
+export type TestEligibleShippingMethodsInput = {
+    shippingAddress: CreateAddressInput;
+    lines: Array<TestShippingMethodOrderLineInput>;
+};
+
+export type TestShippingMethodInput = {
+    checker: ConfigurableOperationInput;
+    calculator: ConfigurableOperationInput;
+    shippingAddress: CreateAddressInput;
+    lines: Array<TestShippingMethodOrderLineInput>;
+};
+
+export type TestShippingMethodOrderLineInput = {
+    productVariantId: Scalars['ID'];
+    quantity: Scalars['Int'];
+};
+
+export type TestShippingMethodQuote = {
+    __typename?: 'TestShippingMethodQuote';
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+    description: Scalars['String'];
+    metadata?: Maybe<Scalars['JSON']>;
+};
+
+export type TestShippingMethodResult = {
+    __typename?: 'TestShippingMethodResult';
+    eligible: Scalars['Boolean'];
+    quote?: Maybe<TestShippingMethodQuote>;
+};
+
+export type UpdateAddressInput = {
+    id: Scalars['ID'];
+    fullName?: Maybe<Scalars['String']>;
+    company?: Maybe<Scalars['String']>;
+    streetLine1?: Maybe<Scalars['String']>;
+    streetLine2?: Maybe<Scalars['String']>;
+    city?: Maybe<Scalars['String']>;
+    province?: Maybe<Scalars['String']>;
+    postalCode?: Maybe<Scalars['String']>;
+    countryCode?: Maybe<Scalars['String']>;
+    phoneNumber?: Maybe<Scalars['String']>;
+    defaultShippingAddress?: Maybe<Scalars['Boolean']>;
+    defaultBillingAddress?: Maybe<Scalars['Boolean']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateAdministratorInput = {
+    id: Scalars['ID'];
+    firstName?: Maybe<Scalars['String']>;
+    lastName?: Maybe<Scalars['String']>;
+    emailAddress?: Maybe<Scalars['String']>;
+    password?: Maybe<Scalars['String']>;
+    roleIds?: Maybe<Array<Scalars['ID']>>;
+};
+
+export type UpdateChannelInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    token?: Maybe<Scalars['String']>;
+    defaultLanguageCode?: Maybe<LanguageCode>;
+    pricesIncludeTax?: Maybe<Scalars['Boolean']>;
+    currencyCode?: Maybe<CurrencyCode>;
+    defaultTaxZoneId?: Maybe<Scalars['ID']>;
+    defaultShippingZoneId?: Maybe<Scalars['ID']>;
+};
+
+export type UpdateCollectionInput = {
+    id: Scalars['ID'];
+    isPrivate?: Maybe<Scalars['Boolean']>;
+    featuredAssetId?: Maybe<Scalars['ID']>;
+    parentId?: Maybe<Scalars['ID']>;
+    assetIds?: Maybe<Array<Scalars['ID']>>;
+    filters?: Maybe<Array<ConfigurableOperationInput>>;
+    translations?: Maybe<Array<CollectionTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateCountryInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    translations?: Maybe<Array<CountryTranslationInput>>;
+    enabled?: Maybe<Scalars['Boolean']>;
+};
+
+export type UpdateCustomerGroupInput = {
+    id: Scalars['ID'];
+    name?: Maybe<Scalars['String']>;
+};
+
+export type UpdateCustomerInput = {
+    id: Scalars['ID'];
+    title?: Maybe<Scalars['String']>;
+    firstName?: Maybe<Scalars['String']>;
+    lastName?: Maybe<Scalars['String']>;
+    phoneNumber?: Maybe<Scalars['String']>;
+    emailAddress?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateFacetInput = {
+    id: Scalars['ID'];
+    isPrivate?: Maybe<Scalars['Boolean']>;
+    code?: Maybe<Scalars['String']>;
+    translations?: Maybe<Array<FacetTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateFacetValueInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    translations?: Maybe<Array<FacetValueTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateGlobalSettingsInput = {
+    availableLanguages?: Maybe<Array<LanguageCode>>;
+    trackInventory?: Maybe<Scalars['Boolean']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdatePaymentMethodInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    enabled?: Maybe<Scalars['Boolean']>;
+    configArgs?: Maybe<Array<ConfigArgInput>>;
+};
+
+export type UpdateProductInput = {
+    id: Scalars['ID'];
+    enabled?: Maybe<Scalars['Boolean']>;
+    featuredAssetId?: Maybe<Scalars['ID']>;
+    assetIds?: Maybe<Array<Scalars['ID']>>;
+    facetValueIds?: Maybe<Array<Scalars['ID']>>;
+    translations?: Maybe<Array<ProductTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateProductOptionGroupInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    translations?: Maybe<Array<ProductOptionGroupTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateProductOptionInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    translations?: Maybe<Array<ProductOptionGroupTranslationInput>>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdateProductVariantInput = {
+    id: Scalars['ID'];
+    enabled?: Maybe<Scalars['Boolean']>;
+    translations?: Maybe<Array<ProductVariantTranslationInput>>;
+    facetValueIds?: Maybe<Array<Scalars['ID']>>;
+    sku?: Maybe<Scalars['String']>;
+    taxCategoryId?: Maybe<Scalars['ID']>;
+    price?: Maybe<Scalars['Int']>;
+    featuredAssetId?: Maybe<Scalars['ID']>;
+    assetIds?: Maybe<Array<Scalars['ID']>>;
+    stockOnHand?: Maybe<Scalars['Int']>;
+    trackInventory?: Maybe<Scalars['Boolean']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type UpdatePromotionInput = {
+    id: Scalars['ID'];
+    name?: Maybe<Scalars['String']>;
+    enabled?: Maybe<Scalars['Boolean']>;
+    startsAt?: Maybe<Scalars['DateTime']>;
+    endsAt?: Maybe<Scalars['DateTime']>;
+    couponCode?: Maybe<Scalars['String']>;
+    perCustomerUsageLimit?: Maybe<Scalars['Int']>;
+    conditions?: Maybe<Array<ConfigurableOperationInput>>;
+    actions?: Maybe<Array<ConfigurableOperationInput>>;
+};
+
+export type UpdateRoleInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    description?: Maybe<Scalars['String']>;
+    permissions?: Maybe<Array<Permission>>;
+    channelIds?: Maybe<Array<Scalars['ID']>>;
+};
+
+export type UpdateShippingMethodInput = {
+    id: Scalars['ID'];
+    code?: Maybe<Scalars['String']>;
+    description?: Maybe<Scalars['String']>;
+    checker?: Maybe<ConfigurableOperationInput>;
+    calculator?: Maybe<ConfigurableOperationInput>;
+};
+
+export type UpdateTaxCategoryInput = {
+    id: Scalars['ID'];
+    name?: Maybe<Scalars['String']>;
+};
+
+export type UpdateTaxRateInput = {
+    id: Scalars['ID'];
+    name?: Maybe<Scalars['String']>;
+    value?: Maybe<Scalars['Int']>;
+    enabled?: Maybe<Scalars['Boolean']>;
+    categoryId?: Maybe<Scalars['ID']>;
+    zoneId?: Maybe<Scalars['ID']>;
+    customerGroupId?: Maybe<Scalars['ID']>;
+};
+
+export type UpdateZoneInput = {
+    id: Scalars['ID'];
+    name?: Maybe<Scalars['String']>;
+};
+
+export type User = Node & {
+    __typename?: 'User';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    identifier: Scalars['String'];
+    verified: Scalars['Boolean'];
+    roles: Array<Role>;
+    lastLogin?: Maybe<Scalars['String']>;
+    customFields?: Maybe<Scalars['JSON']>;
+};
+
+export type Zone = Node & {
+    __typename?: 'Zone';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    name: Scalars['String'];
+    members: Array<Country>;
+};
+export type SearchProductsAdminQueryVariables = {
+    input: SearchInput;
+};
+
+export type SearchProductsAdminQuery = { __typename?: 'Query' } & {
+    search: { __typename?: 'SearchResponse' } & Pick<SearchResponse, 'totalItems'> & {
+            items: Array<
+                { __typename?: 'SearchResult' } & Pick<
+                    SearchResult,
+                    | 'enabled'
+                    | 'productId'
+                    | 'productName'
+                    | 'productPreview'
+                    | 'productVariantId'
+                    | 'productVariantName'
+                    | 'productVariantPreview'
+                    | 'sku'
+                >
+            >;
+        };
+};
+
+export type SearchFacetValuesQueryVariables = {
+    input: SearchInput;
+};
+
+export type SearchFacetValuesQuery = { __typename?: 'Query' } & {
+    search: { __typename?: 'SearchResponse' } & Pick<SearchResponse, 'totalItems'> & {
+            facetValues: Array<
+                { __typename?: 'FacetValueResult' } & Pick<FacetValueResult, 'count'> & {
+                        facetValue: { __typename?: 'FacetValue' } & Pick<FacetValue, 'id' | 'name'>;
+                    }
+            >;
+        };
+};
+
+export type SearchGetPricesQueryVariables = {
+    input: SearchInput;
+};
+
+export type SearchGetPricesQuery = { __typename?: 'Query' } & {
+    search: { __typename?: 'SearchResponse' } & {
+        items: Array<
+            { __typename?: 'SearchResult' } & {
+                price:
+                    | ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
+                    | ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>);
+                priceWithTax:
+                    | ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
+                    | ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>);
+            }
+        >;
+    };
+};
+type DiscriminateUnion<T, U> = T extends U ? T : never;
+
+type RequireField<T, TNames extends string> = T & { [P in TNames]: (T & { [name: string]: never })[P] };
+
+export namespace SearchProductsAdmin {
+    export type Variables = SearchProductsAdminQueryVariables;
+    export type Query = SearchProductsAdminQuery;
+    export type Search = SearchProductsAdminQuery['search'];
+    export type Items = NonNullable<SearchProductsAdminQuery['search']['items'][0]>;
+}
+
+export namespace SearchFacetValues {
+    export type Variables = SearchFacetValuesQueryVariables;
+    export type Query = SearchFacetValuesQuery;
+    export type Search = SearchFacetValuesQuery['search'];
+    export type FacetValues = NonNullable<SearchFacetValuesQuery['search']['facetValues'][0]>;
+    export type FacetValue = (NonNullable<SearchFacetValuesQuery['search']['facetValues'][0]>)['facetValue'];
+}
+
+export namespace SearchGetPrices {
+    export type Variables = SearchGetPricesQueryVariables;
+    export type Query = SearchGetPricesQuery;
+    export type Search = SearchGetPricesQuery['search'];
+    export type Items = NonNullable<SearchGetPricesQuery['search']['items'][0]>;
+    export type Price = (NonNullable<SearchGetPricesQuery['search']['items'][0]>)['price'];
+    export type PriceRangeInlineFragment = DiscriminateUnion<
+        RequireField<(NonNullable<SearchGetPricesQuery['search']['items'][0]>)['price'], '__typename'>,
+        { __typename: 'PriceRange' }
+    >;
+    export type SinglePriceInlineFragment = DiscriminateUnion<
+        RequireField<(NonNullable<SearchGetPricesQuery['search']['items'][0]>)['price'], '__typename'>,
+        { __typename: 'SinglePrice' }
+    >;
+    export type PriceWithTax = (NonNullable<SearchGetPricesQuery['search']['items'][0]>)['priceWithTax'];
+    export type _PriceRangeInlineFragment = DiscriminateUnion<
+        RequireField<(NonNullable<SearchGetPricesQuery['search']['items'][0]>)['priceWithTax'], '__typename'>,
+        { __typename: 'PriceRange' }
+    >;
+    export type _SinglePriceInlineFragment = DiscriminateUnion<
+        RequireField<(NonNullable<SearchGetPricesQuery['search']['items'][0]>)['priceWithTax'], '__typename'>,
+        { __typename: 'SinglePrice' }
+    >;
+}

+ 39 - 17
packages/elasticsearch-plugin/src/build-elastic-body.spec.ts

@@ -6,11 +6,14 @@ import { defaultOptions, SearchConfig } from './options';
 
 describe('buildElasticBody()', () => {
     const searchConfig = defaultOptions.searchConfig;
+    const CHANNEL_ID = 42;
+    const CHANNEL_ID_TERM = { term: { channelId: CHANNEL_ID } };
 
     it('search term', () => {
-        const result = buildElasticBody({ term: 'test' }, searchConfig);
+        const result = buildElasticBody({ term: 'test' }, searchConfig, CHANNEL_ID);
         expect(result.query).toEqual({
             bool: {
+                filter: [CHANNEL_ID_TERM],
                 must: [
                     {
                         multi_match: {
@@ -25,41 +28,41 @@ describe('buildElasticBody()', () => {
     });
 
     it('facetValueIds', () => {
-        const result = buildElasticBody({ facetValueIds: ['1', '2'] }, searchConfig);
+        const result = buildElasticBody({ facetValueIds: ['1', '2'] }, searchConfig, CHANNEL_ID);
         expect(result.query).toEqual({
             bool: {
-                filter: [{ term: { facetValueIds: '1' } }, { term: { facetValueIds: '2' } }],
+                filter: [CHANNEL_ID_TERM, { term: { facetValueIds: '1' } }, { term: { facetValueIds: '2' } }],
             },
         });
     });
 
     it('collectionId', () => {
-        const result = buildElasticBody({ collectionId: '1' }, searchConfig);
+        const result = buildElasticBody({ collectionId: '1' }, searchConfig, CHANNEL_ID);
         expect(result.query).toEqual({
             bool: {
-                filter: [{ term: { collectionIds: '1' } }],
+                filter: [CHANNEL_ID_TERM, { term: { collectionIds: '1' } }],
             },
         });
     });
 
     it('paging', () => {
-        const result = buildElasticBody({ skip: 20, take: 10 }, searchConfig);
+        const result = buildElasticBody({ skip: 20, take: 10 }, searchConfig, CHANNEL_ID);
         expect(result).toEqual({
             from: 20,
             size: 10,
-            query: { bool: {} },
+            query: { bool: { filter: [CHANNEL_ID_TERM] } },
             sort: [],
         });
     });
 
     describe('sorting', () => {
         it('name', () => {
-            const result = buildElasticBody({ sort: { name: SortOrder.DESC } }, searchConfig);
-            expect(result.sort).toEqual([{ productName: { order: 'desc' } }]);
+            const result = buildElasticBody({ sort: { name: SortOrder.DESC } }, searchConfig, CHANNEL_ID);
+            expect(result.sort).toEqual([{ 'productName.keyword': { order: 'desc' } }]);
         });
 
         it('price', () => {
-            const result = buildElasticBody({ sort: { price: SortOrder.ASC } }, searchConfig);
+            const result = buildElasticBody({ sort: { price: SortOrder.ASC } }, searchConfig, CHANNEL_ID);
             expect(result.sort).toEqual([{ price: { order: 'asc' } }]);
         });
 
@@ -67,24 +70,25 @@ describe('buildElasticBody()', () => {
             const result = buildElasticBody(
                 { sort: { price: SortOrder.ASC }, groupByProduct: true },
                 searchConfig,
+                CHANNEL_ID,
             );
             expect(result.sort).toEqual([{ priceMin: { order: 'asc' } }]);
         });
     });
 
     it('enabledOnly true', () => {
-        const result = buildElasticBody({}, searchConfig, true);
+        const result = buildElasticBody({}, searchConfig, CHANNEL_ID, true);
         expect(result.query).toEqual({
             bool: {
-                filter: [{ term: { enabled: true } }],
+                filter: [CHANNEL_ID_TERM, { term: { enabled: true } }],
             },
         });
     });
 
     it('enabledOnly false', () => {
-        const result = buildElasticBody({}, searchConfig, false);
+        const result = buildElasticBody({}, searchConfig, CHANNEL_ID, false);
         expect(result.query).toEqual({
-            bool: {},
+            bool: { filter: [CHANNEL_ID_TERM] },
         });
     });
 
@@ -102,6 +106,7 @@ describe('buildElasticBody()', () => {
                 facetValueIds: ['6', '7'],
             },
             searchConfig,
+            CHANNEL_ID,
             true,
         );
 
@@ -120,6 +125,7 @@ describe('buildElasticBody()', () => {
                         },
                     ],
                     filter: [
+                        CHANNEL_ID_TERM,
                         { term: { facetValueIds: '6' } },
                         { term: { facetValueIds: '7' } },
                         { term: { collectionIds: '42' } },
@@ -127,14 +133,19 @@ describe('buildElasticBody()', () => {
                     ],
                 },
             },
-            sort: [{ productName: { order: 'desc' } }],
+            sort: [{ 'productName.keyword': { order: 'desc' } }],
         });
     });
 
     it('multiMatchType option', () => {
-        const result = buildElasticBody({ term: 'test' }, { ...searchConfig, multiMatchType: 'phrase' });
+        const result = buildElasticBody(
+            { term: 'test' },
+            { ...searchConfig, multiMatchType: 'phrase' },
+            CHANNEL_ID,
+        );
         expect(result.query).toEqual({
             bool: {
+                filter: [CHANNEL_ID_TERM],
                 must: [
                     {
                         multi_match: {
@@ -160,9 +171,10 @@ describe('buildElasticBody()', () => {
                 },
             },
         };
-        const result = buildElasticBody({ term: 'test' }, config);
+        const result = buildElasticBody({ term: 'test' }, config, CHANNEL_ID);
         expect(result.query).toEqual({
             bool: {
+                filter: [CHANNEL_ID_TERM],
                 must: [
                     {
                         multi_match: {
@@ -181,10 +193,12 @@ describe('buildElasticBody()', () => {
             const result = buildElasticBody(
                 { priceRange: { min: 500, max: 1500 }, groupByProduct: false },
                 searchConfig,
+                CHANNEL_ID,
             );
             expect(result.query).toEqual({
                 bool: {
                     filter: [
+                        CHANNEL_ID_TERM,
                         {
                             range: {
                                 price: {
@@ -202,10 +216,12 @@ describe('buildElasticBody()', () => {
             const result = buildElasticBody(
                 { priceRangeWithTax: { min: 500, max: 1500 }, groupByProduct: false },
                 searchConfig,
+                CHANNEL_ID,
             );
             expect(result.query).toEqual({
                 bool: {
                     filter: [
+                        CHANNEL_ID_TERM,
                         {
                             range: {
                                 priceWithTax: {
@@ -223,10 +239,12 @@ describe('buildElasticBody()', () => {
             const result = buildElasticBody(
                 { priceRange: { min: 500, max: 1500 }, groupByProduct: true },
                 searchConfig,
+                CHANNEL_ID,
             );
             expect(result.query).toEqual({
                 bool: {
                     filter: [
+                        CHANNEL_ID_TERM,
                         {
                             range: {
                                 priceMin: {
@@ -250,10 +268,12 @@ describe('buildElasticBody()', () => {
             const result = buildElasticBody(
                 { priceRangeWithTax: { min: 500, max: 1500 }, groupByProduct: true },
                 searchConfig,
+                CHANNEL_ID,
             );
             expect(result.query).toEqual({
                 bool: {
                     filter: [
+                        CHANNEL_ID_TERM,
                         {
                             range: {
                                 priceWithTaxMin: {
@@ -282,10 +302,12 @@ describe('buildElasticBody()', () => {
                     facetValueIds: ['5'],
                 },
                 searchConfig,
+                CHANNEL_ID,
             );
             expect(result.query).toEqual({
                 bool: {
                     filter: [
+                        CHANNEL_ID_TERM,
                         { term: { facetValueIds: '5' } },
                         { term: { collectionIds: '3' } },
                         {

+ 8 - 2
packages/elasticsearch-plugin/src/build-elastic-body.ts

@@ -1,5 +1,5 @@
 import { PriceRange, SortOrder } from '@vendure/common/lib/generated-types';
-import { DeepRequired } from '@vendure/core';
+import { DeepRequired, ID } from '@vendure/core';
 
 import { SearchConfig } from './options';
 import { ElasticSearchInput, SearchRequestBody } from './types';
@@ -10,6 +10,7 @@ import { ElasticSearchInput, SearchRequestBody } from './types';
 export function buildElasticBody(
     input: ElasticSearchInput,
     searchConfig: DeepRequired<SearchConfig>,
+    channelId: ID,
     enabledOnly: boolean = false,
 ): SearchRequestBody {
     const {
@@ -26,6 +27,9 @@ export function buildElasticBody(
     const query: any = {
         bool: {},
     };
+    ensureBoolFilterExists(query);
+    query.bool.filter.push({ term: { channelId } });
+
     if (term) {
         query.bool.must = [
             {
@@ -70,7 +74,9 @@ export function buildElasticBody(
     const sortArray = [];
     if (sort) {
         if (sort.name) {
-            sortArray.push({ productName: { order: sort.name === SortOrder.ASC ? 'asc' : 'desc' } });
+            sortArray.push({
+                'productName.keyword': { order: sort.name === SortOrder.ASC ? 'asc' : 'desc' },
+            });
         }
         if (sort.price) {
             const priceField = groupByProduct ? 'priceMin' : 'price';

+ 81 - 22
packages/elasticsearch-plugin/src/elasticsearch-index.service.ts

@@ -1,5 +1,4 @@
-import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
-import { ClientProxy } from '@nestjs/microservices';
+import { Injectable } from '@nestjs/common';
 import {
     ID,
     Job,
@@ -9,11 +8,21 @@ import {
     Product,
     ProductVariant,
     RequestContext,
+    WorkerMessage,
     WorkerService,
 } from '@vendure/core';
 
 import { ReindexMessageResponse } from './indexer.controller';
-import { ReindexMessage, UpdateProductOrVariantMessage, UpdateVariantsByIdMessage } from './types';
+import {
+    AssignProductToChannelMessage,
+    DeleteProductMessage,
+    DeleteVariantMessage,
+    ReindexMessage,
+    RemoveProductFromChannelMessage,
+    UpdateProductMessage,
+    UpdateVariantMessage,
+    UpdateVariantsByIdMessage,
+} from './types';
 
 @Injectable()
 export class ElasticsearchIndexService {
@@ -30,18 +39,80 @@ export class ElasticsearchIndexService {
         });
     }
 
+    updateProduct(ctx: RequestContext, product: Product) {
+        const data = { ctx, productId: product.id };
+        return this.createShortWorkerJob(new UpdateProductMessage(data), {
+            entity: 'Product',
+            id: product.id,
+        });
+    }
+
+    updateVariants(ctx: RequestContext, variants: ProductVariant[]) {
+        const variantIds = variants.map(v => v.id);
+        const data = { ctx, variantIds };
+        return this.createShortWorkerJob(new UpdateVariantMessage(data), {
+            entity: 'ProductVariant',
+            ids: variantIds,
+        });
+    }
+
+    deleteProduct(ctx: RequestContext, product: Product) {
+        const data = { ctx, productId: product.id };
+        return this.createShortWorkerJob(new DeleteProductMessage(data), {
+            entity: 'Product',
+            id: product.id,
+        });
+    }
+
+    deleteVariant(ctx: RequestContext, variants: ProductVariant[]) {
+        const variantIds = variants.map(v => v.id);
+        const data = { ctx, variantIds };
+        return this.createShortWorkerJob(new DeleteVariantMessage(data), {
+            entity: 'ProductVariant',
+            id: variantIds,
+        });
+    }
+
+    assignProductToChannel(ctx: RequestContext, product: Product, channelId: ID) {
+        const data = { ctx, productId: product.id, channelId };
+        return this.createShortWorkerJob(new AssignProductToChannelMessage(data), {
+            entity: 'Product',
+            id: product.id,
+        });
+    }
+
+    removeProductFromChannel(ctx: RequestContext, product: Product, channelId: ID) {
+        const data = { ctx, productId: product.id, channelId };
+        return this.createShortWorkerJob(new RemoveProductFromChannelMessage(data), {
+            entity: 'Product',
+            id: product.id,
+        });
+    }
+
+    updateVariantsById(ctx: RequestContext, ids: ID[]) {
+        return this.jobService.createJob({
+            name: 'update-variants',
+            metadata: {
+                variantIds: ids,
+            },
+            work: reporter => {
+                Logger.verbose(`sending UpdateVariantsByIdMessage`);
+                this.workerService
+                    .send(new UpdateVariantsByIdMessage({ ctx, ids }))
+                    .subscribe(this.createObserver(reporter));
+            },
+        });
+    }
+
     /**
-     * Updates the search index only for the affected entities.
+     * Creates a short-running job that does not expect progress updates.
      */
-    updateProductOrVariant(ctx: RequestContext, updatedEntity: Product | ProductVariant) {
+    private createShortWorkerJob<T extends WorkerMessage<any, any>>(message: T, metadata: any) {
         return this.jobService.createJob({
             name: 'update-index',
+            metadata,
             work: reporter => {
-                const data =
-                    updatedEntity instanceof Product
-                        ? { ctx, productId: updatedEntity.id }
-                        : { ctx, variantId: updatedEntity.id };
-                this.workerService.send(new UpdateProductOrVariantMessage(data)).subscribe({
+                this.workerService.send(message).subscribe({
                     complete: () => reporter.complete(true),
                     error: err => {
                         Logger.error(err);
@@ -52,18 +123,6 @@ export class ElasticsearchIndexService {
         });
     }
 
-    updateVariantsById(ctx: RequestContext, ids: ID[]) {
-        return this.jobService.createJob({
-            name: 'update-index',
-            work: reporter => {
-                Logger.verbose(`sending reindex message`);
-                this.workerService
-                    .send(new UpdateVariantsByIdMessage({ ctx, ids }))
-                    .subscribe(this.createObserver(reporter));
-            },
-        });
-    }
-
     private createObserver(reporter: JobReporter) {
         let total: number | undefined;
         let duration = 0;

+ 25 - 4
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -79,7 +79,12 @@ export class ElasticsearchService {
     ): Promise<Omit<ElasticSearchResponse, 'facetValues' | 'priceRange'>> {
         const { indexPrefix } = this.options;
         const { groupByProduct } = input;
-        const elasticSearchBody = buildElasticBody(input, this.options.searchConfig, enabledOnly);
+        const elasticSearchBody = buildElasticBody(
+            input,
+            this.options.searchConfig,
+            ctx.channelId,
+            enabledOnly,
+        );
         if (groupByProduct) {
             const { body }: { body: SearchResponseBody<ProductIndexItem> } = await this.client.search({
                 index: indexPrefix + PRODUCT_INDEX_NAME,
@@ -112,7 +117,12 @@ export class ElasticsearchService {
         enabledOnly: boolean = false,
     ): Promise<Array<{ facetValue: FacetValue; count: number }>> {
         const { indexPrefix } = this.options;
-        const elasticSearchBody = buildElasticBody(input, this.options.searchConfig, enabledOnly);
+        const elasticSearchBody = buildElasticBody(
+            input,
+            this.options.searchConfig,
+            ctx.channelId,
+            enabledOnly,
+        );
         elasticSearchBody.from = 0;
         elasticSearchBody.size = 0;
         elasticSearchBody.aggs = {
@@ -133,9 +143,10 @@ export class ElasticsearchService {
 
         const facetValues = await this.facetValueService.findByIds(buckets.map(b => b.key), ctx.languageCode);
         return facetValues.map((facetValue, index) => {
+            const bucket = buckets.find(b => b.key.toString() === facetValue.id.toString());
             return {
                 facetValue,
-                count: buckets[index].doc_count,
+                count: bucket ? bucket.doc_count : 0,
             };
         });
     }
@@ -143,7 +154,7 @@ export class ElasticsearchService {
     async priceRange(ctx: RequestContext, input: ElasticSearchInput): Promise<SearchPriceData> {
         const { indexPrefix, searchConfig } = this.options;
         const { groupByProduct } = input;
-        const elasticSearchBody = buildElasticBody(input, searchConfig, true);
+        const elasticSearchBody = buildElasticBody(input, searchConfig, ctx.channelId, true);
         elasticSearchBody.from = 0;
         elasticSearchBody.size = 0;
         elasticSearchBody.aggs = {
@@ -223,6 +234,15 @@ export class ElasticsearchService {
         return job;
     }
 
+    /**
+     * Rebuilds the full search index.
+     */
+    async updateAll(ctx: RequestContext): Promise<JobInfo> {
+        const job = this.elasticsearchIndexService.reindex(ctx);
+        job.start();
+        return job;
+    }
+
     private async createIndices(prefix: string) {
         try {
             const index = prefix + VARIANT_INDEX_NAME;
@@ -296,6 +316,7 @@ export class ElasticsearchService {
                 min: source.priceWithTaxMin,
                 max: source.priceWithTaxMax,
             },
+            channelIds: [],
             score: hit._score,
         };
         this.addCustomMappings(result, source, this.options.customProductMappings);

+ 3 - 1
packages/elasticsearch-plugin/src/graphql-schema-extensions.ts

@@ -4,6 +4,7 @@ import { DocumentNode } from 'graphql';
 import { ElasticsearchOptions } from './options';
 
 export function generateSchemaExtensions(options: ElasticsearchOptions): DocumentNode {
+    const customMappingTypes = generateCustomMappingTypes(options);
     return gql`
         extend type SearchResponse {
             prices: SearchResponsePriceData!
@@ -31,7 +32,7 @@ export function generateSchemaExtensions(options: ElasticsearchOptions): Documen
             max: Int!
         }
 
-        ${generateCustomMappingTypes(options)}
+        ${customMappingTypes ? customMappingTypes : ''}
     `;
 }
 
@@ -80,4 +81,5 @@ function generateCustomMappingTypes(options: ElasticsearchOptions): DocumentNode
             ${sdl}
         `;
     }
+    return;
 }

+ 210 - 114
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -29,12 +29,17 @@ import {
 } from './constants';
 import { ElasticsearchOptions } from './options';
 import {
+    AssignProductToChannelMessage,
     BulkOperation,
     BulkOperationDoc,
     BulkResponseBody,
+    DeleteProductMessage,
+    DeleteVariantMessage,
     ProductIndexItem,
     ReindexMessage,
-    UpdateProductOrVariantMessage,
+    RemoveProductFromChannelMessage,
+    UpdateProductMessage,
+    UpdateVariantMessage,
     UpdateVariantsByIdMessage,
     VariantIndexItem,
 } from './types';
@@ -44,6 +49,7 @@ export const variantRelations = [
     'product.featuredAsset',
     'product.facetValues',
     'product.facetValues.facet',
+    'product.channels',
     'featuredAsset',
     'facetValues',
     'facetValues.facet',
@@ -68,21 +74,96 @@ export class ElasticsearchIndexerController {
     ) {}
 
     /**
-     * Updates the search index only for the affected entities.
+     * Updates the search index only for the affected product.
      */
-    @MessagePattern(UpdateProductOrVariantMessage.pattern)
-    updateProductOrVariant({
+    @MessagePattern(UpdateProductMessage.pattern)
+    updateProduct({
         ctx: rawContext,
         productId,
-        variantId,
-    }: UpdateProductOrVariantMessage['data']): Observable<boolean> {
+    }: UpdateProductMessage['data']): Observable<UpdateProductMessage['response']> {
         const ctx = RequestContext.fromObject(rawContext);
         return defer(async () => {
-            if (productId) {
-                await this.updateProduct(ctx, productId);
-            } else if (variantId) {
-                await this.updateProductVariant(ctx, variantId);
-            }
+            await this.updateProductInternal(ctx, productId, ctx.channelId);
+            return true;
+        });
+    }
+
+    /**
+     * Updates the search index only for the affected product.
+     */
+    @MessagePattern(DeleteProductMessage.pattern)
+    deleteProduct({
+        ctx: rawContext,
+        productId,
+    }: DeleteProductMessage['data']): Observable<DeleteProductMessage['response']> {
+        const ctx = RequestContext.fromObject(rawContext);
+        return defer(async () => {
+            await this.deleteProductInternal(productId, ctx.channelId);
+            const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
+            await this.deleteVariantsInternal(variants.map(v => v.id), ctx.channelId);
+            return true;
+        });
+    }
+
+    /**
+     * Updates the search index only for the affected product.
+     */
+    @MessagePattern(AssignProductToChannelMessage.pattern)
+    assignProductsToChannel({
+        ctx: rawContext,
+        productId,
+        channelId,
+    }: AssignProductToChannelMessage['data']): Observable<AssignProductToChannelMessage['response']> {
+        const ctx = RequestContext.fromObject(rawContext);
+        return defer(async () => {
+            await this.updateProductInternal(ctx, productId, channelId);
+            const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
+            await this.updateVariantsInternal(ctx, variants.map(v => v.id), channelId);
+            return true;
+        });
+    }
+
+    /**
+     * Updates the search index only for the affected product.
+     */
+    @MessagePattern(RemoveProductFromChannelMessage.pattern)
+    removeProductFromChannel({
+        ctx: rawContext,
+        productId,
+        channelId,
+    }: RemoveProductFromChannelMessage['data']): Observable<RemoveProductFromChannelMessage['response']> {
+        const ctx = RequestContext.fromObject(rawContext);
+        return defer(async () => {
+            await this.deleteProductInternal(productId, channelId);
+            const variants = await this.productVariantService.getVariantsByProductId(ctx, productId);
+            await this.deleteVariantsInternal(variants.map(v => v.id), channelId);
+            return true;
+        });
+    }
+
+    /**
+     * Updates the search index only for the affected entities.
+     */
+    @MessagePattern(UpdateVariantMessage.pattern)
+    updateVariants({
+        ctx: rawContext,
+        variantIds,
+    }: UpdateVariantMessage['data']): Observable<UpdateVariantMessage['response']> {
+        const ctx = RequestContext.fromObject(rawContext);
+        return defer(async () => {
+            await this.updateVariantsInternal(ctx, variantIds, ctx.channelId);
+            return true;
+        });
+    }
+
+    @MessagePattern(DeleteVariantMessage.pattern)
+    private deleteVaiants({
+        ctx: rawContext,
+        variantIds,
+    }: DeleteVariantMessage['data']): Observable<DeleteVariantMessage['response']> {
+        const ctx = RequestContext.fromObject(rawContext);
+        return defer(async () => {
+            await this.deleteVariantsInternal(variantIds, ctx.channelId);
             return true;
         });
     }
@@ -110,35 +191,22 @@ export class ElasticsearchIndexerController {
                         const end = begin + batchSize;
                         Logger.verbose(`Updating ids from index ${begin} to ${end}`);
                         const batchIds = ids.slice(begin, end);
-
                         const variants = await this.getVariantsByIds(ctx, batchIds);
-
-                        const variantsToIndex: Array<BulkOperation | BulkOperationDoc<VariantIndexItem>> = [];
-                        const productsToIndex: Array<BulkOperation | BulkOperationDoc<ProductIndexItem>> = [];
-
-                        // tslint:disable-next-line:prefer-for-of
-                        for (let j = 0; j < variants.length; j++) {
-                            const variant = variants[j];
-                            variantsInProduct.push(variant);
-                            variantsToIndex.push({ update: { _id: variant.id.toString() } });
-                            variantsToIndex.push({ doc: this.createVariantIndexItem(variant) });
-
-                            const nextVariant = variants[j + 1];
-                            if (nextVariant && nextVariant.productId !== variant.productId) {
-                                productsToIndex.push({ update: { _id: variant.productId.toString() } });
-                                productsToIndex.push({ doc: this.createProductIndexItem(variantsInProduct) });
-                                variantsInProduct = [];
-                            }
-                        }
-                        await this.executeBulkOperations(
-                            VARIANT_INDEX_NAME,
-                            VARIANT_INDEX_TYPE,
-                            variantsToIndex,
-                        );
-                        await this.executeBulkOperations(
-                            PRODUCT_INDEX_NAME,
-                            PRODUCT_INDEX_TYPE,
-                            productsToIndex,
+                        variantsInProduct = await this.processVariantBatch(
+                            variants,
+                            variantsInProduct,
+                            (operations, variant) => {
+                                operations.push(
+                                    { update: { _id: this.getId(variant.id, ctx.channelId) } },
+                                    { doc: this.createVariantIndexItem(variant, ctx.channelId) },
+                                );
+                            },
+                            (operations, product, _variants) => {
+                                operations.push(
+                                    { update: { _id: this.getId(product.id, ctx.channelId) } },
+                                    { doc: this.createProductIndexItem(_variants, ctx.channelId) },
+                                );
+                            },
                         );
                         observer.next({
                             total: ids.length,
@@ -174,30 +242,28 @@ export class ElasticsearchIndexerController {
                 let variantsInProduct: ProductVariant[] = [];
 
                 for (let i = 0; i < batches; i++) {
-                    Logger.verbose(`Processing batch ${i + 1} of ${batches}`, loggerCtx);
-
                     const variants = await this.getBatch(ctx, qb, i);
-                    Logger.verbose(`ProductVariants count: ${variants.length}`);
-
-                    const variantsToIndex: Array<BulkOperation | VariantIndexItem> = [];
-                    const productsToIndex: Array<BulkOperation | ProductIndexItem> = [];
-
-                    // tslint:disable-next-line:prefer-for-of
-                    for (let j = 0; j < variants.length; j++) {
-                        const variant = variants[j];
-                        variantsInProduct.push(variant);
-                        variantsToIndex.push({ index: { _id: variant.id.toString() } });
-                        variantsToIndex.push(this.createVariantIndexItem(variant));
-
-                        const nextVariant = variants[j + 1];
-                        if (nextVariant && nextVariant.productId !== variant.productId) {
-                            productsToIndex.push({ index: { _id: variant.productId.toString() } });
-                            productsToIndex.push(this.createProductIndexItem(variantsInProduct) as any);
-                            variantsInProduct = [];
-                        }
-                    }
-                    await this.executeBulkOperations(VARIANT_INDEX_NAME, VARIANT_INDEX_TYPE, variantsToIndex);
-                    await this.executeBulkOperations(PRODUCT_INDEX_NAME, PRODUCT_INDEX_TYPE, productsToIndex);
+
+                    Logger.verbose(
+                        `Processing batch ${i + 1} of ${batches}. ProductVariants count: ${variants.length}`,
+                        loggerCtx,
+                    );
+                    variantsInProduct = await this.processVariantBatch(
+                        variants,
+                        variantsInProduct,
+                        (operations, variant) => {
+                            operations.push(
+                                { index: { _id: this.getId(variant.id, ctx.channelId) } },
+                                this.createVariantIndexItem(variant, ctx.channelId),
+                            );
+                        },
+                        (operations, product, _variants) => {
+                            operations.push(
+                                { index: { _id: this.getId(product.id, ctx.channelId) } },
+                                this.createProductIndexItem(_variants, ctx.channelId),
+                            );
+                        },
+                    );
                     observer.next({
                         total: count,
                         completed: Math.min((i + 1) * batchSize, count),
@@ -215,34 +281,63 @@ export class ElasticsearchIndexerController {
         });
     }
 
-    private async updateProductVariant(ctx: RequestContext, variantId: ID) {
+    private async processVariantBatch(
+        variants: ProductVariant[],
+        variantsInProduct: ProductVariant[],
+        processVariants: (
+            operations: Array<BulkOperation | BulkOperationDoc<VariantIndexItem> | VariantIndexItem>,
+            variant: ProductVariant,
+        ) => void,
+        processProducts: (
+            operations: Array<BulkOperation | BulkOperationDoc<ProductIndexItem> | ProductIndexItem>,
+            product: Product,
+            variants: ProductVariant[],
+        ) => void,
+    ) {
+        const variantsToIndex: Array<BulkOperation | VariantIndexItem> = [];
+        const productsToIndex: Array<BulkOperation | ProductIndexItem> = [];
+        const productIdsIndexed = new Set<ID>();
+        // tslint:disable-next-line:prefer-for-of
+        for (let j = 0; j < variants.length; j++) {
+            const variant = variants[j];
+            variantsInProduct.push(variant);
+            processVariants(variantsToIndex, variant);
+            const nextVariant = variants[j + 1];
+            const nextVariantIsNewProduct = nextVariant && nextVariant.productId !== variant.productId;
+            const thisVariantIsLastAndProductNotAdded =
+                !nextVariant && !productIdsIndexed.has(variant.productId);
+            if (nextVariantIsNewProduct || thisVariantIsLastAndProductNotAdded) {
+                processProducts(productsToIndex, variant.product, variantsInProduct);
+                variantsInProduct = [];
+                productIdsIndexed.add(variant.productId);
+            }
+        }
+        await this.executeBulkOperations(VARIANT_INDEX_NAME, VARIANT_INDEX_TYPE, variantsToIndex);
+        await this.executeBulkOperations(PRODUCT_INDEX_NAME, PRODUCT_INDEX_TYPE, productsToIndex);
+        return variantsInProduct;
+    }
+
+    private async updateVariantsInternal(ctx: RequestContext, variantIds: ID[], channelId: ID) {
         let updatedVariants: ProductVariant[] = [];
-        let removedVariantId: ID | undefined;
 
-        const productVariant = await this.connection.getRepository(ProductVariant).findOne(variantId, {
+        const productVariants = await this.connection.getRepository(ProductVariant).findByIds(variantIds, {
             relations: variantRelations,
         });
-        if (productVariant) {
-            if (productVariant.deletedAt) {
-                removedVariantId = variantId;
-            } else {
-                updatedVariants = this.hydrateVariants(ctx, [productVariant]);
-            }
-        }
+        updatedVariants = this.hydrateVariants(ctx, productVariants);
 
         if (updatedVariants.length) {
             // When ProductVariants change, we need to update the corresponding Product index
             // since e.g. price changes must be reflected on the Product level too.
             const productIdsOfVariants = unique(updatedVariants.map(v => v.productId));
             for (const variantProductId of productIdsOfVariants) {
-                await this.updateProduct(ctx, variantProductId);
+                await this.updateProductInternal(ctx, variantProductId, channelId);
             }
             const operations = updatedVariants.reduce(
                 (ops, variant) => {
                     return [
                         ...ops,
-                        { update: { _id: variant.id.toString() } },
-                        { doc: this.createVariantIndexItem(variant), doc_as_upsert: true },
+                        { update: { _id: this.getId(variant.id, channelId) } },
+                        { doc: this.createVariantIndexItem(variant, channelId), doc_as_upsert: true },
                     ];
                 },
                 [] as Array<BulkOperation | BulkOperationDoc<VariantIndexItem>>,
@@ -250,57 +345,47 @@ export class ElasticsearchIndexerController {
             Logger.verbose(`Updating ${updatedVariants.length} ProductVariants`, loggerCtx);
             await this.executeBulkOperations(VARIANT_INDEX_NAME, VARIANT_INDEX_TYPE, operations);
         }
-        if (removedVariantId) {
-            Logger.verbose(`Deleting 1 ProductVariant (${removedVariantId})`, loggerCtx);
-            const operations: BulkOperation[] = [{ delete: { _id: removedVariantId.toString() } }];
-            await this.executeBulkOperations(VARIANT_INDEX_NAME, VARIANT_INDEX_TYPE, operations);
-        }
     }
 
-    private async updateProduct(ctx: RequestContext, productId: ID) {
+    private async updateProductInternal(ctx: RequestContext, productId: ID, channelId: ID) {
         let updatedProductVariants: ProductVariant[] = [];
-        let removedProductId: ID | undefined;
-        let removedVariantIds: ID[] = [];
         const product = await this.connection.getRepository(Product).findOne(productId, {
             relations: ['variants'],
         });
         if (product) {
-            if (product.deletedAt) {
-                removedProductId = productId;
-                removedVariantIds = product.variants.map(v => v.id);
-            } else {
-                updatedProductVariants = await this.connection
-                    .getRepository(ProductVariant)
-                    .findByIds(product.variants.map(v => v.id), {
-                        relations: variantRelations,
-                    });
+            updatedProductVariants = await this.connection
+                .getRepository(ProductVariant)
+                .findByIds(product.variants.map(v => v.id), {
+                    relations: variantRelations,
+                });
+            if (product.enabled === false) {
+                updatedProductVariants.forEach(v => (v.enabled = false));
             }
         }
         if (updatedProductVariants.length) {
             Logger.verbose(`Updating 1 Product (${productId})`, loggerCtx);
             updatedProductVariants = this.hydrateVariants(ctx, updatedProductVariants);
-            const updatedProductIndexItem = this.createProductIndexItem(updatedProductVariants);
+            const updatedProductIndexItem = this.createProductIndexItem(updatedProductVariants, channelId);
             const operations: [BulkOperation, BulkOperationDoc<ProductIndexItem>] = [
-                { update: { _id: updatedProductIndexItem.productId.toString() } },
+                { update: { _id: this.getId(updatedProductIndexItem.productId, channelId) } },
                 { doc: updatedProductIndexItem, doc_as_upsert: true },
             ];
             await this.executeBulkOperations(PRODUCT_INDEX_NAME, PRODUCT_INDEX_TYPE, operations);
         }
-        if (removedVariantIds.length) {
-            const operations = removedVariantIds.reduce(
-                (ops, id) => {
-                    Logger.verbose(`Deleting 1 ProductVariant (${id})`, loggerCtx);
-                    return [...ops, { delete: { _id: id.toString() } }];
-                },
-                [] as BulkOperation[],
-            );
-            await this.executeBulkOperations(VARIANT_INDEX_NAME, VARIANT_INDEX_TYPE, operations);
-        }
-        if (removedProductId) {
-            Logger.verbose(`Deleting 1 Product (${removedProductId})`, loggerCtx);
-            const operations: BulkOperation[] = [{ delete: { _id: removedProductId.toString() } }];
-            await this.executeBulkOperations(PRODUCT_INDEX_NAME, PRODUCT_INDEX_TYPE, operations);
-        }
+    }
+
+    private async deleteProductInternal(productId: ID, channelId: ID) {
+        Logger.verbose(`Deleting 1 Product (${productId})`, loggerCtx);
+        const operations: BulkOperation[] = [{ delete: { _id: this.getId(productId, channelId) } }];
+        await this.executeBulkOperations(PRODUCT_INDEX_NAME, PRODUCT_INDEX_TYPE, operations);
+    }
+
+    private async deleteVariantsInternal(variantIds: ID[], channelId: ID) {
+        Logger.verbose(`Deleting ${variantIds.length} ProductVariants`, loggerCtx);
+        const operations: BulkOperation[] = variantIds.map(id => ({
+            delete: { _id: this.getId(id, channelId) },
+        }));
+        await this.executeBulkOperations(VARIANT_INDEX_NAME, VARIANT_INDEX_TYPE, operations);
     }
 
     private async executeBulkOperations(
@@ -334,7 +419,10 @@ export class ElasticsearchIndexerController {
                     }
                 });
             } else {
-                Logger.verbose(`Executed ${body.items.length} bulk operations on index [${fullIndexName}]`);
+                Logger.verbose(
+                    `Executed ${body.items.length} bulk operations on index [${fullIndexName}]`,
+                    loggerCtx,
+                );
             }
             return body;
         } catch (e) {
@@ -386,14 +474,15 @@ export class ElasticsearchIndexerController {
             .map(v => translateDeep(v, ctx.languageCode, ['product']));
     }
 
-    private createVariantIndexItem(v: ProductVariant): VariantIndexItem {
+    private createVariantIndexItem(v: ProductVariant, channelId: ID): VariantIndexItem {
         const item: VariantIndexItem = {
+            channelId,
+            productVariantId: v.id as string,
             sku: v.sku,
             slug: v.product.slug,
             productId: v.product.id as string,
             productName: v.product.name,
             productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '',
-            productVariantId: v.id as string,
             productVariantName: v.name,
             productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '',
             price: v.price,
@@ -401,6 +490,7 @@ export class ElasticsearchIndexerController {
             currencyCode: v.currencyCode,
             description: v.product.description,
             facetIds: this.getFacetIds([v]),
+            channelIds: v.product.channels.map(c => c.id as string),
             facetValueIds: this.getFacetValueIds([v]),
             collectionIds: v.collections.map(c => c.id.toString()),
             enabled: v.enabled && v.product.enabled,
@@ -412,11 +502,12 @@ export class ElasticsearchIndexerController {
         return item;
     }
 
-    private createProductIndexItem(variants: ProductVariant[]): ProductIndexItem {
+    private createProductIndexItem(variants: ProductVariant[], channelId: ID): ProductIndexItem {
         const first = variants[0];
         const prices = variants.map(v => v.price);
         const pricesWithTax = variants.map(v => v.priceWithTax);
         const item: ProductIndexItem = {
+            channelId,
             sku: variants.map(v => v.sku),
             slug: variants.map(v => v.product.slug),
             productId: first.product.id,
@@ -434,7 +525,8 @@ export class ElasticsearchIndexerController {
             facetIds: this.getFacetIds(variants),
             facetValueIds: this.getFacetValueIds(variants),
             collectionIds: variants.reduce((ids, v) => [...ids, ...v.collections.map(c => c.id)], [] as ID[]),
-            enabled: first.product.enabled,
+            channelIds: first.product.channels.map(c => c.id as string),
+            enabled: variants.some(v => v.enabled),
         };
 
         const customMappings = Object.entries(this.options.customProductMappings);
@@ -463,4 +555,8 @@ export class ElasticsearchIndexerController {
         const productFacetValueIds = variants[0].product.facetValues.map(facetValueIds);
         return unique([...variantFacetValueIds, ...productFacetValueIds]);
     }
+
+    private getId(entityId: ID, channelId: ID): string {
+        return `${channelId.toString()}__${entityId.toString()}`;
+    }
 }

+ 28 - 5
packages/elasticsearch-plugin/src/plugin.ts

@@ -1,6 +1,5 @@
 import { Client } from '@elastic/elasticsearch';
 import {
-    CatalogModificationEvent,
     CollectionModificationEvent,
     DeepRequired,
     EventBus,
@@ -11,7 +10,10 @@ import {
     OnVendureClose,
     PluginCommonModule,
     Product,
+    ProductChannelEvent,
+    ProductEvent,
     ProductVariant,
+    ProductVariantEvent,
     TaxRateModificationEvent,
     Type,
     VendurePlugin,
@@ -246,9 +248,30 @@ export class ElasticsearchPlugin implements OnVendureBootstrap, OnVendureClose {
 
         await this.elasticsearchService.createIndicesIfNotExists();
 
-        this.eventBus.ofType(CatalogModificationEvent).subscribe(event => {
-            if (event.entity instanceof Product || event.entity instanceof ProductVariant) {
-                return this.elasticsearchIndexService.updateProductOrVariant(event.ctx, event.entity).start();
+        this.eventBus.ofType(ProductEvent).subscribe(event => {
+            if (event.type === 'deleted') {
+                return this.elasticsearchIndexService.deleteProduct(event.ctx, event.product).start();
+            } else {
+                return this.elasticsearchIndexService.updateProduct(event.ctx, event.product).start();
+            }
+        });
+        this.eventBus.ofType(ProductVariantEvent).subscribe(event => {
+            if (event.type === 'deleted') {
+                return this.elasticsearchIndexService.deleteVariant(event.ctx, event.variants).start();
+            } else {
+                return this.elasticsearchIndexService.updateVariants(event.ctx, event.variants).start();
+            }
+        });
+
+        this.eventBus.ofType(ProductChannelEvent).subscribe(event => {
+            if (event.type === 'assigned') {
+                return this.elasticsearchIndexService
+                    .assignProductToChannel(event.ctx, event.product, event.channelId)
+                    .start();
+            } else {
+                return this.elasticsearchIndexService
+                    .removeProductFromChannel(event.ctx, event.product, event.channelId)
+                    .start();
             }
         });
 
@@ -271,7 +294,7 @@ export class ElasticsearchPlugin implements OnVendureBootstrap, OnVendureClose {
         this.eventBus.ofType(TaxRateModificationEvent).subscribe(event => {
             const defaultTaxZone = event.ctx.channel.defaultTaxZone;
             if (defaultTaxZone && idsAreEqual(defaultTaxZone.id, event.taxRate.zone.id)) {
-                return this.elasticsearchService.reindex(event.ctx);
+                return this.elasticsearchService.updateAll(event.ctx);
             }
         });
     }

+ 33 - 5
packages/elasticsearch-plugin/src/types.ts

@@ -30,6 +30,7 @@ export type PriceRangeBucket = {
 };
 
 export type VariantIndexItem = Omit<SearchResult, 'score' | 'price' | 'priceWithTax'> & {
+    channelId: ID;
     price: number;
     priceWithTax: number;
     [customMapping: string]: any;
@@ -38,6 +39,7 @@ export type ProductIndexItem = {
     sku: string[];
     slug: string[];
     productId: ID;
+    channelId: ID;
     productName: string[];
     productPreview: string;
     productVariantId: ID[];
@@ -48,6 +50,7 @@ export type ProductIndexItem = {
     facetIds: ID[];
     facetValueIds: ID[];
     collectionIds: ID[];
+    channelIds: ID[];
     enabled: boolean;
     priceMin: number;
     priceMax: number;
@@ -132,10 +135,14 @@ export interface ReindexMessageResponse {
     duration: number;
 }
 
-export type UpdateProductOrVariantMessageData = {
+export type UpdateProductMessageData = {
     ctx: RequestContext;
-    productId?: ID;
-    variantId?: ID;
+    productId: ID;
+};
+
+export type UpdateVariantMessageData = {
+    ctx: RequestContext;
+    variantIds: ID[];
 };
 
 export interface UpdateVariantsByIdMessageData {
@@ -143,11 +150,26 @@ export interface UpdateVariantsByIdMessageData {
     ids: ID[];
 }
 
+export interface ProductChannelMessageData {
+    ctx: RequestContext;
+    productId: ID;
+    channelId: ID;
+}
+
 export class ReindexMessage extends WorkerMessage<{ ctx: RequestContext }, ReindexMessageResponse> {
     static readonly pattern = 'Reindex';
 }
-export class UpdateProductOrVariantMessage extends WorkerMessage<UpdateProductOrVariantMessageData, boolean> {
-    static readonly pattern = 'UpdateProductOrVariant';
+export class UpdateVariantMessage extends WorkerMessage<UpdateVariantMessageData, boolean> {
+    static readonly pattern = 'UpdateProduct';
+}
+export class UpdateProductMessage extends WorkerMessage<UpdateProductMessageData, boolean> {
+    static readonly pattern = 'UpdateVariant';
+}
+export class DeleteVariantMessage extends WorkerMessage<UpdateVariantMessageData, boolean> {
+    static readonly pattern = 'DeleteProduct';
+}
+export class DeleteProductMessage extends WorkerMessage<UpdateProductMessageData, boolean> {
+    static readonly pattern = 'DeleteVariant';
 }
 export class UpdateVariantsByIdMessage extends WorkerMessage<
     UpdateVariantsByIdMessageData,
@@ -155,6 +177,12 @@ export class UpdateVariantsByIdMessage extends WorkerMessage<
 > {
     static readonly pattern = 'UpdateVariantsById';
 }
+export class AssignProductToChannelMessage extends WorkerMessage<ProductChannelMessageData, boolean> {
+    static readonly pattern = 'AssignProductToChannel';
+}
+export class RemoveProductFromChannelMessage extends WorkerMessage<ProductChannelMessageData, boolean> {
+    static readonly pattern = 'RemoveProductFromChannel';
+}
 
 type Maybe<T> = T | null | undefined;
 type CustomMappingDefinition<Args extends any[], T extends string, R> = {

+ 7 - 0
scripts/codegen/generate-graphql-types.ts

@@ -13,6 +13,7 @@ const E2E_ADMIN_QUERY_FILES = path.join(__dirname, '../../packages/core/e2e/**/!
 const E2E_SHOP_QUERY_FILES = [
     path.join(__dirname, '../../packages/core/e2e/graphql/shop-definitions.ts'),
 ];
+const E2E_ELASTICSEARCH_PLUGIN_QUERY_FILES = path.join(__dirname, '../../packages/elasticsearch-plugin/e2e/**/*.ts');
 const ADMIN_SCHEMA_OUTPUT_FILE = path.join(__dirname, '../../schema-admin.json');
 const SHOP_SCHEMA_OUTPUT_FILE = path.join(__dirname, '../../schema-shop.json');
 
@@ -63,6 +64,12 @@ Promise.all([
                     plugins: clientPlugins,
                     config,
                 },
+                [path.join(__dirname, '../../packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts')]: {
+                    schema: [ADMIN_SCHEMA_OUTPUT_FILE],
+                    documents: E2E_ELASTICSEARCH_PLUGIN_QUERY_FILES,
+                    plugins: clientPlugins,
+                    config,
+                },
                 [path.join(__dirname, '../../packages/admin-ui/src/app/common/generated-types.ts')]: {
                     schema: [ADMIN_SCHEMA_OUTPUT_FILE, path.join(__dirname, 'client-schema.ts')],
                     documents: CLIENT_QUERY_FILES,