From fced1dc2c2f6ae4f43da01797518b8f777a4caaf Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 10 Jun 2021 13:42:12 +0200 Subject: [PATCH] fix(core): Update search index when removing translated variants Fixes #896 --- .../e2e/default-search-plugin.e2e-spec.ts | 68 +++++++++++++++++++ .../indexer/indexer.controller.ts | 42 +++++++++--- .../e2e/elasticsearch-plugin.e2e-spec.ts | 66 ++++++++++++++++++ 3 files changed, 166 insertions(+), 10 deletions(-) diff --git a/packages/core/e2e/default-search-plugin.e2e-spec.ts b/packages/core/e2e/default-search-plugin.e2e-spec.ts index 92bf924782..86664d341c 100644 --- a/packages/core/e2e/default-search-plugin.e2e-spec.ts +++ b/packages/core/e2e/default-search-plugin.e2e-spec.ts @@ -2,6 +2,7 @@ import { pick } from '@vendure/common/lib/pick'; import { DefaultJobQueuePlugin, + DefaultLogger, DefaultSearchPlugin, facetValueCollectionFilter, mergeConfig, @@ -72,6 +73,7 @@ jest.setTimeout(10000); describe('Default search plugin', () => { const { server, adminClient, shopClient } = createTestEnvironment( mergeConfig(testConfig, { + logger: new DefaultLogger(), plugins: [DefaultSearchPlugin, DefaultJobQueuePlugin], }), ); @@ -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(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( + 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', () => { diff --git a/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts b/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts index 7668422574..9c54bba19a 100644 --- a/packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts +++ b/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 { 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'; @@ -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>) + .map(t => t.languageCode), + ]); await this.removeSearchIndexItems( - ctx.languageCode, ctx.channelId, variants.map(v => v.id), + languageVariants, ); } return true; @@ -177,15 +183,19 @@ export class IndexerController { async removeVariantFromChannel(data: VariantChannelMessageData): Promise { 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 { 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) @@ -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>) + .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; @@ -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> = []; + 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)); } /** diff --git a/packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts b/packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts index b8f643cf5a..3d1f9654ba 100644 --- a/packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts +++ b/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']); }); + + // 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(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( + 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', () => {