Browse Source

refactor(server): Separate search plugin into api / service layers

Michael Bromley 7 years ago
parent
commit
8dde64a6e3

+ 8 - 4
server/src/api/resolvers/search.resolver.ts

@@ -1,6 +1,4 @@
-import { Args, Query, Resolver } from '@nestjs/graphql';
-import { InjectConnection } from '@nestjs/typeorm';
-import { Connection } from 'typeorm';
+import { Mutation, Query, Resolver } from '@nestjs/graphql';
 
 
 import { Permission } from '../../../../shared/generated-types';
 import { Permission } from '../../../../shared/generated-types';
 import { Allow } from '../../api/decorators/allow.decorator';
 import { Allow } from '../../api/decorators/allow.decorator';
@@ -10,7 +8,13 @@ import { InternalServerError } from '../../common/error/errors';
 export class SearchResolver {
 export class SearchResolver {
     @Query()
     @Query()
     @Allow(Permission.Public)
     @Allow(Permission.Public)
-    async search(@Args() args: any): Promise<any[]> {
+    async search(...args: any): Promise<any[]> {
+        throw new InternalServerError(`error.no-search-plugin-configured`);
+    }
+
+    @Mutation()
+    @Allow(Permission.UpdateCatalog)
+    async reindex(...args: any[]): Promise<boolean> {
         throw new InternalServerError(`error.no-search-plugin-configured`);
         throw new InternalServerError(`error.no-search-plugin-configured`);
     }
     }
 }
 }

+ 3 - 0
server/src/api/types/search.api.graphql

@@ -1,5 +1,8 @@
 type Query {
 type Query {
     search(input: SearchInput!): [SearchResult!]!
     search(input: SearchInput!): [SearchResult!]!
+}
+
+type Mutation {
     reindex: Boolean!
     reindex: Boolean!
 }
 }
 
 

+ 5 - 74
server/src/plugin/default-search-engine/default-search-plugin.ts

@@ -1,91 +1,22 @@
-import { Connection } from 'typeorm';
-
-import { LanguageCode } from '../../../../shared/generated-types';
 import { Type } from '../../../../shared/shared-types';
 import { Type } from '../../../../shared/shared-types';
-import { unique } from '../../../../shared/unique';
 import { VendureConfig, VendurePlugin } from '../../config';
 import { VendureConfig, VendurePlugin } from '../../config';
-import { FacetValue, Product, ProductVariant } from '../../entity';
-import { EventBus } from '../../event-bus/event-bus';
-import { CatalogModificationEvent } from '../../event-bus/events/catalog-modification-event';
-import { translateDeep } from '../../service/helpers/utils/translate-entity';
 
 
-import { FulltextSearchResolver } from './fulltext-search-resolver';
+import { FulltextSearchResolver } from './fulltext-search.resolver';
+import { FulltextSearchService } from './fulltext-search.service';
 import { SearchIndexItem } from './search-index-item.entity';
 import { SearchIndexItem } from './search-index-item.entity';
 
 
 export class DefaultSearchPlugin implements VendurePlugin {
 export class DefaultSearchPlugin implements VendurePlugin {
-    private connection: Connection;
+    private fulltextSearchService: FulltextSearchService;
 
 
     async init(config: Required<VendureConfig>): Promise<Required<VendureConfig>> {
     async init(config: Required<VendureConfig>): Promise<Required<VendureConfig>> {
         return config;
         return config;
     }
     }
 
 
-    onBootstrap(inject: <T>(type: Type<T>) => T): void | Promise<void> {
-        this.connection = inject(Connection);
-        const eventBus = inject(EventBus);
-
-        eventBus.subscribe(CatalogModificationEvent, event => this.buildSearchIndex(event.ctx.languageCode));
-    }
-
     defineEntities(): Array<Type<any>> {
     defineEntities(): Array<Type<any>> {
         return [SearchIndexItem];
         return [SearchIndexItem];
     }
     }
 
 
-    defineResolvers(): Array<Type<any>> {
-        return [FulltextSearchResolver];
-    }
-
-    /**
-     * Clears the search index for the given language and rebuilds from scratch.
-     */
-    private async buildSearchIndex(languageCode: LanguageCode) {
-        const variants = await this.connection.getRepository(ProductVariant).find({
-            relations: [
-                'product',
-                'product.featuredAsset',
-                'product.facetValues',
-                'product.facetValues.facet',
-                'featuredAsset',
-                'facetValues',
-                'facetValues.facet',
-            ],
-        });
-
-        await this.connection.getRepository(SearchIndexItem).delete({ languageCode });
-        const items = variants
-            .map(v => translateDeep(v, languageCode, ['product']))
-            .map(
-                v =>
-                    new SearchIndexItem({
-                        languageCode,
-                        productVariantId: v.id,
-                        productId: v.product.id,
-                        productName: v.product.name,
-                        description: v.product.description,
-                        productVariantName: v.name,
-                        productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '',
-                        productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '',
-                        facetIds: this.getFacetIds(v),
-                        facetValueIds: this.getFacetValueIds(v),
-                    }),
-            );
-        await this.connection.getRepository(SearchIndexItem).save(items);
-    }
-
-    private async updateSearchIndex(languageCode: LanguageCode, entity: Product | ProductVariant) {
-        //
-    }
-
-    private getFacetIds(variant: ProductVariant): string[] {
-        const facetIds = (fv: FacetValue) => fv.facet.id.toString();
-        const variantFacetIds = variant.facetValues.map(facetIds);
-        const productFacetIds = variant.product.facetValues.map(facetIds);
-        return unique([...variantFacetIds, ...productFacetIds]);
-    }
-
-    private getFacetValueIds(variant: ProductVariant): string[] {
-        const facetValueIds = (fv: FacetValue) => fv.id.toString();
-        const variantFacetValueIds = variant.facetValues.map(facetValueIds);
-        const productFacetValueIds = variant.product.facetValues.map(facetValueIds);
-        return unique([...variantFacetValueIds, ...productFacetValueIds]);
+    defineProviders(): Array<Type<any>> {
+        return [FulltextSearchService, FulltextSearchResolver];
     }
     }
 }
 }

+ 0 - 30
server/src/plugin/default-search-engine/fulltext-search-resolver.ts

@@ -1,30 +0,0 @@
-import { Args, Query, Resolver } from '@nestjs/graphql';
-import { InjectConnection } from '@nestjs/typeorm';
-import { Connection } from 'typeorm';
-
-import { Permission } from '../../../../shared/generated-types';
-import { Allow } from '../../api/decorators/allow.decorator';
-import { SearchResolver as BaseSearchResolver } from '../../api/resolvers/search.resolver';
-
-import { SearchIndexItem } from './search-index-item.entity';
-
-@Resolver()
-export class FulltextSearchResolver extends BaseSearchResolver {
-    constructor(@InjectConnection() private connection: Connection) {
-        super();
-    }
-
-    @Query()
-    @Allow(Permission.Public)
-    async search(@Args() args: any) {
-        return this.connection
-            .getRepository(SearchIndexItem)
-            .createQueryBuilder('si')
-            .addSelect(`MATCH (productName) AGAINST ('${args.input.term}')`, 'score')
-            .orderBy('score', 'DESC')
-            .getMany()
-            .then(res => {
-                return res;
-            });
-    }
-}

+ 28 - 0
server/src/plugin/default-search-engine/fulltext-search.resolver.ts

@@ -0,0 +1,28 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+
+import { Permission } from '../../../../shared/generated-types';
+import { RequestContext } from '../../api/common/request-context';
+import { Allow } from '../../api/decorators/allow.decorator';
+import { Ctx } from '../../api/decorators/request-context.decorator';
+import { SearchResolver as BaseSearchResolver } from '../../api/resolvers/search.resolver';
+
+import { FulltextSearchService } from './fulltext-search.service';
+
+@Resolver()
+export class FulltextSearchResolver extends BaseSearchResolver {
+    constructor(private fulltextSearchService: FulltextSearchService) {
+        super();
+    }
+
+    @Query()
+    @Allow(Permission.Public)
+    async search(@Ctx() ctx: RequestContext, @Args() args: any) {
+        return this.fulltextSearchService.search(ctx, args.input.term);
+    }
+
+    @Mutation()
+    @Allow(Permission.UpdateCatalog)
+    async reindex(@Ctx() ctx: RequestContext): Promise<boolean> {
+        return this.fulltextSearchService.reindex(ctx);
+    }
+}

+ 81 - 0
server/src/plugin/default-search-engine/fulltext-search.service.ts

@@ -0,0 +1,81 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { Connection } from 'typeorm';
+
+import { unique } from '../../../../shared/unique';
+import { RequestContext } from '../../api/common/request-context';
+import { FacetValue, ProductVariant } from '../../entity';
+import { EventBus } from '../../event-bus/event-bus';
+import { CatalogModificationEvent } from '../../event-bus/events/catalog-modification-event';
+import { translateDeep } from '../../service/helpers/utils/translate-entity';
+
+import { SearchIndexItem } from './search-index-item.entity';
+
+@Injectable()
+export class FulltextSearchService {
+    constructor(@InjectConnection() private connection: Connection, private eventBus: EventBus) {
+        eventBus.subscribe(CatalogModificationEvent, event => this.reindex(event.ctx));
+    }
+
+    async search(ctx: RequestContext, term: string) {
+        return this.connection
+            .getRepository(SearchIndexItem)
+            .createQueryBuilder('si')
+            .addSelect(`MATCH (productName) AGAINST ('${term}')`, 'score')
+            .orderBy('score', 'DESC')
+            .getMany()
+            .then(res => {
+                return res;
+            });
+    }
+
+    async reindex(ctx: RequestContext): Promise<boolean> {
+        const { languageCode } = ctx;
+        const variants = await this.connection.getRepository(ProductVariant).find({
+            relations: [
+                'product',
+                'product.featuredAsset',
+                'product.facetValues',
+                'product.facetValues.facet',
+                'featuredAsset',
+                'facetValues',
+                'facetValues.facet',
+            ],
+        });
+
+        await this.connection.getRepository(SearchIndexItem).delete({ languageCode });
+        const items = variants
+            .map(v => translateDeep(v, languageCode, ['product']))
+            .map(
+                v =>
+                    new SearchIndexItem({
+                        languageCode,
+                        productVariantId: v.id,
+                        productId: v.product.id,
+                        productName: v.product.name,
+                        description: v.product.description,
+                        productVariantName: v.name,
+                        productPreview: v.product.featuredAsset ? v.product.featuredAsset.preview : '',
+                        productVariantPreview: v.featuredAsset ? v.featuredAsset.preview : '',
+                        facetIds: this.getFacetIds(v),
+                        facetValueIds: this.getFacetValueIds(v),
+                    }),
+            );
+        await this.connection.getRepository(SearchIndexItem).save(items);
+        return true;
+    }
+
+    private getFacetIds(variant: ProductVariant): string[] {
+        const facetIds = (fv: FacetValue) => fv.facet.id.toString();
+        const variantFacetIds = variant.facetValues.map(facetIds);
+        const productFacetIds = variant.product.facetValues.map(facetIds);
+        return unique([...variantFacetIds, ...productFacetIds]);
+    }
+
+    private getFacetValueIds(variant: ProductVariant): string[] {
+        const facetValueIds = (fv: FacetValue) => fv.id.toString();
+        const variantFacetValueIds = variant.facetValues.map(facetValueIds);
+        const productFacetValueIds = variant.product.facetValues.map(facetValueIds);
+        return unique([...variantFacetValueIds, ...productFacetValueIds]);
+    }
+}