Skip to content

Commit

Permalink
feat(core): Make Facets/FacetValues Channel-aware
Browse files Browse the repository at this point in the history
Relates to #612

BREAKING CHANGE: The Facet and FacetValue entities are now channel-aware. This change to the
schema will require a DB migration.
  • Loading branch information
michaelbromley committed Feb 10, 2021
1 parent 3aae4fb commit e8fcb99
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 33 deletions.
216 changes: 214 additions & 2 deletions packages/core/e2e/facet.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { pick } from '@vendure/common/lib/pick';
import { createTestEnvironment } from '@vendure/testing';
import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
import gql from 'graphql-tag';
import path from 'path';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';

import { FACET_VALUE_FRAGMENT, FACET_WITH_VALUES_FRAGMENT } from './graphql/fragments';
import {
AssignProductsToChannel,
ChannelFragment,
CreateChannel,
CreateFacet,
CreateFacetValues,
CurrencyCode,
DeleteFacet,
DeleteFacetValues,
DeletionResult,
Expand All @@ -25,13 +29,16 @@ import {
UpdateProductVariants,
} from './graphql/generated-e2e-admin-types';
import {
ASSIGN_PRODUCT_TO_CHANNEL,
CREATE_CHANNEL,
CREATE_FACET,
GET_FACET_LIST,
GET_PRODUCT_WITH_VARIANTS,
UPDATE_FACET,
UPDATE_PRODUCT,
UPDATE_PRODUCT_VARIANTS,
} from './graphql/shared-definitions';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';

// tslint:disable:no-non-null-assertion

Expand Down Expand Up @@ -363,6 +370,211 @@ describe('Facet resolver', () => {
expect(result.deleteFacet.result).toBe(DeletionResult.DELETED);
});
});

describe('channels', () => {
const SECOND_CHANNEL_TOKEN = 'second_channel_token';
let createdFacet: CreateFacet.CreateFacet;

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.USD,
pricesIncludeTax: true,
defaultShippingZoneId: 'T_1',
defaultTaxZoneId: 'T_1',
},
});

const { assignProductsToChannel } = await adminClient.query<
AssignProductsToChannel.Mutation,
AssignProductsToChannel.Variables
>(ASSIGN_PRODUCT_TO_CHANNEL, {
input: {
channelId: (createChannel as ChannelFragment).id,
productIds: ['T_1'],
priceFactor: 0.5,
},
});

adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
});

it('create Facet in channel', async () => {
const { createFacet } = await adminClient.query<CreateFacet.Mutation, CreateFacet.Variables>(
CREATE_FACET,
{
input: {
isPrivate: false,
code: 'channel-facet',
translations: [{ languageCode: LanguageCode.en, name: 'Channel Facet' }],
values: [
{
code: 'channel-value-1',
translations: [{ languageCode: LanguageCode.en, name: 'Channel Value 1' }],
},
{
code: 'channel-value-2',
translations: [{ languageCode: LanguageCode.en, name: 'Channel Value 2' }],
},
],
},
},
);

expect(createFacet.code).toBe('channel-facet');

createdFacet = createFacet;
});

it('facets list in channel', async () => {
const result = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);

const { items } = result.facets;
expect(items.length).toBe(1);
expect(items.map(i => i.code)).toEqual(['channel-facet']);
});

it('Product.facetValues in channel', async () => {
adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
input: {
id: 'T_1',
facetValueIds: [brandFacet.values[0].id, ...createdFacet.values.map(v => v.id)],
},
});
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
UPDATE_PRODUCT_VARIANTS,
{
input: [
{
id: 'T_1',
facetValueIds: [brandFacet.values[0].id, ...createdFacet.values.map(v => v.id)],
},
],
},
);

adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
const { product } = await adminClient.query<
GetProductWithVariants.Query,
GetProductWithVariants.Variables
>(GET_PRODUCT_WITH_VARIANTS, {
id: 'T_1',
});

expect(product?.facetValues.map(fv => fv.code)).toEqual(['channel-value-1', 'channel-value-2']);
});

it('ProductVariant.facetValues in channel', async () => {
const { product } = await adminClient.query<
GetProductWithVariants.Query,
GetProductWithVariants.Variables
>(GET_PRODUCT_WITH_VARIANTS, {
id: 'T_1',
});

expect(product?.variants[0].facetValues.map(fv => fv.code)).toEqual([
'channel-value-1',
'channel-value-2',
]);
});

it('updating Product facetValuesIds in channel only affects that channel', async () => {
adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
input: {
id: 'T_1',
facetValueIds: [createdFacet.values[0].id],
},
});

const { product: productC2 } = await adminClient.query<
GetProductWithVariants.Query,
GetProductWithVariants.Variables
>(GET_PRODUCT_WITH_VARIANTS, {
id: 'T_1',
});

expect(productC2?.facetValues.map(fv => fv.code)).toEqual([createdFacet.values[0].code]);

adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
const { product: productCD } = await adminClient.query<
GetProductWithVariants.Query,
GetProductWithVariants.Variables
>(GET_PRODUCT_WITH_VARIANTS, {
id: 'T_1',
});

expect(productCD?.facetValues.map(fv => fv.code)).toEqual([
brandFacet.values[0].code,
createdFacet.values[0].code,
]);
});

it('updating ProductVariant facetValuesIds in channel only affects that channel', async () => {
adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
UPDATE_PRODUCT_VARIANTS,
{
input: [
{
id: 'T_1',
facetValueIds: [createdFacet.values[0].id],
},
],
},
);

const { product: productC2 } = await adminClient.query<
GetProductWithVariants.Query,
GetProductWithVariants.Variables
>(GET_PRODUCT_WITH_VARIANTS, {
id: 'T_1',
});

expect(productC2?.variants.find(v => v.id === 'T_1')?.facetValues.map(fv => fv.code)).toEqual([
createdFacet.values[0].code,
]);

adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
const { product: productCD } = await adminClient.query<
GetProductWithVariants.Query,
GetProductWithVariants.Variables
>(GET_PRODUCT_WITH_VARIANTS, {
id: 'T_1',
});

expect(productCD?.variants.find(v => v.id === 'T_1')?.facetValues.map(fv => fv.code)).toEqual([
brandFacet.values[0].code,
createdFacet.values[0].code,
]);
});

it(
'attempting to create FacetValue in Facet from another Channel throws',
assertThrowsWithMessage(async () => {
adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
await adminClient.query<CreateFacetValues.Mutation, CreateFacetValues.Variables>(
CREATE_FACET_VALUES,
{
input: [
{
facetId: brandFacet.id,
code: 'channel-brand',
translations: [{ languageCode: LanguageCode.en, name: 'Channel Brand' }],
},
],
},
);
}, `No Facet with the id '1' could be found`),
);
});
});

export const GET_FACET_WITH_VALUES = gql`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,16 @@ export class ProductEntityResolver {
@Ctx() ctx: RequestContext,
@Parent() product: Product,
): Promise<Array<Translated<FacetValue>>> {
if (product.facetValues) {
return product.facetValues as Array<Translated<FacetValue>>;
if (product.facetValues?.length === 0) {
return [];
}
return this.productService.getFacetValuesForProduct(ctx, product.id);
let facetValues: Array<Translated<FacetValue>>;
if (product.facetValues?.[0]?.channels) {
facetValues = product.facetValues as Array<Translated<FacetValue>>;
} else {
facetValues = await this.productService.getFacetValuesForProduct(ctx, product.id);
}
return facetValues.filter(fv => fv.channels.find(c => idsAreEqual(c.id, ctx.channelId)));
}

@ResolveField()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,25 @@ export class ProductVariantEntityResolver {
@Parent() productVariant: ProductVariant,
@Api() apiType: ApiType,
): Promise<Array<Translated<FacetValue>>> {
if (productVariant.facetValues?.length === 0) {
return [];
}
let facetValues: Array<Translated<FacetValue>>;
if (productVariant.facetValues) {
if (productVariant.facetValues?.[0]?.channels) {
facetValues = productVariant.facetValues as Array<Translated<FacetValue>>;
} else {
facetValues = await this.productVariantService.getFacetValuesForVariant(ctx, productVariant.id);
}
if (apiType === 'shop') {
facetValues = facetValues.filter(fv => !fv.facet.isPrivate);
}
return facetValues;

return facetValues.filter(fv => {
if (!fv.channels.find(c => idsAreEqual(c.id, ctx.channelId))) {
return false;
}
if (apiType === 'shop' && fv.facet.isPrivate) {
return false;
}
return true;
});
}
}

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/data-import/providers/importer/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ export class Importer {
languageCode: LanguageCode,
): Promise<ID[]> {
const facetValueIds: ID[] = [];
const ctx = new RequestContext({
channel: this.channelService.getDefaultChannel(),
apiType: 'admin',
isAuthorized: true,
authorizedAsOwnerOnly: false,
session: {} as any,
});

for (const item of facets) {
const facetName = item.facet;
Expand All @@ -252,7 +259,7 @@ export class Importer {
if (existing) {
facetEntity = existing;
} else {
facetEntity = await this.facetService.create(RequestContext.empty(), {
facetEntity = await this.facetService.create(ctx, {
isPrivate: false,
code: normalizeString(facetName, '-'),
translations: [{ languageCode, name: facetName }],
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/entity/facet-value/facet-value.entity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { DeepPartial } from '@vendure/common/lib/shared-types';
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany } from 'typeorm';

import { ChannelAware } from '../../common/types/common-types';
import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
import { HasCustomFields } from '../../config/custom-field/custom-field-types';
import { VendureEntity } from '../base/base.entity';
import { Channel } from '../channel/channel.entity';
import { CustomFacetValueFields } from '../custom-entity-fields';
import { Facet } from '../facet/facet.entity';

Expand All @@ -16,7 +18,7 @@ import { FacetValueTranslation } from './facet-value-translation.entity';
* @docsCategory entities
*/
@Entity()
export class FacetValue extends VendureEntity implements Translatable, HasCustomFields {
export class FacetValue extends VendureEntity implements Translatable, HasCustomFields, ChannelAware {
constructor(input?: DeepPartial<FacetValue>) {
super(input);
}
Expand All @@ -32,4 +34,8 @@ export class FacetValue extends VendureEntity implements Translatable, HasCustom

@Column(type => CustomFacetValueFields)
customFields: CustomFacetValueFields;

@ManyToMany(type => Channel)
@JoinTable()
channels: Channel[];
}
10 changes: 8 additions & 2 deletions packages/core/src/entity/facet/facet.entity.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { DeepPartial } from '@vendure/common/lib/shared-types';
import { Column, Entity, OneToMany } from 'typeorm';
import { Column, Entity, JoinTable, ManyToMany, OneToMany } from 'typeorm';

import { ChannelAware } from '../../common/types/common-types';
import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
import { HasCustomFields } from '../../config/custom-field/custom-field-types';
import { VendureEntity } from '../base/base.entity';
import { Channel } from '../channel/channel.entity';
import { CustomFacetFields } from '../custom-entity-fields';
import { FacetValue } from '../facet-value/facet-value.entity';

Expand All @@ -21,7 +23,7 @@ import { FacetTranslation } from './facet-translation.entity';
* @docsCategory entities
*/
@Entity()
export class Facet extends VendureEntity implements Translatable, HasCustomFields {
export class Facet extends VendureEntity implements Translatable, HasCustomFields, ChannelAware {
constructor(input?: DeepPartial<Facet>) {
super(input);
}
Expand All @@ -42,4 +44,8 @@ export class Facet extends VendureEntity implements Translatable, HasCustomField

@Column(type => CustomFacetFields)
customFields: CustomFacetFields;

@ManyToMany(type => Channel)
@JoinTable()
channels: Channel[];
}
1 change: 1 addition & 0 deletions packages/core/src/service/services/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@vendure/common/lib/generated-types';
import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
import { ID, Type } from '@vendure/common/lib/shared-types';
import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
import { unique } from '@vendure/common/lib/unique';

import { RequestContext } from '../../api/common/request-context';
Expand Down
Loading

0 comments on commit e8fcb99

Please sign in to comment.