diff --git a/packages/core/e2e/product-channel.e2e-spec.ts b/packages/core/e2e/product-channel.e2e-spec.ts index 8787dcae3e..8bf99a6b26 100644 --- a/packages/core/e2e/product-channel.e2e-spec.ts +++ b/packages/core/e2e/product-channel.e2e-spec.ts @@ -10,11 +10,14 @@ import { AssignProductVariantsToChannel, CreateAdministrator, CreateChannel, + CreateProduct, + CreateProductVariants, CreateRole, CurrencyCode, GetProductWithVariants, LanguageCode, Permission, + ProductVariantFragment, RemoveProductsFromChannel, RemoveProductVariantsFromChannel, } from './graphql/generated-e2e-admin-types'; @@ -23,6 +26,8 @@ import { ASSIGN_PRODUCT_TO_CHANNEL, CREATE_ADMINISTRATOR, CREATE_CHANNEL, + CREATE_PRODUCT, + CREATE_PRODUCT_VARIANTS, CREATE_ROLE, GET_PRODUCT_WITH_VARIANTS, REMOVE_PRODUCTVARIANT_FROM_CHANNEL, @@ -195,6 +200,7 @@ describe('ChannelAware Products and ProductVariants', () => { }); it('does not assign Product to same channel twice', async () => { + adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); const { assignProductsToChannel } = await adminClient.query< AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables @@ -295,7 +301,7 @@ describe('ChannelAware Products and ProductVariants', () => { >(GET_PRODUCT_WITH_VARIANTS, { id: product1.id, }); - expect(product!.channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']); + expect(product!.channels.map(c => c.id).sort()).toEqual(['T_3']); expect(product!.variants.map(v => v.price)).toEqual([ Math.round((product1.variants[0].price * PRICE_FACTOR) / 1.2), ]); @@ -303,6 +309,19 @@ describe('ChannelAware Products and ProductVariants', () => { expect(product!.variants.map(v => v.priceWithTax)).toEqual([ product1.variants[0].price * PRICE_FACTOR, ]); + + await adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); + const { product: check } = await adminClient.query< + GetProductWithVariants.Query, + GetProductWithVariants.Variables + >(GET_PRODUCT_WITH_VARIANTS, { + id: product1.id, + }); + + // from the default channel, all channels are visible + expect(check?.channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']); + expect(check?.variants[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_3']); + expect(check?.variants[1].channels.map(c => c.id).sort()).toEqual(['T_1']); }); it('does not assign ProductVariant to same channel twice', async () => { @@ -393,4 +412,62 @@ describe('ChannelAware Products and ProductVariants', () => { expect(product!.channels.map(c => c.id).sort()).toEqual(['T_1']); }); }); + + describe('creating Product in sub-channel', () => { + let createdProduct: CreateProduct.CreateProduct; + let createdVariant: ProductVariantFragment; + + it('creates a Product in sub-channel', async () => { + adminClient.setChannelToken(SECOND_CHANNEL_TOKEN); + + const { createProduct } = await adminClient.query< + CreateProduct.Mutation, + CreateProduct.Variables + >(CREATE_PRODUCT, { + input: { + translations: [ + { + languageCode: LanguageCode.en, + name: 'Channel Product', + slug: 'channel-product', + description: 'Channel product', + }, + ], + }, + }); + const { createProductVariants } = await adminClient.query< + CreateProductVariants.Mutation, + CreateProductVariants.Variables + >(CREATE_PRODUCT_VARIANTS, { + input: [ + { + productId: createProduct.id, + sku: 'PV1', + optionIds: [], + translations: [{ languageCode: LanguageCode.en, name: 'Variant 1' }], + }, + ], + }); + + createdProduct = createProduct; + createdVariant = createProductVariants[0]!; + + // from sub-channel, only that channel is visible + expect(createdProduct.channels.map(c => c.id).sort()).toEqual(['T_2']); + expect(createdVariant.channels.map(c => c.id).sort()).toEqual(['T_2']); + + adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); + + const { product } = await adminClient.query< + GetProductWithVariants.Query, + GetProductWithVariants.Variables + >(GET_PRODUCT_WITH_VARIANTS, { + id: createProduct.id, + }); + + // from the default channel, all channels are visible + expect(product?.channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']); + expect(product?.variants[0].channels.map(c => c.id).sort()).toEqual(['T_1', 'T_2']); + }); + }); }); diff --git a/packages/core/src/api/resolvers/entity/product-entity.resolver.ts b/packages/core/src/api/resolvers/entity/product-entity.resolver.ts index aa716c1a34..b77354c795 100644 --- a/packages/core/src/api/resolvers/entity/product-entity.resolver.ts +++ b/packages/core/src/api/resolvers/entity/product-entity.resolver.ts @@ -1,6 +1,8 @@ import { Info, Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants'; import { Translated } from '../../../common/types/locale-types'; +import { idsAreEqual } from '../../../common/utils'; import { Asset } from '../../../entity/asset/asset.entity'; import { Channel } from '../../../entity/channel/channel.entity'; import { Collection } from '../../../entity/collection/collection.entity'; @@ -88,10 +90,8 @@ export class ProductAdminEntityResolver { @ResolveField() async channels(@Ctx() ctx: RequestContext, @Parent() product: Product): Promise { - if (product.channels) { - return product.channels; - } else { - return this.productService.getProductChannels(ctx, product.id); - } + const isDefaultChannel = ctx.channel.code === DEFAULT_CHANNEL_CODE; + const channels = product.channels || (await this.productService.getProductChannels(ctx, product.id)); + return channels.filter(channel => (isDefaultChannel ? true : idsAreEqual(channel.id, ctx.channelId))); } } diff --git a/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts b/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts index 6e85e57f58..bd731fc10c 100644 --- a/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts +++ b/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts @@ -103,16 +103,7 @@ export class ProductVariantAdminEntityResolver { @ResolveField() async channels(@Ctx() ctx: RequestContext, @Parent() productVariant: ProductVariant): Promise { const isDefaultChannel = ctx.channel.code === DEFAULT_CHANNEL_CODE; - if (!isDefaultChannel && productVariant.channels) { - return productVariant.channels; - } else { - const channels = await this.productVariantService.getProductVariantChannels( - ctx, - productVariant.id, - ); - return channels.filter(channel => - isDefaultChannel ? true : idsAreEqual(channel.id, ctx.channelId), - ); - } + const channels = await this.productVariantService.getProductVariantChannels(ctx, productVariant.id); + return channels.filter(channel => (isDefaultChannel ? true : idsAreEqual(channel.id, ctx.channelId))); } } diff --git a/packages/core/src/service/services/product-variant.service.ts b/packages/core/src/service/services/product-variant.service.ts index 726c07d1a6..f6b35b24e9 100644 --- a/packages/core/src/service/services/product-variant.service.ts +++ b/packages/core/src/service/services/product-variant.service.ts @@ -324,7 +324,14 @@ export class ProductVariantService { ); } + const defaultChannelId = this.channelService.getDefaultChannel().id; await this.createProductVariantPrice(ctx, createdVariant.id, input.price, ctx.channelId); + if (!idsAreEqual(ctx.channelId, defaultChannelId)) { + // When creating a ProductVariant _not_ in the default Channel, we still need to + // create a ProductVariantPrice for it in the default Channel, otherwise errors will + // result when trying to query it there. + await this.createProductVariantPrice(ctx, createdVariant.id, input.price, defaultChannelId); + } return createdVariant.id; }