Skip to content

Commit

Permalink
fix(core): Products without variants are indexed by DefaultSearchPlugin
Browse files Browse the repository at this point in the history
Relates to #609
  • Loading branch information
michaelbromley committed Feb 10, 2021
1 parent 5967c8a commit 2dab174
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 9 deletions.
77 changes: 77 additions & 0 deletions packages/core/e2e/default-search-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
CreateChannel,
CreateCollection,
CreateFacet,
CreateProduct,
CreateProductVariants,
CurrencyCode,
DeleteAsset,
DeleteProduct,
Expand Down Expand Up @@ -47,6 +49,8 @@ import {
CREATE_CHANNEL,
CREATE_COLLECTION,
CREATE_FACET,
CREATE_PRODUCT,
CREATE_PRODUCT_VARIANTS,
DELETE_ASSET,
DELETE_PRODUCT,
DELETE_PRODUCT_VARIANT,
Expand Down Expand Up @@ -874,6 +878,79 @@ describe('Default search plugin', () => {
});
});

// https://github.com/vendure-ecommerce/vendure/issues/609
describe('Synthetic index items', () => {
let createdProductId: string;

it('creates synthetic index item for Product with no variants', async () => {
const { createProduct } = await adminClient.query<
CreateProduct.Mutation,
CreateProduct.Variables
>(CREATE_PRODUCT, {
input: {
facetValueIds: ['T_1'],
translations: [
{
languageCode: LanguageCode.en,
name: 'Strawberry cheesecake',
slug: 'strawberry-cheesecake',
description: 'A yummy dessert',
},
],
},
});

await awaitRunningJobs(adminClient);
const result = await doAdminSearchQuery({ groupByProduct: true, term: 'strawberry' });
expect(
result.search.items.map(
pick([
'productId',
'enabled',
'productName',
'productVariantName',
'slug',
'description',
]),
),
).toEqual([
{
productId: createProduct.id,
enabled: false,
productName: 'Strawberry cheesecake',
productVariantName: 'Strawberry cheesecake',
slug: 'strawberry-cheesecake',
description: 'A yummy dessert',
},
]);
createdProductId = createProduct.id;
});

it('removes synthetic index item once a variant is created', async () => {
const { createProductVariants } = await adminClient.query<
CreateProductVariants.Mutation,
CreateProductVariants.Variables
>(CREATE_PRODUCT_VARIANTS, {
input: [
{
productId: createdProductId,
sku: 'SC01',
price: 1399,
translations: [
{ languageCode: LanguageCode.en, name: 'Strawberry Cheesecake Pie' },
],
},
],
});
await awaitRunningJobs(adminClient);

const result = await doAdminSearchQuery({ groupByProduct: false, term: 'strawberry' });
expect(result.search.items.map(pick(['productVariantName']))).toEqual([
{ productVariantName: 'Strawberry Cheesecake Pie' },
]);
});
});

describe('channel handling', () => {
const SECOND_CHANNEL_TOKEN = 'second-channel-token';
let secondChannel: ChannelFragment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,15 +269,19 @@ export class IndexerController {
where: { deletedAt: null },
},
);
if (product.enabled === false) {
updatedVariants.forEach(v => (v.enabled = false));
}
const variantsInCurrentChannel = updatedVariants.filter(
v => !!v.channels.find(c => idsAreEqual(c.id, ctx.channelId)),
);
Logger.verbose(`Updating ${variantsInCurrentChannel.length} variants`, workerLoggerCtx);
if (variantsInCurrentChannel.length) {
await this.saveVariants(variantsInCurrentChannel);
if (updatedVariants.length === 0) {
await this.saveSyntheticVariant(ctx, product);
} else {
if (product.enabled === false) {
updatedVariants.forEach(v => (v.enabled = false));
}
const variantsInCurrentChannel = updatedVariants.filter(
v => !!v.channels.find(c => idsAreEqual(c.id, ctx.channelId)),
);
Logger.verbose(`Updating ${variantsInCurrentChannel.length} variants`, workerLoggerCtx);
if (variantsInCurrentChannel.length) {
await this.saveVariants(variantsInCurrentChannel);
}
}
}
return true;
Expand Down Expand Up @@ -337,6 +341,8 @@ export class IndexerController {
private async saveVariants(variants: ProductVariant[]) {
const items: SearchIndexItem[] = [];

await this.removeSyntheticVariants(variants);

for (const variant of variants) {
const languageVariants = unique([
...variant.translations.map(t => t.languageCode),
Expand Down Expand Up @@ -400,6 +406,56 @@ export class IndexerController {
await this.queue.push(() => this.connection.getRepository(SearchIndexItem).save(items));
}

/**
* If a Product has no variants, we create a synthetic variant for the purposes
* of making that product visible via the search query.
*/
private async saveSyntheticVariant(ctx: RequestContext, product: Product) {
const productTranslation = this.getTranslation(product, ctx.languageCode);
const item = new SearchIndexItem({
channelId: ctx.channelId,
languageCode: ctx.languageCode,
productVariantId: 0,
price: 0,
priceWithTax: 0,
sku: '',
enabled: false,
slug: productTranslation.slug,
productId: product.id,
productName: productTranslation.name,
description: productTranslation.description,
productVariantName: productTranslation.name,
productAssetId: product.featuredAsset?.id ?? null,
productPreviewFocalPoint: product.featuredAsset?.focalPoint ?? null,
productVariantPreviewFocalPoint: null,
productVariantAssetId: null,
productPreview: product.featuredAsset?.preview ?? '',
productVariantPreview: '',
channelIds: [ctx.channelId.toString()],
facetIds: product.facetValues?.map(fv => fv.facet.id.toString()) ?? [],
facetValueIds: product.facetValues?.map(fv => fv.id.toString()) ?? [],
collectionIds: [],
collectionSlugs: [],
});
await this.queue.push(() => this.connection.getRepository(SearchIndexItem).save(item));
}

/**
* Removes any synthetic variants for the given product
*/
private async removeSyntheticVariants(variants: ProductVariant[]) {
const prodIds = unique(variants.map(v => v.productId));
for (const productId of prodIds) {
await this.queue.push(() =>
this.connection.getRepository(SearchIndexItem).delete({
productId,
sku: '',
price: 0,
}),
);
}
}

private getTranslation<T extends Translatable>(
translatable: T,
languageCode: LanguageCode,
Expand Down

0 comments on commit 2dab174

Please sign in to comment.