Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
94ddb75476

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

@@ -7,7 +7,7 @@ weight: 0
 
 
 ## Requirements
 ## Requirements
  
  
-* [Node.js](https://nodejs.org/en/) **v10.13.0** or above
+* [Node.js](https://nodejs.org/en/) **v12** or above, with support for **even-numbered Node.js versions**.
 * If you want to use MySQL, MariaDB, or Postgres as your data store, then you'll need an instance available locally. However, if you are just testing out Vendure, we recommend using SQLite, which has no external requirements.
 * If you want to use MySQL, MariaDB, or Postgres as your data store, then you'll need an instance available locally. However, if you are just testing out Vendure, we recommend using SQLite, which has no external requirements.
 * For Windows users: make sure you have **[windows build tools](https://www.npmjs.com/package/windows-build-tools) installed**
 * For Windows users: make sure you have **[windows build tools](https://www.npmjs.com/package/windows-build-tools) installed**
   * `npm install --global --production windows-build-tools`
   * `npm install --global --production windows-build-tools`

+ 1 - 0
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -137,6 +137,7 @@ export class ChannelDetailComponent
                     const input = {
                     const input = {
                         id: channel.id,
                         id: channel.id,
                         code: formValue.code,
                         code: formValue.code,
+                        token: formValue.token,
                         pricesIncludeTax: formValue.pricesIncludeTax,
                         pricesIncludeTax: formValue.pricesIncludeTax,
                         currencyCode: formValue.currencyCode,
                         currencyCode: formValue.currencyCode,
                         defaultShippingZoneId: formValue.defaultShippingZoneId,
                         defaultShippingZoneId: formValue.defaultShippingZoneId,

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

@@ -2,6 +2,7 @@
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import {
 import {
     DefaultJobQueuePlugin,
     DefaultJobQueuePlugin,
+    DefaultLogger,
     DefaultSearchPlugin,
     DefaultSearchPlugin,
     facetValueCollectionFilter,
     facetValueCollectionFilter,
     mergeConfig,
     mergeConfig,
@@ -72,6 +73,7 @@ jest.setTimeout(10000);
 describe('Default search plugin', () => {
 describe('Default search plugin', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
         mergeConfig(testConfig, {
+            logger: new DefaultLogger(),
             plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin],
             plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin],
         }),
         }),
     );
     );
@@ -1282,6 +1284,72 @@ describe('Default search plugin', () => {
                 });
                 });
                 expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
                 expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
             });
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/896
+            it('removing from channel with multiple languages', async () => {
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+
+                await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                    input: {
+                        id: 'T_4',
+                        translations: [
+                            {
+                                languageCode: LanguageCode.en,
+                                name: 'product en',
+                                slug: 'product-en',
+                                description: 'en',
+                            },
+                            {
+                                languageCode: LanguageCode.de,
+                                name: 'product de',
+                                slug: 'product-de',
+                                description: 'de',
+                            },
+                        ],
+                    },
+                });
+
+                await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
+                    ASSIGN_PRODUCT_TO_CHANNEL,
+                    {
+                        input: { channelId: secondChannel.id, productIds: ['T_4'] },
+                    },
+                );
+                await awaitRunningJobs(adminClient);
+
+                async function searchSecondChannelForDEProduct() {
+                    adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+                    const { search } = await adminClient.query<
+                        SearchProductsShop.Query,
+                        SearchProductsShop.Variables
+                    >(
+                        SEARCH_PRODUCTS,
+                        {
+                            input: { term: 'product', groupByProduct: true },
+                        },
+                        { languageCode: LanguageCode.de },
+                    );
+                    return search;
+                }
+
+                const search1 = await searchSecondChannelForDEProduct();
+                expect(search1.items.map(i => i.productName)).toEqual(['product de']);
+
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                const { removeProductsFromChannel } = await adminClient.query<
+                    RemoveProductsFromChannel.Mutation,
+                    RemoveProductsFromChannel.Variables
+                >(REMOVE_PRODUCT_FROM_CHANNEL, {
+                    input: {
+                        productIds: ['T_4'],
+                        channelId: secondChannel.id,
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+
+                const search2 = await searchSecondChannelForDEProduct();
+                expect(search2.items.map(i => i.productName)).toEqual([]);
+            });
         });
         });
 
 
         describe('multiple language handling', () => {
         describe('multiple language handling', () => {

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

@@ -1,7 +1,5 @@
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 
 
-import { ERROR_RESULT_FRAGMENT } from '../../../admin-ui/src/lib/core/src/data/definitions/shared-definitions';
-
 export const TEST_ORDER_FRAGMENT = gql`
 export const TEST_ORDER_FRAGMENT = gql`
     fragment TestOrderFragment on Order {
     fragment TestOrderFragment on Order {
         id
         id

+ 1 - 4
packages/core/package.json

@@ -58,9 +58,8 @@
     "express": "^4.17.1",
     "express": "^4.17.1",
     "fs-extra": "^9.0.1",
     "fs-extra": "^9.0.1",
     "graphql": "15.5.0",
     "graphql": "15.5.0",
-    "graphql-iso-date": "^3.6.1",
+    "graphql-scalars": "^1.10.0",
     "graphql-tag": "^2.12.4",
     "graphql-tag": "^2.12.4",
-    "graphql-type-json": "^0.3.2",
     "graphql-upload": "^12.0.0",
     "graphql-upload": "^12.0.0",
     "http-proxy-middleware": "^1.0.5",
     "http-proxy-middleware": "^1.0.5",
     "i18next": "^19.8.1",
     "i18next": "^19.8.1",
@@ -82,8 +81,6 @@
     "@types/csv-parse": "^1.2.2",
     "@types/csv-parse": "^1.2.2",
     "@types/express": "^4.17.8",
     "@types/express": "^4.17.8",
     "@types/faker": "^4.1.7",
     "@types/faker": "^4.1.7",
-    "@types/graphql-iso-date": "^3.4.0",
-    "@types/graphql-type-json": "^0.3.2",
     "@types/graphql-upload": "^8.0.4",
     "@types/graphql-upload": "^8.0.4",
     "@types/gulp": "^4.0.7",
     "@types/gulp": "^4.0.7",
     "@types/mime-types": "^2.1.0",
     "@types/mime-types": "^2.1.0",

+ 1 - 3
packages/core/src/api/config/generate-resolvers.ts

@@ -1,8 +1,7 @@
 import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { IFieldResolver, IResolvers } from 'apollo-server-express';
 import { IFieldResolver, IResolvers } from 'apollo-server-express';
 import { GraphQLSchema } from 'graphql';
 import { GraphQLSchema } from 'graphql';
-import { GraphQLDateTime } from 'graphql-iso-date';
-import GraphQLJSON from 'graphql-type-json';
+import { GraphQLDateTime, GraphQLJSON } from 'graphql-scalars';
 import { GraphQLUpload } from 'graphql-upload';
 import { GraphQLUpload } from 'graphql-upload';
 
 
 import { REQUEST_CONTEXT_KEY } from '../../common/constants';
 import { REQUEST_CONTEXT_KEY } from '../../common/constants';
@@ -14,7 +13,6 @@ import { shopErrorOperationTypeResolvers } from '../../common/error/generated-gr
 import { Translatable } from '../../common/types/locale-types';
 import { Translatable } from '../../common/types/locale-types';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { CustomFieldConfig, RelationCustomFieldConfig } from '../../config/custom-field/custom-field-types';
 import { CustomFieldConfig, RelationCustomFieldConfig } from '../../config/custom-field/custom-field-types';
-import { CustomFieldRelationService } from '../../service/helpers/custom-field-relation/custom-field-relation.service';
 import { CustomFieldRelationResolverService } from '../common/custom-field-relation-resolver.service';
 import { CustomFieldRelationResolverService } from '../common/custom-field-relation-resolver.service';
 import { ApiType } from '../common/get-api-type';
 import { ApiType } from '../common/get-api-type';
 import { RequestContext } from '../common/request-context';
 import { RequestContext } from '../common/request-context';

+ 3 - 1
packages/core/src/bootstrap.ts

@@ -91,7 +91,9 @@ export async function bootstrapWorker(
 ): Promise<{ app: INestApplicationContext; startJobQueue: () => Promise<void> }> {
 ): Promise<{ app: INestApplicationContext; startJobQueue: () => Promise<void> }> {
     const vendureConfig = await preBootstrapConfig(userConfig);
     const vendureConfig = await preBootstrapConfig(userConfig);
     const config = disableSynchronize(vendureConfig);
     const config = disableSynchronize(vendureConfig);
-    (config.logger as any).setDefaultContext('Vendure Worker');
+    if (config.logger instanceof DefaultLogger) {
+        config.logger.setDefaultContext('Vendure Worker');
+    }
     Logger.useLogger(config.logger);
     Logger.useLogger(config.logger);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
 
 

+ 2 - 0
packages/core/src/common/ttl-cache.ts

@@ -1,6 +1,8 @@
 /**
 /**
  * An in-memory cache with a configurable TTL (time to live) which means cache items
  * An in-memory cache with a configurable TTL (time to live) which means cache items
  * expire after they have been in the cache longer than that time.
  * expire after they have been in the cache longer than that time.
+ *
+ * The `ttl` config option is in milliseconds. Defaults to 30 seconds TTL and a cache size of 1000.
  */
  */
 export class TtlCache<K, V> {
 export class TtlCache<K, V> {
     private cache = new Map<K, { value: V; expires: number }>();
     private cache = new Map<K, { value: V; expires: number }>();

+ 32 - 10
packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts

@@ -12,6 +12,7 @@ import { asyncObservable, idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { Logger } from '../../../config/logger/vendure-logger';
 import { Logger } from '../../../config/logger/vendure-logger';
 import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
 import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
+import { ProductVariantTranslation } from '../../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { Product } from '../../../entity/product/product.entity';
 import { Product } from '../../../entity/product/product.entity';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
@@ -151,10 +152,15 @@ export class IndexerController {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
         const variants = await this.connection.getRepository(ProductVariant).findByIds(data.variantIds);
         const variants = await this.connection.getRepository(ProductVariant).findByIds(data.variantIds);
         if (variants.length) {
         if (variants.length) {
+            const languageVariants = unique([
+                ...variants
+                    .reduce((vt, v) => [...vt, ...v.translations], [] as Array<Translation<ProductVariant>>)
+                    .map(t => t.languageCode),
+            ]);
             await this.removeSearchIndexItems(
             await this.removeSearchIndexItems(
-                ctx.languageCode,
                 ctx.channelId,
                 ctx.channelId,
                 variants.map(v => v.id),
                 variants.map(v => v.id),
+                languageVariants,
             );
             );
         }
         }
         return true;
         return true;
@@ -177,15 +183,19 @@ export class IndexerController {
 
 
     async removeVariantFromChannel(data: VariantChannelMessageData): Promise<boolean> {
     async removeVariantFromChannel(data: VariantChannelMessageData): Promise<boolean> {
         const ctx = RequestContext.deserialize(data.ctx);
         const ctx = RequestContext.deserialize(data.ctx);
-        await this.removeSearchIndexItems(ctx.languageCode, data.channelId, [data.productVariantId]);
+        const variant = await this.connection.getRepository(ProductVariant).findOne(data.productVariantId);
+        const languageVariants = variant?.translations.map(t => t.languageCode) ?? [];
+        await this.removeSearchIndexItems(data.channelId, [data.productVariantId], languageVariants);
         return true;
         return true;
     }
     }
 
 
     async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
     async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
         const id = data.asset.id;
         const id = data.asset.id;
+
         function getFocalPoint(point?: { x: number; y: number }) {
         function getFocalPoint(point?: { x: number; y: number }) {
             return point && point.x && point.y ? point : null;
             return point && point.x && point.y ? point : null;
         }
         }
+
         const focalPoint = getFocalPoint(data.asset.focalPoint);
         const focalPoint = getFocalPoint(data.asset.focalPoint);
         await this.connection
         await this.connection
             .getRepository(SearchIndexItem)
             .getRepository(SearchIndexItem)
@@ -266,9 +276,16 @@ export class IndexerController {
             relations: ['variants'],
             relations: ['variants'],
         });
         });
         if (product) {
         if (product) {
+            const languageVariants = unique([
+                ...product.translations.map(t => t.languageCode),
+                ...product.variants
+                    .reduce((vt, v) => [...vt, ...v.translations], [] as Array<Translation<ProductVariant>>)
+                    .map(t => t.languageCode),
+            ]);
+
             const removedVariantIds = product.variants.map(v => v.id);
             const removedVariantIds = product.variants.map(v => v.id);
             if (removedVariantIds.length) {
             if (removedVariantIds.length) {
-                await this.removeSearchIndexItems(ctx.languageCode, channelId, removedVariantIds);
+                await this.removeSearchIndexItems(channelId, removedVariantIds, languageVariants);
             }
             }
         }
         }
         return true;
         return true;
@@ -436,13 +453,18 @@ export class IndexerController {
     /**
     /**
      * Remove items from the search index
      * Remove items from the search index
      */
      */
-    private async removeSearchIndexItems(languageCode: LanguageCode, channelId: ID, variantIds: ID[]) {
-        const compositeKeys = variantIds.map(id => ({
-            productVariantId: id,
-            channelId,
-            languageCode,
-        })) as any[];
-        await this.queue.push(() => this.connection.getRepository(SearchIndexItem).delete(compositeKeys));
+    private async removeSearchIndexItems(channelId: ID, variantIds: ID[], languageCodes: LanguageCode[]) {
+        const keys: Array<Partial<SearchIndexItem>> = [];
+        for (const productVariantId of variantIds) {
+            for (const languageCode of languageCodes) {
+                keys.push({
+                    productVariantId,
+                    channelId,
+                    languageCode,
+                });
+            }
+        }
+        await this.queue.push(() => this.connection.getRepository(SearchIndexItem).delete(keys as any));
     }
     }
 
 
     /**
     /**

+ 5 - 2
packages/core/src/service/services/channel.service.ts

@@ -88,10 +88,13 @@ export class ChannelService {
         entityType: Type<T>,
         entityType: Type<T>,
         entityId: ID,
         entityId: ID,
         channelIds: ID[],
         channelIds: ID[],
-    ): Promise<T> {
-        const entity = await this.connection.getEntityOrThrow(ctx, entityType, entityId, {
+    ): Promise<T | undefined> {
+        const entity = await this.connection.getRepository(ctx, entityType).findOne(entityId, {
             relations: ['channels'],
             relations: ['channels'],
         });
         });
+        if (!entity) {
+            return;
+        }
         for (const id of channelIds) {
         for (const id of channelIds) {
             entity.channels = entity.channels.filter(c => !idsAreEqual(c.id, id));
             entity.channels = entity.channels.filter(c => !idsAreEqual(c.id, id));
         }
         }

+ 4 - 4
packages/core/src/service/services/collection.service.ts

@@ -87,9 +87,7 @@ export class CollectionService implements OnModuleInit {
                 Logger.verbose(`Processing ${job.data.collectionIds.length} Collections`);
                 Logger.verbose(`Processing ${job.data.collectionIds.length} Collections`);
                 let completed = 0;
                 let completed = 0;
                 for (const collectionId of job.data.collectionIds) {
                 for (const collectionId of job.data.collectionIds) {
-                    const collection = await this.connection.getRepository(Collection).findOne(collectionId, {
-                        relations: ['productVariants'],
-                    });
+                    const collection = await this.connection.getRepository(Collection).findOne(collectionId);
                     if (!collection) {
                     if (!collection) {
                         Logger.warn(`Could not find Collection with id ${collectionId}, skipping`);
                         Logger.warn(`Could not find Collection with id ${collectionId}, skipping`);
                         continue;
                         continue;
@@ -482,8 +480,10 @@ export class CollectionService implements OnModuleInit {
             const productVariants = await this.connection
             const productVariants = await this.connection
                 .getRepository(ctx, ProductVariant)
                 .getRepository(ctx, ProductVariant)
                 .createQueryBuilder('variant')
                 .createQueryBuilder('variant')
+                .select('variant.id', 'id')
                 .innerJoin('variant.collections', 'collection', 'collection.id = :id', { id: collection.id })
                 .innerJoin('variant.collections', 'collection', 'collection.id = :id', { id: collection.id })
-                .getMany();
+                .getRawMany();
+
             return productVariants.map(v => v.id);
             return productVariants.map(v => v.id);
         }
         }
     }
     }

+ 0 - 8
packages/create/src/create-vendure-app.ts

@@ -106,14 +106,6 @@ async function createApp(
             'migration:run': usingTs ? 'ts-node migration run' : 'node migration run',
             'migration:run': usingTs ? 'ts-node migration run' : 'node migration run',
             'migration:revert': usingTs ? 'ts-node migration revert' : 'node migration revert',
             'migration:revert': usingTs ? 'ts-node migration revert' : 'node migration revert',
         },
         },
-        /**
-         * A work-around for the breaking update of tslib as described here:
-         * https://github.com/typeorm/typeorm/issues/6054
-         * TODO: Remove this once the TypeScript team come up with a solution
-         */
-        resolutions: {
-            tslib: '1.11.2',
-        },
     };
     };
 
 
     console.log();
     console.log();

+ 3 - 65
packages/dev-server/dev-config.ts

@@ -2,63 +2,11 @@
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import { AssetServerPlugin } from '@vendure/asset-server-plugin';
 import { AssetServerPlugin } from '@vendure/asset-server-plugin';
 import { ADMIN_API_PATH, API_PORT, SHOP_API_PATH } from '@vendure/common/lib/shared-constants';
 import { ADMIN_API_PATH, API_PORT, SHOP_API_PATH } from '@vendure/common/lib/shared-constants';
-import {
-    Asset,
-    DefaultJobQueuePlugin,
-    DefaultLogger,
-    defaultPromotionActions,
-    DefaultSearchPlugin,
-    dummyPaymentHandler,
-    examplePaymentHandler,
-    FulfillmentHandler,
-    LanguageCode,
-    LogLevel,
-    manualFulfillmentHandler,
-    PaymentMethodEligibilityChecker,
-    PromotionItemAction,
-    VendureConfig,
-} from '@vendure/core';
-import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
+import { DefaultJobQueuePlugin, DefaultLogger, DefaultSearchPlugin, dummyPaymentHandler, LogLevel, VendureConfig, } from '@vendure/core';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import path from 'path';
 import path from 'path';
 import { ConnectionOptions } from 'typeorm';
 import { ConnectionOptions } from 'typeorm';
 
 
-const testPaymentChecker = new PaymentMethodEligibilityChecker({
-    code: 'test-checker',
-    description: [{ languageCode: LanguageCode.en, value: 'test checker' }],
-    args: {},
-    check: (ctx, order) => true,
-});
-
-const testPromoAction = new PromotionItemAction({
-    code: 'discount-price-action',
-    description: [{ languageCode: LanguageCode.en, value: 'Apply discount price' }],
-    args: {},
-    execute: (ctx, orderItem, orderLine) => {
-        if ((orderLine.productVariant.customFields as any).discountPrice) {
-            return -(
-                orderLine.unitPriceWithTax - (orderLine.productVariant.customFields as any).discountPrice
-            );
-        }
-        return 0;
-    },
-});
-
-const myHandler = new FulfillmentHandler({
-    code: 'test-handler',
-    args: {},
-    description: [{ languageCode: LanguageCode.en, value: 'test fulfillment handler' }],
-    createFulfillment: ctx => {
-        return {
-            method: 'test-handler',
-            trackingCode: '123123123123',
-            customFields: {
-                logoId: 1,
-            },
-        };
-    },
-});
-
 /**
 /**
  * Config settings used during development
  * Config settings used during development
  */
  */
@@ -93,23 +41,13 @@ export const devConfig: VendureConfig = {
         ...getDbConfig(),
         ...getDbConfig(),
     },
     },
     paymentOptions: {
     paymentOptions: {
-        paymentMethodEligibilityCheckers: [testPaymentChecker],
         paymentMethodHandlers: [dummyPaymentHandler],
         paymentMethodHandlers: [dummyPaymentHandler],
     },
     },
-    promotionOptions: {
-        promotionActions: [...defaultPromotionActions, testPromoAction],
-    },
-    customFields: {
-        /*Asset: [{ name: 'description', type: 'string' }],*/
-        ProductVariant: [{ name: 'discountPrice', type: 'int' }],
-    },
-    logger: new DefaultLogger({ level: LogLevel.Info }),
+    customFields: {},
+    logger: new DefaultLogger({level: LogLevel.Info}),
     importExportOptions: {
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },
     },
-    shippingOptions: {
-        fulfillmentHandlers: [manualFulfillmentHandler, myHandler],
-    },
     plugins: [
     plugins: [
         AssetServerPlugin.init({
         AssetServerPlugin.init({
             route: 'assets',
             route: 'assets',

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

@@ -1013,6 +1013,72 @@ describe('Elasticsearch plugin', () => {
                 });
                 });
                 expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
                 expect(searchGrouped.items.map(i => i.productName)).toEqual(['xyz']);
             });
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/896
+            it('removing from channel with multiple languages', async () => {
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+
+                await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
+                    input: {
+                        id: 'T_4',
+                        translations: [
+                            {
+                                languageCode: LanguageCode.en,
+                                name: 'product en',
+                                slug: 'product-en',
+                                description: 'en',
+                            },
+                            {
+                                languageCode: LanguageCode.de,
+                                name: 'product de',
+                                slug: 'product-de',
+                                description: 'de',
+                            },
+                        ],
+                    },
+                });
+
+                await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
+                    ASSIGN_PRODUCT_TO_CHANNEL,
+                    {
+                        input: { channelId: secondChannel.id, productIds: ['T_4'] },
+                    },
+                );
+                await awaitRunningJobs(adminClient);
+
+                async function searchSecondChannelForDEProduct() {
+                    adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
+                    const { search } = await adminClient.query<
+                        SearchProductsShop.Query,
+                        SearchProductsShop.Variables
+                    >(
+                        SEARCH_PRODUCTS,
+                        {
+                            input: { term: 'product', groupByProduct: true },
+                        },
+                        { languageCode: LanguageCode.de },
+                    );
+                    return search;
+                }
+
+                const search1 = await searchSecondChannelForDEProduct();
+                expect(search1.items.map(i => i.productName)).toEqual(['product de']);
+
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                const { removeProductsFromChannel } = await adminClient.query<
+                    RemoveProductsFromChannel.Mutation,
+                    RemoveProductsFromChannel.Variables
+                >(REMOVE_PRODUCT_FROM_CHANNEL, {
+                    input: {
+                        productIds: ['T_4'],
+                        channelId: secondChannel.id,
+                    },
+                });
+                await awaitRunningJobs(adminClient);
+
+                const search2 = await searchSecondChannelForDEProduct();
+                expect(search2.items.map(i => i.productName)).toEqual([]);
+            });
         });
         });
 
 
         describe('multiple language handling', () => {
         describe('multiple language handling', () => {

+ 1 - 0
packages/email-plugin/src/plugin.ts

@@ -118,6 +118,7 @@ import {
  * ```ts
  * ```ts
  * EmailPlugin.init({
  * EmailPlugin.init({
  *   devMode: true,
  *   devMode: true,
+ *   route: 'mailbox',
  *   handlers: defaultEmailHandlers,
  *   handlers: defaultEmailHandlers,
  *   templatePath: path.join(__dirname, 'vendure/email/templates'),
  *   templatePath: path.join(__dirname, 'vendure/email/templates'),
  *   outputPath: path.join(__dirname, 'test-emails'),
  *   outputPath: path.join(__dirname, 'test-emails'),

+ 9 - 33
yarn.lock

@@ -3797,20 +3797,6 @@
   dependencies:
   dependencies:
     "@types/node" "*"
     "@types/node" "*"
 
 
-"@types/graphql-iso-date@^3.4.0":
-  version "3.4.0"
-  resolved "https://registry.npmjs.org/@types/graphql-iso-date/-/graphql-iso-date-3.4.0.tgz#b6710b21e3b0bfdb1a0529b285148d98eac18b1f"
-  integrity sha512-V3jITHTsoI2E8TGt9+/HPDz6LWt3z9/HYnPJYWI6WwiLRexsngg7KzaQlCgQkA4jkEbGPROUD0hJFc9F02W9WA==
-  dependencies:
-    graphql "^15.1.0"
-
-"@types/graphql-type-json@^0.3.2":
-  version "0.3.2"
-  resolved "https://registry.npmjs.org/@types/graphql-type-json/-/graphql-type-json-0.3.2.tgz#1a7105e6546fc1630a5db4834bfbc0eb554986e4"
-  integrity sha512-c1cq4o8EhY0Z39ua8UXwG8uBs23xBYA/Uw0tXFl6SuTUpkVv/IJqf6pHQbfdC7nwFRhX2ifTOV/UIg0Q/IJsbg==
-  dependencies:
-    graphql "^14.5.3"
-
 "@types/graphql-upload@^8.0.4":
 "@types/graphql-upload@^8.0.4":
   version "8.0.4"
   version "8.0.4"
   resolved "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.4.tgz#23a8ffb3d2fe6e0ee07e6f16ee9d9d5e995a2f4f"
   resolved "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.4.tgz#23a8ffb3d2fe6e0ee07e6f16ee9d9d5e995a2f4f"
@@ -9675,11 +9661,6 @@ graphql-extensions@^0.14.0:
     apollo-server-env "^3.1.0"
     apollo-server-env "^3.1.0"
     apollo-server-types "^0.8.0"
     apollo-server-types "^0.8.0"
 
 
-graphql-iso-date@^3.6.1:
-  version "3.6.1"
-  resolved "https://registry.npmjs.org/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96"
-  integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==
-
 graphql-request@^3.3.0:
 graphql-request@^3.3.0:
   version "3.4.0"
   version "3.4.0"
   resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-3.4.0.tgz#3a400cd5511eb3c064b1873afb059196bbea9c2b"
   resolved "https://registry.npmjs.org/graphql-request/-/graphql-request-3.4.0.tgz#3a400cd5511eb3c064b1873afb059196bbea9c2b"
@@ -9689,6 +9670,13 @@ graphql-request@^3.3.0:
     extract-files "^9.0.0"
     extract-files "^9.0.0"
     form-data "^3.0.0"
     form-data "^3.0.0"
 
 
+graphql-scalars@^1.10.0:
+  version "1.10.0"
+  resolved "https://registry.npmjs.org/graphql-scalars/-/graphql-scalars-1.10.0.tgz#9daf9252b16e6fae553a06976163a23f41b65dfd"
+  integrity sha512-LONlj8FfhA2iGpkZJWf5e4PVAHXxnZEHSOEvowLYvNXl/TNnhIck8VmE+lren/aa6GKrG+lZufo5lgnyjxcF6g==
+  dependencies:
+    tslib "~2.2.0"
+
 graphql-subscriptions@^1.0.0:
 graphql-subscriptions@^1.0.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.2.0.tgz#d82ff76e7504ac91acbbea15f36cd3904043937b"
   resolved "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.2.0.tgz#d82ff76e7504ac91acbbea15f36cd3904043937b"
@@ -9726,11 +9714,6 @@ graphql-tools@^4.0.8:
     iterall "^1.1.3"
     iterall "^1.1.3"
     uuid "^3.1.0"
     uuid "^3.1.0"
 
 
-graphql-type-json@^0.3.2:
-  version "0.3.2"
-  resolved "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115"
-  integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
-
 graphql-upload@^11.0.0:
 graphql-upload@^11.0.0:
   version "11.0.0"
   version "11.0.0"
   resolved "https://registry.npmjs.org/graphql-upload/-/graphql-upload-11.0.0.tgz#24b245ff18f353bab6715e8a055db9fd73035e10"
   resolved "https://registry.npmjs.org/graphql-upload/-/graphql-upload-11.0.0.tgz#24b245ff18f353bab6715e8a055db9fd73035e10"
@@ -9758,18 +9741,11 @@ graphql-ws@4.1.5:
   resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.1.5.tgz#03526b29acb54a424a9fbe300a4bd69ff65a50b3"
   resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.1.5.tgz#03526b29acb54a424a9fbe300a4bd69ff65a50b3"
   integrity sha512-yUQ1AjegD1Y9jDS699kyw7Mw+9H+rILm2HoS8N5a5B5YTH93xy3yifFhAJpKGc2wb/8yGdlVy8gTcud0TPqi6Q==
   integrity sha512-yUQ1AjegD1Y9jDS699kyw7Mw+9H+rILm2HoS8N5a5B5YTH93xy3yifFhAJpKGc2wb/8yGdlVy8gTcud0TPqi6Q==
 
 
-graphql@*, graphql@15.5.0, graphql@^15.1.0, graphql@^15.3.0:
+graphql@*, graphql@15.5.0, graphql@^15.3.0:
   version "15.5.0"
   version "15.5.0"
   resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5"
   resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5"
   integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==
   integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA==
 
 
-graphql@^14.5.3:
-  version "14.7.0"
-  resolved "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72"
-  integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==
-  dependencies:
-    iterall "^1.2.2"
-
 growly@^1.3.0:
 growly@^1.3.0:
   version "1.3.0"
   version "1.3.0"
   resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
   resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -11191,7 +11167,7 @@ istanbul-reports@^3.0.2:
     html-escaper "^2.0.0"
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
     istanbul-lib-report "^3.0.0"
 
 
-iterall@1.3.0, iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2, iterall@^1.3.0:
+iterall@1.3.0, iterall@^1.1.3, iterall@^1.2.1, iterall@^1.3.0:
   version "1.3.0"
   version "1.3.0"
   resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
   resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
   integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
   integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==