Browse Source

feat(core): Implement removeProductsFromChannel mutation

Michael Bromley 6 years ago
parent
commit
6a165dca99

+ 28 - 0
packages/common/src/generated-types.ts

@@ -162,6 +162,12 @@ export enum AssetType {
   BINARY = 'BINARY'
   BINARY = 'BINARY'
 }
 }
 
 
+export type AssignProductsToChannelInput = {
+  productIds: Array<Scalars['ID']>,
+  channelId: Scalars['ID'],
+  priceFactor?: Maybe<Scalars['Float']>,
+};
+
 export type BooleanCustomFieldConfig = CustomField & {
 export type BooleanCustomFieldConfig = CustomField & {
   __typename?: 'BooleanCustomFieldConfig',
   __typename?: 'BooleanCustomFieldConfig',
   name: Scalars['String'],
   name: Scalars['String'],
@@ -1758,6 +1764,10 @@ export type Mutation = {
   updateProductVariants: Array<Maybe<ProductVariant>>,
   updateProductVariants: Array<Maybe<ProductVariant>>,
   /** Delete a ProductVariant */
   /** Delete a ProductVariant */
   deleteProductVariant: DeletionResponse,
   deleteProductVariant: DeletionResponse,
+  /** Assigns Products to the specified Channel */
+  assignProductsToChannel: Array<Product>,
+  /** Removes Products from the specified Channel */
+  removeProductsFromChannel: Array<Product>,
   createPromotion: Promotion,
   createPromotion: Promotion,
   updatePromotion: Promotion,
   updatePromotion: Promotion,
   deletePromotion: DeletionResponse,
   deletePromotion: DeletionResponse,
@@ -2058,6 +2068,16 @@ export type MutationDeleteProductVariantArgs = {
 };
 };
 
 
 
 
+export type MutationAssignProductsToChannelArgs = {
+  input: AssignProductsToChannelInput
+};
+
+
+export type MutationRemoveProductsFromChannelArgs = {
+  input: RemoveProductsFromChannelInput
+};
+
+
 export type MutationCreatePromotionArgs = {
 export type MutationCreatePromotionArgs = {
   input: CreatePromotionInput
   input: CreatePromotionInput
 };
 };
@@ -2403,6 +2423,7 @@ export type PriceRange = {
 export type Product = Node & {
 export type Product = Node & {
   __typename?: 'Product',
   __typename?: 'Product',
   enabled: Scalars['Boolean'],
   enabled: Scalars['Boolean'],
+  channels: Array<Channel>,
   id: Scalars['ID'],
   id: Scalars['ID'],
   createdAt: Scalars['DateTime'],
   createdAt: Scalars['DateTime'],
   updatedAt: Scalars['DateTime'],
   updatedAt: Scalars['DateTime'],
@@ -2934,6 +2955,11 @@ export type RefundOrderInput = {
   reason?: Maybe<Scalars['String']>,
   reason?: Maybe<Scalars['String']>,
 };
 };
 
 
+export type RemoveProductsFromChannelInput = {
+  productIds: Array<Scalars['ID']>,
+  channelId: Scalars['ID'],
+};
+
 export type Return = Node & StockMovement & {
 export type Return = Node & StockMovement & {
   __typename?: 'Return',
   __typename?: 'Return',
   id: Scalars['ID'],
   id: Scalars['ID'],
@@ -3020,6 +3046,8 @@ export type SearchResponse = {
 export type SearchResult = {
 export type SearchResult = {
   __typename?: 'SearchResult',
   __typename?: 'SearchResult',
   enabled: Scalars['Boolean'],
   enabled: Scalars['Boolean'],
+  /** An array of ids of the Collections in which this result appears */
+  channelIds: Array<Scalars['ID']>,
   sku: Scalars['String'],
   sku: Scalars['String'],
   slug: Scalars['String'],
   slug: Scalars['String'],
   productId: Scalars['ID'],
   productId: Scalars['ID'],

+ 18 - 0
packages/core/e2e/__snapshots__/product.e2e-spec.ts.snap

@@ -3,6 +3,12 @@
 exports[`Product resolver product mutation createProduct creates a new Product 1`] = `
 exports[`Product resolver product mutation createProduct creates a new Product 1`] = `
 Object {
 Object {
   "assets": Array [],
   "assets": Array [],
+  "channels": Array [
+    Object {
+      "code": "__default_channel__",
+      "id": "T_1",
+    },
+  ],
   "description": "A baked potato",
   "description": "A baked potato",
   "enabled": true,
   "enabled": true,
   "facetValues": Array [],
   "facetValues": Array [],
@@ -33,6 +39,12 @@ Object {
 exports[`Product resolver product mutation updateProduct updates a Product 1`] = `
 exports[`Product resolver product mutation updateProduct updates a Product 1`] = `
 Object {
 Object {
   "assets": Array [],
   "assets": Array [],
+  "channels": Array [
+    Object {
+      "code": "__default_channel__",
+      "id": "T_1",
+    },
+  ],
   "description": "A blob of mashed potato",
   "description": "A blob of mashed potato",
   "enabled": true,
   "enabled": true,
   "facetValues": Array [],
   "facetValues": Array [],
@@ -73,6 +85,12 @@ Object {
       "type": "IMAGE",
       "type": "IMAGE",
     },
     },
   ],
   ],
+  "channels": Array [
+    Object {
+      "code": "__default_channel__",
+      "id": "T_1",
+    },
+  ],
   "description": "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.",
   "description": "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.",
   "enabled": true,
   "enabled": true,
   "facetValues": Array [
   "facetValues": Array [

+ 55 - 1
packages/core/e2e/channel.e2e-spec.ts

@@ -19,6 +19,7 @@ import {
     LanguageCode,
     LanguageCode,
     Me,
     Me,
     Permission,
     Permission,
+    RemoveProductsFromChannel,
     UpdateProduct,
     UpdateProduct,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import {
 import {
@@ -260,7 +261,6 @@ describe('Channels', () => {
         it(
         it(
             'throws if attempting to assign Product to channel to which the admin has no access',
             'throws if attempting to assign Product to channel to which the admin has no access',
             assertThrowsWithMessage(async () => {
             assertThrowsWithMessage(async () => {
-                expect(product1).toBeDefined();
                 await adminClient.asUserWithCredentials('admin2@test.com', 'test');
                 await adminClient.asUserWithCredentials('admin2@test.com', 'test');
                 await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
                 await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
                     ASSIGN_PRODUCT_TO_CHANNEL,
                     ASSIGN_PRODUCT_TO_CHANNEL,
@@ -305,6 +305,51 @@ describe('Channels', () => {
                 product1.variants.map(v => v.price * PRICE_FACTOR),
                 product1.variants.map(v => v.price * PRICE_FACTOR),
             );
             );
         });
         });
+
+        it('does not assign Product to same channel twice', async () => {
+            const { assignProductsToChannel } = await adminClient.query<
+                AssignProductsToChannel.Mutation,
+                AssignProductsToChannel.Variables
+            >(ASSIGN_PRODUCT_TO_CHANNEL, {
+                input: {
+                    channelId: 'T_2',
+                    productIds: [product1.id],
+                },
+            });
+
+            expect(assignProductsToChannel[0].channels.map(c => c.id)).toEqual(['T_1', 'T_2']);
+        });
+
+        it(
+            'throws if attempting to remove Product from default Channel',
+            assertThrowsWithMessage(async () => {
+                await adminClient.query<
+                    RemoveProductsFromChannel.Mutation,
+                    RemoveProductsFromChannel.Variables
+                >(REMOVE_PRODUCT_FROM_CHANNEL, {
+                    input: {
+                        productIds: [product1.id],
+                        channelId: 'T_1',
+                    },
+                });
+            }, 'Products cannot be removed from the default Channel'),
+        );
+
+        it('removes Product from Channel', async () => {
+            await adminClient.asSuperAdmin();
+            await adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+            const { removeProductsFromChannel } = await adminClient.query<
+                RemoveProductsFromChannel.Mutation,
+                RemoveProductsFromChannel.Variables
+            >(REMOVE_PRODUCT_FROM_CHANNEL, {
+                input: {
+                    productIds: [product1.id],
+                    channelId: 'T_2',
+                },
+            });
+
+            expect(removeProductsFromChannel[0].channels.map(c => c.id)).toEqual(['T_1']);
+        });
     });
     });
 });
 });
 
 
@@ -316,3 +361,12 @@ const ASSIGN_PRODUCT_TO_CHANNEL = gql`
     }
     }
     ${PRODUCT_WITH_VARIANTS_FRAGMENT}
     ${PRODUCT_WITH_VARIANTS_FRAGMENT}
 `;
 `;
+
+const REMOVE_PRODUCT_FROM_CHANNEL = gql`
+    mutation RemoveProductsFromChannel($input: RemoveProductsFromChannelInput!) {
+        removeProductsFromChannel(input: $input) {
+            ...ProductWithVariants
+        }
+    }
+    ${PRODUCT_WITH_VARIANTS_FRAGMENT}
+`;

+ 27 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1769,6 +1769,8 @@ export type Mutation = {
     deleteProductVariant: DeletionResponse;
     deleteProductVariant: DeletionResponse;
     /** Assigns Products to the specified Channel */
     /** Assigns Products to the specified Channel */
     assignProductsToChannel: Array<Product>;
     assignProductsToChannel: Array<Product>;
+    /** Removes Products from the specified Channel */
+    removeProductsFromChannel: Array<Product>;
     createPromotion: Promotion;
     createPromotion: Promotion;
     updatePromotion: Promotion;
     updatePromotion: Promotion;
     deletePromotion: DeletionResponse;
     deletePromotion: DeletionResponse;
@@ -2021,6 +2023,10 @@ export type MutationAssignProductsToChannelArgs = {
     input: AssignProductsToChannelInput;
     input: AssignProductsToChannelInput;
 };
 };
 
 
+export type MutationRemoveProductsFromChannelArgs = {
+    input: RemoveProductsFromChannelInput;
+};
+
 export type MutationCreatePromotionArgs = {
 export type MutationCreatePromotionArgs = {
     input: CreatePromotionInput;
     input: CreatePromotionInput;
 };
 };
@@ -2843,6 +2849,11 @@ export type RefundOrderInput = {
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type RemoveProductsFromChannelInput = {
+    productIds: Array<Scalars['ID']>;
+    channelId: Scalars['ID'];
+};
+
 export type Return = Node &
 export type Return = Node &
     StockMovement & {
     StockMovement & {
         __typename?: 'Return';
         __typename?: 'Return';
@@ -2931,6 +2942,8 @@ export type SearchResponse = {
 export type SearchResult = {
 export type SearchResult = {
     __typename?: 'SearchResult';
     __typename?: 'SearchResult';
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
+    /** An array of ids of the Collections in which this result appears */
+    channelIds: Array<Scalars['ID']>;
     sku: Scalars['String'];
     sku: Scalars['String'];
     slug: Scalars['String'];
     slug: Scalars['String'];
     productId: Scalars['ID'];
     productId: Scalars['ID'];
@@ -3425,6 +3438,14 @@ export type AssignProductsToChannelMutation = { __typename?: 'Mutation' } & {
     assignProductsToChannel: Array<{ __typename?: 'Product' } & ProductWithVariantsFragment>;
     assignProductsToChannel: Array<{ __typename?: 'Product' } & ProductWithVariantsFragment>;
 };
 };
 
 
+export type RemoveProductsFromChannelMutationVariables = {
+    input: RemoveProductsFromChannelInput;
+};
+
+export type RemoveProductsFromChannelMutation = { __typename?: 'Mutation' } & {
+    removeProductsFromChannel: Array<{ __typename?: 'Product' } & ProductWithVariantsFragment>;
+};
+
 export type GetCollectionsWithAssetsQueryVariables = {};
 export type GetCollectionsWithAssetsQueryVariables = {};
 
 
 export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & {
 export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & {
@@ -5092,6 +5113,12 @@ export namespace AssignProductsToChannel {
     export type AssignProductsToChannel = ProductWithVariantsFragment;
     export type AssignProductsToChannel = ProductWithVariantsFragment;
 }
 }
 
 
+export namespace RemoveProductsFromChannel {
+    export type Variables = RemoveProductsFromChannelMutationVariables;
+    export type Mutation = RemoveProductsFromChannelMutation;
+    export type RemoveProductsFromChannel = ProductWithVariantsFragment;
+}
+
 export namespace GetCollectionsWithAssets {
 export namespace GetCollectionsWithAssets {
     export type Variables = GetCollectionsWithAssetsQueryVariables;
     export type Variables = GetCollectionsWithAssetsQueryVariables;
     export type Query = GetCollectionsWithAssetsQuery;
     export type Query = GetCollectionsWithAssetsQuery;

+ 10 - 0
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -8,6 +8,7 @@ import {
     MutationDeleteProductArgs,
     MutationDeleteProductArgs,
     MutationDeleteProductVariantArgs,
     MutationDeleteProductVariantArgs,
     MutationRemoveOptionGroupFromProductArgs,
     MutationRemoveOptionGroupFromProductArgs,
+    MutationRemoveProductsFromChannelArgs,
     MutationUpdateProductArgs,
     MutationUpdateProductArgs,
     MutationUpdateProductVariantsArgs,
     MutationUpdateProductVariantsArgs,
     Permission,
     Permission,
@@ -149,4 +150,13 @@ export class ProductResolver {
     ): Promise<Array<Translated<Product>>> {
     ): Promise<Array<Translated<Product>>> {
         return this.productService.assignProductsToChannel(ctx, args.input);
         return this.productService.assignProductsToChannel(ctx, args.input);
     }
     }
+
+    @Mutation()
+    @Allow(Permission.UpdateCatalog)
+    async removeProductsFromChannel(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationRemoveProductsFromChannelArgs,
+    ): Promise<Array<Translated<Product>>> {
+        return this.productService.removeProductsFromChannel(ctx, args.input);
+    }
 }
 }

+ 8 - 0
packages/core/src/api/schema/admin-api/product.api.graphql

@@ -31,6 +31,9 @@ type Mutation {
 
 
     "Assigns Products to the specified Channel"
     "Assigns Products to the specified Channel"
     assignProductsToChannel(input: AssignProductsToChannelInput!): [Product!]!
     assignProductsToChannel(input: AssignProductsToChannelInput!): [Product!]!
+
+    "Removes Products from the specified Channel"
+    removeProductsFromChannel(input: RemoveProductsFromChannelInput!): [Product!]!
 }
 }
 
 
 type Product implements Node {
 type Product implements Node {
@@ -129,3 +132,8 @@ input AssignProductsToChannelInput {
     channelId: ID!
     channelId: ID!
     priceFactor: Float
     priceFactor: Float
 }
 }
+
+input RemoveProductsFromChannelInput {
+    productIds: [ID!]!
+    channelId: ID!
+}

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -46,6 +46,7 @@
     "password-reset-token-has-expired": "Password reset token has expired.",
     "password-reset-token-has-expired": "Password reset token has expired.",
     "password-reset-token-not-recognized": "Password reset token not recognized",
     "password-reset-token-not-recognized": "Password reset token not recognized",
     "permission-invalid": "The permission \"{ permission }\" is not valid",
     "permission-invalid": "The permission \"{ permission }\" is not valid",
+    "products-cannot-be-removed-from-default-channel": "Products cannot be removed from the default Channel",
     "product-id-or-slug-must-be-provided": "Either the product id or slug must be provided",
     "product-id-or-slug-must-be-provided": "Either the product id or slug must be provided",
     "product-id-slug-mismatch": "The provided id and slug refer to different Products",
     "product-id-slug-mismatch": "The provided id and slug refer to different Products",
     "product-variant-option-ids-not-compatible": "ProductVariant optionIds must include one optionId from each of the groups: {groupNames}",
     "product-variant-option-ids-not-compatible": "ProductVariant optionIds must include one optionId from each of the groups: {groupNames}",

+ 26 - 0
packages/core/src/service/helpers/utils/find-by-ids-in-channel.ts

@@ -0,0 +1,26 @@
+import { ID, Type } from '@vendure/common/lib/shared-types';
+import { Connection, FindManyOptions, FindOptionsUtils } from 'typeorm';
+
+import { ChannelAware } from '../../../common/types/common-types';
+import { VendureEntity } from '../../../entity';
+
+/**
+ * Like the TypeOrm `Repository.findByIds()` method, but limits the results to
+ * the given Channel.
+ */
+export function findByIdsInChannel<T extends ChannelAware | VendureEntity>(
+    connection: Connection,
+    entity: Type<T>,
+    ids: ID[],
+    channelId: ID,
+    findOptions?: FindManyOptions<T>,
+) {
+    const qb = connection.getRepository(entity).createQueryBuilder('product');
+    FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, findOptions);
+    // tslint:disable-next-line:no-non-null-assertion
+    FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
+    return qb
+        .leftJoin('product.channels', 'channel')
+        .andWhere('channel.id = :channelId', { channelId })
+        .getMany();
+}

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

@@ -1,6 +1,7 @@
 export * from './helpers/job-manager/job';
 export * from './helpers/job-manager/job';
 export * from './helpers/utils/translate-entity';
 export * from './helpers/utils/translate-entity';
 export * from './helpers/utils/patch-entity';
 export * from './helpers/utils/patch-entity';
+export * from './helpers/utils/find-by-ids-in-channel';
 export * from './helpers/utils/get-entity-or-throw';
 export * from './helpers/utils/get-entity-or-throw';
 export * from './helpers/list-query-builder/list-query-builder';
 export * from './helpers/list-query-builder/list-query-builder';
 export * from './helpers/order-state-machine/order-state';
 export * from './helpers/order-state-machine/order-state';

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

@@ -10,7 +10,7 @@ import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ChannelAware } from '../../common/types/common-types';
 import { ChannelAware } from '../../common/types/common-types';
-import { assertFound } from '../../common/utils';
+import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { VendureEntity } from '../../entity/base/base.entity';
 import { VendureEntity } from '../../entity/base/base.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Channel } from '../../entity/channel/channel.entity';
@@ -62,6 +62,24 @@ export class ChannelService {
         return entity;
         return entity;
     }
     }
 
 
+    /**
+     * Removes the entity from the given Channels and saves.
+     */
+    async removeFromChannels<T extends ChannelAware & VendureEntity>(
+        entityType: Type<T>,
+        entityId: ID,
+        channelIds: ID[],
+    ): Promise<T> {
+        const entity = await getEntityOrThrow(this.connection, entityType, entityId, {
+            relations: ['channels'],
+        });
+        for (const id of channelIds) {
+            entity.channels = entity.channels.filter(c => !idsAreEqual(c.id, id));
+        }
+        await this.connection.getRepository(entityType).save(entity as any);
+        return entity;
+    }
+
     /**
     /**
      * Given a channel token, returns the corresponding Channel if it exists.
      * Given a channel token, returns the corresponding Channel if it exists.
      */
      */

+ 36 - 8
packages/core/src/service/services/product.service.ts

@@ -6,6 +6,7 @@ import {
     DeletionResponse,
     DeletionResponse,
     DeletionResult,
     DeletionResult,
     Permission,
     Permission,
+    RemoveProductsFromChannelInput,
     UpdateProductInput,
     UpdateProductInput,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
@@ -25,6 +26,7 @@ import { EventBus } from '../../event-bus/event-bus';
 import { CatalogModificationEvent } from '../../event-bus/events/catalog-modification-event';
 import { CatalogModificationEvent } from '../../event-bus/events/catalog-modification-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { findByIdsInChannel } from '../helpers/utils/find-by-ids-in-channel';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 
@@ -89,6 +91,16 @@ export class ProductService {
         return translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]);
         return translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]);
     }
     }
 
 
+    async findByIds(ctx: RequestContext, productIds: ID[]): Promise<Array<Translated<Product>>> {
+        return findByIdsInChannel(this.connection, Product, productIds, ctx.channelId, {
+            relations: this.relations,
+        }).then(products =>
+            products.map(product =>
+                translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]),
+            ),
+        );
+    }
+
     async getProductChannels(productId: ID): Promise<Channel[]> {
     async getProductChannels(productId: ID): Promise<Channel[]> {
         const product = await getEntityOrThrow(this.connection, Product, productId, {
         const product = await getEntityOrThrow(this.connection, Product, productId, {
             relations: ['channels'],
             relations: ['channels'],
@@ -184,16 +196,32 @@ export class ProductService {
                     input.channelId,
                     input.channelId,
                 );
                 );
             }
             }
+            this.eventBus.publish(new CatalogModificationEvent(ctx, product));
         }
         }
+        return this.findByIds(ctx, productsWithVariants.map(p => p.id));
+    }
 
 
-        return this.connection
-            .getRepository(Product)
-            .findByIds(productsWithVariants.map(p => p.id), { relations: this.relations })
-            .then(products =>
-                products.map(product =>
-                    translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]),
-                ),
-            );
+    async removeProductsFromChannel(
+        ctx: RequestContext,
+        input: RemoveProductsFromChannelInput,
+    ): Promise<Array<Translated<Product>>> {
+        const hasPermission = await this.roleService.userHasPermissionOnChannel(
+            ctx.activeUserId,
+            input.channelId,
+            Permission.UpdateCatalog,
+        );
+        if (!hasPermission) {
+            throw new ForbiddenError();
+        }
+        if (idsAreEqual(input.channelId, this.channelService.getDefaultChannel().id)) {
+            throw new UserInputError('error.products-cannot-be-removed-from-default-channel');
+        }
+        const products = await this.connection.getRepository(Product).findByIds(input.productIds);
+        for (const product of products) {
+            await this.channelService.removeFromChannels(Product, product.id, [input.channelId]);
+            this.eventBus.publish(new CatalogModificationEvent(ctx, product));
+        }
+        return this.findByIds(ctx, products.map(p => p.id));
     }
     }
 
 
     async addOptionGroupToProduct(
     async addOptionGroupToProduct(

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


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