Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(default-search-plugin): add support for 'currencyCode' index #3268

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ModuleRef } from '@nestjs/core';
import { SearchReindexResponse } from '@vendure/common/lib/generated-types';
import { ID, Type } from '@vendure/common/lib/shared-types';
import { buffer, debounceTime, delay, filter, map } from 'rxjs/operators';
import { Column } from 'typeorm';
import { Column, PrimaryColumn } from 'typeorm';

import { Injector } from '../../common';
import { idsAreEqual } from '../../common/utils';
Expand Down Expand Up @@ -109,6 +109,9 @@ export class DefaultSearchPlugin implements OnApplicationBootstrap, OnApplicatio
if (options.indexStockStatus === true) {
this.addStockColumnsToEntity();
}
if (options.indexCurrencyCode) {
this.addCurrencyCodeToEntity();
}
return DefaultSearchPlugin;
}

Expand Down Expand Up @@ -240,4 +243,15 @@ export class DefaultSearchPlugin implements OnApplicationBootstrap, OnApplicatio
Column({ type: 'boolean', default: true })(instance, 'inStock');
Column({ type: 'boolean', default: true })(instance, 'productInStock');
}

/**
* If the `indexCurrencyCode` option is set to `true`, we dynamically add
* a column to the SearchIndexItem entity. This is done in this way to allow us to add
* support for indexing on the currency code, while preventing a backwards-incompatible
* schema change.
*/
private static addCurrencyCodeToEntity() {
const instance = new SearchIndexItem();
PrimaryColumn({ type: 'varchar' })(instance, 'currencyCode');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,6 @@ export class SearchIndexItem {
inStock?: boolean;
// Added dynamically based on the `indexStockStatus` init option.
productInStock?: boolean;
// Added dynamically based on the `indexCurrencyCode` init option.
currencyCode?: CurrencyCode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -436,66 +436,75 @@ export class IndexerController {
channelIds = unique(channelIds);

for (const channel of variant.channels) {
ctx.setChannel(channel);
await this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
const item = new SearchIndexItem({
channelId: ctx.channelId,
languageCode,
productVariantId: variant.id,
price: variant.price,
priceWithTax: variant.priceWithTax,
sku: variant.sku,
enabled: product.enabled === false ? false : variant.enabled,
slug: productTranslation?.slug ?? '',
productId: product.id,
productName: productTranslation?.name ?? '',
description: this.constrainDescription(productTranslation?.description ?? ''),
productVariantName: variantTranslation?.name ?? '',
productAssetId: product.featuredAsset ? product.featuredAsset.id : null,
productPreviewFocalPoint: product.featuredAsset
? product.featuredAsset.focalPoint
: null,
productVariantPreviewFocalPoint: variant.featuredAsset
? variant.featuredAsset.focalPoint
: null,
productVariantAssetId: variant.featuredAsset ? variant.featuredAsset.id : null,
productPreview: product.featuredAsset ? product.featuredAsset.preview : '',
productVariantPreview: variant.featuredAsset ? variant.featuredAsset.preview : '',
channelIds: channelIds.map(x => x.toString()),
facetIds: this.getFacetIds(variant, product),
facetValueIds: this.getFacetValueIds(variant, product),
collectionIds: variant.collections.map(c => c.id.toString()),
collectionSlugs:
collectionTranslations.map(c => c?.slug).filter(notNullOrUndefined) ?? [],
});
if (this.options.indexStockStatus) {
item.inStock =
0 < (await this.productVariantService.getSaleableStockLevel(ctx, variant));
const productInStock = await this.requestContextCache.get(
ctx,
`productVariantsStock-${variant.productId}`,
() =>
this.connection
.getRepository(ctx, ProductVariant)
.find({
loadEagerRelations: false,
where: {
productId: variant.productId,
deletedAt: IsNull(),
},
})
.then(_variants =>
Promise.all(
_variants.map(v =>
this.productVariantService.getSaleableStockLevel(ctx, v),
const availableCurrencyCodes = this.options.indexCurrencyCode
? unique(channel.availableCurrencyCodes)
: [ctx.channel.defaultCurrencyCode];

for (const currencyCode of availableCurrencyCodes) {
const ch = new Channel({ ...channel, defaultCurrencyCode: currencyCode });
ctx.setChannel(ch);

await this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
const item = new SearchIndexItem({
channelId: ctx.channelId,
languageCode,
currencyCode,
productVariantId: variant.id,
price: variant.price,
priceWithTax: variant.priceWithTax,
sku: variant.sku,
enabled: product.enabled === false ? false : variant.enabled,
slug: productTranslation?.slug ?? '',
productId: product.id,
productName: productTranslation?.name ?? '',
description: this.constrainDescription(productTranslation?.description ?? ''),
productVariantName: variantTranslation?.name ?? '',
productAssetId: product.featuredAsset ? product.featuredAsset.id : null,
productPreviewFocalPoint: product.featuredAsset
? product.featuredAsset.focalPoint
: null,
productVariantPreviewFocalPoint: variant.featuredAsset
? variant.featuredAsset.focalPoint
: null,
productVariantAssetId: variant.featuredAsset ? variant.featuredAsset.id : null,
productPreview: product.featuredAsset ? product.featuredAsset.preview : '',
productVariantPreview: variant.featuredAsset ? variant.featuredAsset.preview : '',
channelIds: channelIds.map(x => x.toString()),
facetIds: this.getFacetIds(variant, product),
facetValueIds: this.getFacetValueIds(variant, product),
collectionIds: variant.collections.map(c => c.id.toString()),
collectionSlugs:
collectionTranslations.map(c => c?.slug).filter(notNullOrUndefined) ?? [],
});
if (this.options.indexStockStatus) {
item.inStock =
0 < (await this.productVariantService.getSaleableStockLevel(ctx, variant));
const productInStock = await this.requestContextCache.get(
ctx,
`productVariantsStock-${variant.productId}`,
() =>
this.connection
.getRepository(ctx, ProductVariant)
.find({
loadEagerRelations: false,
where: {
productId: variant.productId,
deletedAt: IsNull(),
},
})
.then(_variants =>
Promise.all(
_variants.map(v =>
this.productVariantService.getSaleableStockLevel(ctx, v),
),
),
),
)
.then(stockLevels => stockLevels.some(stockLevel => 0 < stockLevel)),
);
item.productInStock = productInStock;
)
.then(stockLevels => stockLevels.some(stockLevel => 0 < stockLevel)),
);
item.productInStock = productInStock;
}
items.push(item);
}
items.push(item);
}
}
}
Expand All @@ -514,6 +523,7 @@ export class IndexerController {
const productTranslation = this.getTranslation(product, ctx.languageCode);
const item = new SearchIndexItem({
channelId: ctx.channelId,
currencyCode: ctx.currencyCode,
languageCode: ctx.languageCode,
productVariantId: 0,
price: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ export class MysqlSearchStrategy implements SearchStrategy {
.limit(take)
.offset(skip)
.getRawMany()
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.defaultCurrencyCode)));
.then(res =>
res.map(r =>
mapToSearchResult(
r,
this.options.indexCurrencyCode ? r.si_currencyCode : ctx.channel.defaultCurrencyCode,
),
),
);
}

async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
Expand Down Expand Up @@ -259,6 +266,10 @@ export class MysqlSearchStrategy implements SearchStrategy {
qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);

if (this.options.indexCurrencyCode) {
qb.andWhere('si.currencyCode = :currencyCode', { currencyCode: ctx.currencyCode });
}

if (input.groupByProduct === true) {
qb.groupBy('si.productId');
qb.addSelect('BIT_OR(si.enabled)', 'productEnabled');
Expand All @@ -272,7 +283,7 @@ export class MysqlSearchStrategy implements SearchStrategy {
* "MIN" function in this case to all other columns than the productId.
*/
private createMysqlSelect(groupByProduct: boolean): string {
return getFieldsToSelect(this.options.indexStockStatus)
return getFieldsToSelect(this.options.indexStockStatus, this.options.indexCurrencyCode)
.map(col => {
const qualifiedName = `si.${col}`;
const alias = `si_${col}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ export class PostgresSearchStrategy implements SearchStrategy {
.limit(take)
.offset(skip)
.getRawMany()
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.defaultCurrencyCode)));
.then(res =>
res.map(r =>
mapToSearchResult(
r,
this.options.indexCurrencyCode ? r.si_currencyCode : ctx.channel.defaultCurrencyCode,
),
),
);
}

async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
Expand Down Expand Up @@ -254,6 +261,10 @@ export class PostgresSearchStrategy implements SearchStrategy {
qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);

if (this.options.indexCurrencyCode) {
qb.andWhere('si.currencyCode = :currencyCode', { currencyCode: ctx.currencyCode });
}

if (input.groupByProduct === true) {
qb.groupBy('si.productId');
}
Expand All @@ -267,7 +278,7 @@ export class PostgresSearchStrategy implements SearchStrategy {
* "MIN" function in this case to all other columns than the productId.
*/
private createPostgresSelect(groupByProduct: boolean): string {
return getFieldsToSelect(this.options.indexStockStatus)
return getFieldsToSelect(this.options.indexStockStatus, this.options.indexCurrencyCode)
.map(col => {
const qualifiedName = `si.${col}`;
const alias = `si_${col}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ export const fieldsToSelect = [
'productVariantPreviewFocalPoint',
];

export function getFieldsToSelect(includeStockStatus: boolean = false) {
return includeStockStatus ? [...fieldsToSelect, 'inStock', 'productInStock'] : fieldsToSelect;
export function getFieldsToSelect(includeStockStatus: boolean = false, includeCurrencyCode: boolean = false) {
const _fieldsToSelect = [...fieldsToSelect];
if (includeStockStatus) {
_fieldsToSelect.push('inStock');
}
if (includeCurrencyCode) {
_fieldsToSelect.push('currencyCode');
}
return _fieldsToSelect;
}
9 changes: 9 additions & 0 deletions packages/core/src/plugin/default-search-plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ export interface DefaultSearchPluginInitOptions {
* @default false.
*/
indexStockStatus?: boolean;
/**
* @description
* If set to `true`, the currencyCode of the ProductVariant will be exposed in the
* `search` query results. Enabling this option on an existing Vendure installation
* will require a DB migration/synchronization.
*
* @default false.
*/
indexCurrencyCode?: boolean;
/**
* @description
* If set to `true`, updates to Products, ProductVariants and Collections will not immediately
Expand Down
Loading