Skip to content

Commit

Permalink
feat(core): Add "enabled" field to search index, add & fix e2e tests
Browse files Browse the repository at this point in the history
Relates to #62
  • Loading branch information
michaelbromley committed Apr 24, 2019
1 parent a877853 commit fcd3086
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 47 deletions.
2 changes: 1 addition & 1 deletion packages/common/src/generated-shop-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// tslint:disable
// Generated in 2019-04-24T10:46:04+02:00
// Generated in 2019-04-24T15:08:57+02:00
export type Maybe<T> = T | null;

export interface OrderListOptions {
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// tslint:disable
// Generated in 2019-04-24T10:46:05+02:00
// Generated in 2019-04-24T15:08:58+02:00
export type Maybe<T> = T | null;


Expand Down
3 changes: 3 additions & 0 deletions packages/core/e2e/__snapshots__/product.e2e-spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`Product resolver product mutation createProduct creates a new Product 1
Object {
"assets": Array [],
"description": "A baked potato",
"enabled": true,
"facetValues": Array [],
"featuredAsset": null,
"id": "T_21",
Expand Down Expand Up @@ -33,6 +34,7 @@ exports[`Product resolver product mutation updateProduct updates a Product 1`] =
Object {
"assets": Array [],
"description": "A blob of mashed potato",
"enabled": true,
"facetValues": Array [],
"featuredAsset": null,
"id": "T_21",
Expand Down Expand Up @@ -72,6 +74,7 @@ Object {
},
],
"description": "Discover a truly immersive viewing experience with this monitor curved more deeply than any other. Wrapping around your field of vision the 1,800 R screencreates a wider field of view, enhances depth perception, and minimises peripheral distractions to draw you deeper in to your content.",
"enabled": true,
"facetValues": Array [
Object {
"code": "electronics",
Expand Down
45 changes: 37 additions & 8 deletions packages/core/e2e/default-search-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ import {
UpdateProduct,
UpdateTaxRate,
} from '@vendure/common/lib/generated-types';
import { pick } from '@vendure/common/lib/pick';
import gql from 'graphql-tag';
import path from 'path';

import {
CREATE_COLLECTION,
UPDATE_COLLECTION,
} from '../../../admin-ui/src/app/data/definitions/collection-definitions';
import { SEARCH_PRODUCTS, UPDATE_PRODUCT } from '../../../admin-ui/src/app/data/definitions/product-definitions';
import { CREATE_COLLECTION, UPDATE_COLLECTION } from '../../../admin-ui/src/app/data/definitions/collection-definitions';
import { SEARCH_PRODUCTS, UPDATE_PRODUCT, UPDATE_PRODUCT_VARIANTS } from '../../../admin-ui/src/app/data/definitions/product-definitions';
import { UPDATE_TAX_RATE } from '../../../admin-ui/src/app/data/definitions/settings-definitions';
import { UpdateProductVariants } from '../../common/src/generated-types';
import { SimpleGraphQLClient } from '../mock-data/simple-graphql-client';
import { facetValueCollectionFilter } from '../src/config/collection/default-collection-filters';
import { DefaultSearchPlugin } from '../src/plugin/default-search-plugin/default-search-plugin';
Expand Down Expand Up @@ -203,6 +202,36 @@ describe('Default search plugin', () => {
{ count: 3, facetValue: { id: 'T_6', name: 'plants' } },
]);
});

it('encodes the productId and productVariantId', async () => {
const result = await shopClient.query<SearchProducts.Query, SearchProducts.Variables>(SEARCH_PRODUCTS, {
input: {
groupByProduct: false,
take: 1,
},
});
expect(pick(result.search.items[0], ['productId', 'productVariantId'])).toEqual(
{
productId: 'T_1',
productVariantId: 'T_1',
},
);
});

it('omits results for disabled ProductVariants', async () => {
await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(UPDATE_PRODUCT_VARIANTS, {
input: [
{ id: 'T_3', enabled: false },
],
});
const result = await shopClient.query<SearchProducts.Query, SearchProducts.Variables>(SEARCH_PRODUCTS, {
input: {
groupByProduct: false,
take: 3,
},
});
expect(result.search.items.map(i => i.productVariantId)).toEqual(['T_1', 'T_2', 'T_4']);
});
});

describe('admin api', () => {
Expand All @@ -216,9 +245,9 @@ describe('Default search plugin', () => {

it('matches by collectionId', () => testMatchCollectionId(adminClient));

it('single prices', () => testSinglePrices(shopClient));
it('single prices', () => testSinglePrices(adminClient));

it('price ranges', () => testPriceRanges(shopClient));
it('price ranges', () => testPriceRanges(adminClient));

it('updates index when a Product is changed', async () => {
await adminClient.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
Expand Down Expand Up @@ -292,7 +321,7 @@ describe('Default search plugin', () => {
const { createCollection } = await adminClient.query<
CreateCollection.Mutation,
CreateCollection.Variables
>(CREATE_COLLECTION, {
>(CREATE_COLLECTION, {
input: {
translations: [
{
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/api/common/id-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ID } from '@vendure/common/lib/shared-types';
import { EntityIdStrategy } from '../../config/entity-id-strategy/entity-id-strategy';
import { VendureEntity } from '../../entity/base/base.entity';

const ID_KEYS = ['id', 'productId', 'productVariantId'];

/**
* This service is responsible for encoding/decoding entity IDs according to the configured EntityIdStrategy.
* It should only need to be used in resolvers - the design is that once a request hits the business logic layer
Expand All @@ -21,7 +23,7 @@ export class IdCodec {
* @return A decoded clone of the target
*/
decode<T extends string | number | object | undefined>(target: T, transformKeys?: string[]): T {
const transformKeysWithId = [...(transformKeys || []), 'id'];
const transformKeysWithId = [...(transformKeys || []), ...ID_KEYS];
return this.transformRecursive(
target,
input => this.entityIdStrategy.decodeId(input),
Expand All @@ -39,7 +41,7 @@ export class IdCodec {
* @return An encoded clone of the target
*/
encode<T extends string | number | boolean | object | undefined>(target: T, transformKeys?: string[]): T {
const transformKeysWithId = [...(transformKeys || []), 'id'];
const transformKeysWithId = [...(transformKeys || []), ...ID_KEYS];
return this.transformRecursive(
target,
input => this.entityIdStrategy.encodeId(input),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ export class ProductVariantSubscriber implements EntitySubscriberInterface<Produ
}

async afterUpdate(event: InsertEvent<ProductVariant>) {
const variantPrice = await event.connection.getRepository(ProductVariantPrice).findOne({
where: {
variant: event.entity.id,
channelId: event.queryRunner.data.channelId,
},
});
if (!variantPrice) {
throw new InternalServerError(`error.could-not-find-product-variant-price`);
}
if (event.entity.price !== undefined) {
const variantPrice = await event.connection.getRepository(ProductVariantPrice).findOne({
where: {
variant: event.entity.id,
channelId: event.queryRunner.data.channelId,
},
});
if (!variantPrice) {
throw new InternalServerError(`error.could-not-find-product-variant-price`);
}

variantPrice.price = event.entity.price || 0;
await event.manager.save(variantPrice);
variantPrice.price = event.entity.price || 0;
await event.manager.save(variantPrice);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ export class ShopFulltextSearchResolver implements Omit<BaseSearchResolver, 'rei
@Ctx() ctx: RequestContext,
@Args() args: SearchQueryArgs,
): Promise<Omit<SearchResponse, 'facetValues'>> {
return this.fulltextSearchService.search(ctx, args.input);
return this.fulltextSearchService.search(ctx, args.input, true);
}

@ResolveProperty()
async facetValues(
@Ctx() ctx: RequestContext,
@Context() context: any,
): Promise<Array<{ facetValue: FacetValue; count: number }>> {
return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input);
return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input, true);
}
}

Expand All @@ -47,15 +47,15 @@ export class AdminFulltextSearchResolver implements BaseSearchResolver {
@Ctx() ctx: RequestContext,
@Args() args: SearchQueryArgs,
): Promise<Omit<SearchResponse, 'facetValues'>> {
return this.fulltextSearchService.search(ctx, args.input);
return this.fulltextSearchService.search(ctx, args.input, false);
}

@ResolveProperty()
async facetValues(
@Ctx() ctx: RequestContext,
@Context() context: any,
): Promise<Array<{ facetValue: FacetValue; count: number }>> {
return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input);
return this.fulltextSearchService.facetValues(ctx, context.req.body.variables.input, false);
}

@Mutation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export class FulltextSearchService implements SearchService {
/**
* Perform a fulltext search according to the provided input arguments.
*/
async search(ctx: RequestContext, input: SearchInput): Promise<Omit<SearchResponse, 'facetValues'>> {
const items = await this.searchStrategy.getSearchResults(ctx, input);
const totalItems = await this.searchStrategy.getTotalCount(ctx, input);
async search(ctx: RequestContext, input: SearchInput, enabledOnly: boolean = false): Promise<Omit<SearchResponse, 'facetValues'>> {
const items = await this.searchStrategy.getSearchResults(ctx, input, enabledOnly);
const totalItems = await this.searchStrategy.getTotalCount(ctx, input, enabledOnly);
return {
items,
totalItems,
Expand All @@ -70,8 +70,9 @@ export class FulltextSearchService implements SearchService {
async facetValues(
ctx: RequestContext,
input: SearchInput,
enabledOnly: boolean = false
): Promise<Array<{ facetValue: FacetValue; count: number }>> {
const facetValueIdsMap = await this.searchStrategy.getFacetValueIds(ctx, input);
const facetValueIdsMap = await this.searchStrategy.getFacetValueIds(ctx, input, enabledOnly);
const facetValues = await this.facetValueService.findByIds(
Array.from(facetValueIdsMap.keys()),
ctx.languageCode,
Expand Down Expand Up @@ -182,6 +183,7 @@ export class FulltextSearchService implements SearchService {
v =>
new SearchIndexItem({
sku: v.sku,
enabled: v.enabled,
slug: v.product.slug,
price: v.price,
priceWithTax: v.priceWithTax,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export class SearchIndexItem {
@Column({ type: idType() })
productId: ID;

@Column()
enabled: boolean;

@Index({ fulltext: true })
@Column()
productName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class MysqlSearchStrategy implements SearchStrategy {

constructor(private connection: Connection) {}

async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>> {
async getFacetValueIds(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<Map<ID, number>> {
const facetValuesQb = this.connection
.getRepository(SearchIndexItem)
.createQueryBuilder('si')
Expand All @@ -28,11 +28,14 @@ export class MysqlSearchStrategy implements SearchStrategy {
if (!input.groupByProduct) {
facetValuesQb.groupBy('productVariantId');
}
if (enabledOnly) {
facetValuesQb.andWhere('si.enabled = :enabled', { enabled: true });
}
const facetValuesResult = await facetValuesQb.getRawMany();
return createFacetIdCountMap(facetValuesResult);
}

async getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]> {
async getSearchResults(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<SearchResult[]> {
const take = input.take || 25;
const skip = input.skip || 0;
const sort = input.sort;
Expand All @@ -55,6 +58,9 @@ export class MysqlSearchStrategy implements SearchStrategy {
qb.addOrderBy('price', sort.price);
}
}
if (enabledOnly) {
qb.andWhere('si.enabled = :enabled', { enabled: true });
}

return qb
.take(take)
Expand All @@ -63,11 +69,14 @@ export class MysqlSearchStrategy implements SearchStrategy {
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
}

async getTotalCount(ctx: RequestContext, input: SearchInput): Promise<number> {
async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
const innerQb = this.applyTermAndFilters(
this.connection.getRepository(SearchIndexItem).createQueryBuilder('si'),
input,
);
if (enabledOnly) {
innerQb.andWhere('si.enabled = :enabled', { enabled: true });
}

const totalItemsQb = this.connection
.createQueryBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class PostgresSearchStrategy implements SearchStrategy {

constructor(private connection: Connection) {}

async getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>> {
async getFacetValueIds(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<Map<ID, number>> {
const facetValuesQb = this.connection
.getRepository(SearchIndexItem)
.createQueryBuilder('si')
Expand All @@ -28,11 +28,14 @@ export class PostgresSearchStrategy implements SearchStrategy {
if (!input.groupByProduct) {
facetValuesQb.groupBy('"si"."productVariantId", "si"."productId"');
}
if (enabledOnly) {
facetValuesQb.andWhere('"si"."enabled" = :enabled', { enabled: true });
}
const facetValuesResult = await facetValuesQb.getRawMany();
return createFacetIdCountMap(facetValuesResult);
}

async getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]> {
async getSearchResults(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<SearchResult[]> {
const take = input.take || 25;
const skip = input.skip || 0;
const sort = input.sort;
Expand All @@ -59,6 +62,9 @@ export class PostgresSearchStrategy implements SearchStrategy {
qb.addOrderBy('"si_price"', sort.price);
}
}
if (enabledOnly) {
qb.andWhere('"si"."enabled" = :enabled', { enabled: true });
}

return qb
.take(take)
Expand All @@ -67,15 +73,17 @@ export class PostgresSearchStrategy implements SearchStrategy {
.then(res => res.map(r => mapToSearchResult(r, ctx.channel.currencyCode)));
}

async getTotalCount(ctx: RequestContext, input: SearchInput): Promise<number> {
async getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number> {
const innerQb = this.applyTermAndFilters(
this.connection
.getRepository(SearchIndexItem)
.createQueryBuilder('si')
.select(this.createPostgresSelect(!!input.groupByProduct)),
input,
);

if (enabledOnly) {
innerQb.andWhere('"si"."enabled" = :enabled', { enabled: true });
}
const totalItemsQb = this.connection
.createQueryBuilder()
.select('COUNT(*) as total')
Expand All @@ -102,8 +110,8 @@ export class PostgresSearchStrategy implements SearchStrategy {
(ts_rank_cd(to_tsvector(${minIfGrouped('si.sku')}), to_tsquery(:term)) * 10 +
ts_rank_cd(to_tsvector(${minIfGrouped('si.productName')}), to_tsquery(:term)) * 2 +
ts_rank_cd(to_tsvector(${minIfGrouped(
'si.productVariantName',
)}), to_tsquery(:term)) * 1.5 +
'si.productVariantName',
)}), to_tsquery(:term)) * 1.5 +
ts_rank_cd(to_tsvector(${minIfGrouped('si.description')}), to_tsquery(:term)) * 1)
`,
'score',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function mapToSearchResult(raw: any, currencyCode: CurrencyCode): SearchR
sku: raw.si_sku,
slug: raw.si_slug,
price,
enabled: raw.si_enabled,
priceWithTax,
currencyCode,
productVariantId: raw.si_productVariantId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { RequestContext } from '../../../api';
* should follow.
*/
export interface SearchStrategy {
getSearchResults(ctx: RequestContext, input: SearchInput): Promise<SearchResult[]>;
getTotalCount(ctx: RequestContext, input: SearchInput): Promise<number>;
getSearchResults(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<SearchResult[]>;
getTotalCount(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<number>;
/**
* Returns a map of `facetValueId` => `count`, providing the number of times that
* facetValue occurs in the result set.
*/
getFacetValueIds(ctx: RequestContext, input: SearchInput): Promise<Map<ID, number>>;
getFacetValueIds(ctx: RequestContext, input: SearchInput, enabledOnly: boolean): Promise<Map<ID, number>>;
}
Loading

0 comments on commit fcd3086

Please sign in to comment.