Skip to content

Commit

Permalink
fix(elasticsearch-plugin): Products without variants are indexed
Browse files Browse the repository at this point in the history
Relates to #609
  • Loading branch information
michaelbromley committed Feb 10, 2021
1 parent 2dab174 commit 21b6aa3
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 9 deletions.
91 changes: 86 additions & 5 deletions packages/elasticsearch-plugin/e2e/elasticsearch-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
/* tslint:disable:no-non-null-assertion no-console */
import { Client } from '@elastic/elasticsearch';
import { SortOrder } from '@vendure/common/lib/generated-types';
import { pick } from '@vendure/common/lib/pick';
import {
DefaultJobQueuePlugin,
DefaultLogger,
facetValueCollectionFilter,
Logger,
LogLevel,
mergeConfig,
} from '@vendure/core';
import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN, SimpleGraphQLClient } from '@vendure/testing';
import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
import gql from 'graphql-tag';
import path from 'path';

Expand All @@ -23,6 +21,8 @@ import {
CreateChannel,
CreateCollection,
CreateFacet,
CreateProduct,
CreateProductVariants,
CurrencyCode,
DeleteAsset,
DeleteProduct,
Expand All @@ -39,13 +39,15 @@ import {
UpdateProductVariants,
UpdateTaxRate,
} from '../../core/e2e/graphql/generated-e2e-admin-types';
import { LogicalOperator, SearchProductsShop } from '../../core/e2e/graphql/generated-e2e-shop-types';
import { SearchProductsShop } from '../../core/e2e/graphql/generated-e2e-shop-types';
import {
ASSIGN_PRODUCTVARIANT_TO_CHANNEL,
ASSIGN_PRODUCT_TO_CHANNEL,
CREATE_CHANNEL,
CREATE_COLLECTION,
CREATE_FACET,
CREATE_PRODUCT,
CREATE_PRODUCT_VARIANTS,
DELETE_ASSET,
DELETE_PRODUCT,
DELETE_PRODUCT_VARIANT,
Expand All @@ -59,7 +61,6 @@ import {
} from '../../core/e2e/graphql/shared-definitions';
import { SEARCH_PRODUCTS_SHOP } from '../../core/e2e/graphql/shop-definitions';
import { awaitRunningJobs } from '../../core/e2e/utils/await-running-jobs';
import { loggerCtx } from '../src/constants';
import { ElasticsearchPlugin } from '../src/plugin';

import {
Expand Down Expand Up @@ -713,6 +714,86 @@ describe('Elasticsearch 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(adminClient, {
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(adminClient, {
groupByProduct: true,
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
53 changes: 49 additions & 4 deletions packages/elasticsearch-plugin/src/indexer.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,12 +577,11 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
}

private async updateProductInternal(ctx: RequestContext, productId: ID) {
let updatedProductVariants: ProductVariant[] = [];
const product = await this.connection.getRepository(Product).findOne(productId, {
relations: ['variants', 'channels', 'channels.defaultTaxZone'],
});
if (product) {
updatedProductVariants = await this.connection.getRepository(ProductVariant).findByIds(
const updatedProductVariants = await this.connection.getRepository(ProductVariant).findByIds(
product.variants.map(v => v.id),
{
relations: variantRelations,
Expand All @@ -594,10 +593,10 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
if (product.enabled === false) {
updatedProductVariants.forEach(v => (v.enabled = false));
}
const operations: Array<BulkOperation | BulkOperationDoc<ProductIndexItem>> = [];

if (updatedProductVariants.length) {
Logger.verbose(`Updating 1 Product (${productId})`, loggerCtx);
const operations: Array<BulkOperation | BulkOperationDoc<ProductIndexItem>> = [];
const languageVariants = product.translations.map(t => t.languageCode);

for (const channel of product.channels) {
Expand Down Expand Up @@ -636,8 +635,18 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
);
}
}
await this.executeBulkOperations(PRODUCT_INDEX_NAME, operations);
} else {
const syntheticIndexItem = this.createSyntheticProductIndexItem(ctx, product);
operations.push(
{
update: {
_id: this.getId(syntheticIndexItem.productId, ctx.channelId, ctx.languageCode),
},
},
{ doc: syntheticIndexItem, doc_as_upsert: true },
);
}
await this.executeBulkOperations(PRODUCT_INDEX_NAME, operations);
}
}

Expand Down Expand Up @@ -866,6 +875,42 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
return item;
}

/**
* If a Product has no variants, we create a synthetic variant for the purposes
* of making that product visible via the search query.
*/
private createSyntheticProductIndexItem(ctx: RequestContext, product: Product): ProductIndexItem {
const productTranslation = this.getTranslation(product, ctx.languageCode);
return {
channelId: ctx.channelId,
languageCode: ctx.languageCode,
sku: '',
slug: productTranslation.slug,
productId: product.id,
productName: productTranslation.name,
productAssetId: product.featuredAsset?.id ?? undefined,
productPreview: product.featuredAsset?.preview ?? '',
productPreviewFocalPoint: product.featuredAsset?.focalPoint ?? undefined,
productVariantId: 0,
productVariantName: productTranslation.name,
productVariantAssetId: undefined,
productVariantPreview: '',
productVariantPreviewFocalPoint: undefined,
priceMin: 0,
priceMax: 0,
priceWithTaxMin: 0,
priceWithTaxMax: 0,
currencyCode: ctx.channel.currencyCode,
description: productTranslation.description,
facetIds: product.facetValues?.map(fv => fv.facet.id.toString()) ?? [],
facetValueIds: product.facetValues?.map(fv => fv.id.toString()) ?? [],
collectionIds: [],
collectionSlugs: [],
channelIds: [ctx.channelId],
enabled: false,
};
}

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

0 comments on commit 21b6aa3

Please sign in to comment.