Skip to content

Commit

Permalink
fix(core): Update search index when removing translated variants
Browse files Browse the repository at this point in the history
Fixes #896
  • Loading branch information
michaelbromley committed Jun 10, 2021
1 parent e3e8828 commit fced1dc
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 10 deletions.
68 changes: 68 additions & 0 deletions packages/core/e2e/default-search-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { pick } from '@vendure/common/lib/pick';
import {
DefaultJobQueuePlugin,
DefaultLogger,
DefaultSearchPlugin,
facetValueCollectionFilter,
mergeConfig,
Expand Down Expand Up @@ -72,6 +73,7 @@ jest.setTimeout(10000);
describe('Default search plugin', () => {
const { server, adminClient, shopClient } = createTestEnvironment(
mergeConfig(testConfig, {
logger: new DefaultLogger(),
plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin],
}),
);
Expand Down Expand Up @@ -1282,6 +1284,72 @@ describe('Default search plugin', () => {
});
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { asyncObservable, idsAreEqual } from '../../../common/utils';
import { ConfigService } from '../../../config/config.service';
import { Logger } from '../../../config/logger/vendure-logger';
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 { Product } from '../../../entity/product/product.entity';
import { ProductVariantService } from '../../../service/services/product-variant.service';
Expand Down Expand Up @@ -151,10 +152,15 @@ export class IndexerController {
const ctx = RequestContext.deserialize(data.ctx);
const variants = await this.connection.getRepository(ProductVariant).findByIds(data.variantIds);
if (variants.length) {
const languageVariants = unique([
...variants
.reduce((vt, v) => [...vt, ...v.translations], [] as Array<Translation<ProductVariant>>)
.map(t => t.languageCode),
]);
await this.removeSearchIndexItems(
ctx.languageCode,
ctx.channelId,
variants.map(v => v.id),
languageVariants,
);
}
return true;
Expand All @@ -177,15 +183,19 @@ export class IndexerController {

async removeVariantFromChannel(data: VariantChannelMessageData): Promise<boolean> {
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;
}

async updateAsset(data: UpdateAssetMessageData): Promise<boolean> {
const id = data.asset.id;

function getFocalPoint(point?: { x: number; y: number }) {
return point && point.x && point.y ? point : null;
}

const focalPoint = getFocalPoint(data.asset.focalPoint);
await this.connection
.getRepository(SearchIndexItem)
Expand Down Expand Up @@ -266,9 +276,16 @@ export class IndexerController {
relations: ['variants'],
});
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);
if (removedVariantIds.length) {
await this.removeSearchIndexItems(ctx.languageCode, channelId, removedVariantIds);
await this.removeSearchIndexItems(channelId, removedVariantIds, languageVariants);
}
}
return true;
Expand Down Expand Up @@ -436,13 +453,18 @@ export class IndexerController {
/**
* 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));
}

/**
Expand Down
66 changes: 66 additions & 0 deletions packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,72 @@ describe('Elasticsearch plugin', () => {
});
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', () => {
Expand Down

0 comments on commit fced1dc

Please sign in to comment.