Browse Source

feat(dashboard-plugin): Enhance global search functionality

- Added new mutations for triggering global search index build and rebuild.
- Updated GraphQL schema to include new fields and types for global search results.
- Refactored existing search resolver to accommodate new input structure.
- Introduced a list of non-indexable entities to improve search performance.
- Enhanced indexing and search strategies to support new features.
David Höck 9 months ago
parent
commit
1086571704

+ 13 - 7
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1978,7 +1978,7 @@ export enum GlobalFlag {
 export type GlobalSearchInput = {
   enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
   entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
-  query: Scalars['String']['input'];
+  query?: InputMaybe<Scalars['String']['input']>;
   skip?: InputMaybe<Scalars['Int']['input']>;
   sortDirection?: InputMaybe<GlobalSearchSortDirection>;
   sortField?: InputMaybe<GlobalSearchSortField>;
@@ -1993,9 +1993,13 @@ export type GlobalSearchResult = {
 
 export type GlobalSearchResultItem = {
   __typename?: 'GlobalSearchResultItem';
-  description?: Maybe<Scalars['String']['output']>;
+  data?: Maybe<Scalars['JSON']['output']>;
+  entityCreatedAt: Scalars['DateTime']['output'];
+  entityId: Scalars['ID']['output'];
+  entityType: Scalars['String']['output'];
+  entityUpdatedAt: Scalars['DateTime']['output'];
   id: Scalars['ID']['output'];
-  metadata?: Maybe<Scalars['JSON']['output']>;
+  languageCode: LanguageCode;
   name: Scalars['String']['output'];
 };
 
@@ -2005,10 +2009,10 @@ export enum GlobalSearchSortDirection {
 }
 
 export enum GlobalSearchSortField {
-  CREATED_AT = 'CREATED_AT',
-  ID = 'ID',
-  NAME = 'NAME',
-  UPDATED_AT = 'UPDATED_AT'
+  entityCreatedAt = 'entityCreatedAt',
+  entityUpdatedAt = 'entityUpdatedAt',
+  id = 'id',
+  name = 'name'
 }
 
 export type GlobalSettings = {
@@ -3053,6 +3057,8 @@ export type Mutation = {
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
   transitionPaymentToState: TransitionPaymentToStateResult;
+  triggerGlobalSearchBuildIndex: Scalars['Boolean']['output'];
+  triggerGlobalSearchRebuildIndex: Scalars['Boolean']['output'];
   /** Unsets the billing address for a draft Order */
   unsetDraftOrderBillingAddress: Order;
   /** Unsets the shipping address for a draft Order */

+ 13 - 7
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1931,7 +1931,7 @@ export enum GlobalFlag {
 export type GlobalSearchInput = {
     enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
     entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
-    query: Scalars['String']['input'];
+    query?: InputMaybe<Scalars['String']['input']>;
     skip?: InputMaybe<Scalars['Int']['input']>;
     sortDirection?: InputMaybe<GlobalSearchSortDirection>;
     sortField?: InputMaybe<GlobalSearchSortField>;
@@ -1944,9 +1944,13 @@ export type GlobalSearchResult = {
 };
 
 export type GlobalSearchResultItem = {
-    description?: Maybe<Scalars['String']['output']>;
+    data?: Maybe<Scalars['JSON']['output']>;
+    entityCreatedAt: Scalars['DateTime']['output'];
+    entityId: Scalars['ID']['output'];
+    entityType: Scalars['String']['output'];
+    entityUpdatedAt: Scalars['DateTime']['output'];
     id: Scalars['ID']['output'];
-    metadata?: Maybe<Scalars['JSON']['output']>;
+    languageCode: LanguageCode;
     name: Scalars['String']['output'];
 };
 
@@ -1956,10 +1960,10 @@ export enum GlobalSearchSortDirection {
 }
 
 export enum GlobalSearchSortField {
-    CREATED_AT = 'CREATED_AT',
-    ID = 'ID',
-    NAME = 'NAME',
-    UPDATED_AT = 'UPDATED_AT',
+    entityCreatedAt = 'entityCreatedAt',
+    entityUpdatedAt = 'entityUpdatedAt',
+    id = 'id',
+    name = 'name',
 }
 
 export type GlobalSettings = {
@@ -2977,6 +2981,8 @@ export type Mutation = {
     transitionFulfillmentToState: TransitionFulfillmentToStateResult;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionPaymentToState: TransitionPaymentToStateResult;
+    triggerGlobalSearchBuildIndex: Scalars['Boolean']['output'];
+    triggerGlobalSearchRebuildIndex: Scalars['Boolean']['output'];
     /** Unsets the billing address for a draft Order */
     unsetDraftOrderBillingAddress: Order;
     /** Unsets the shipping address for a draft Order */

+ 13 - 7
packages/common/src/generated-types.ts

@@ -1966,7 +1966,7 @@ export enum GlobalFlag {
 export type GlobalSearchInput = {
   enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
   entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
-  query: Scalars['String']['input'];
+  query?: InputMaybe<Scalars['String']['input']>;
   skip?: InputMaybe<Scalars['Int']['input']>;
   sortDirection?: InputMaybe<GlobalSearchSortDirection>;
   sortField?: InputMaybe<GlobalSearchSortField>;
@@ -1981,9 +1981,13 @@ export type GlobalSearchResult = {
 
 export type GlobalSearchResultItem = {
   __typename?: 'GlobalSearchResultItem';
-  description?: Maybe<Scalars['String']['output']>;
+  data?: Maybe<Scalars['JSON']['output']>;
+  entityCreatedAt: Scalars['DateTime']['output'];
+  entityId: Scalars['ID']['output'];
+  entityType: Scalars['String']['output'];
+  entityUpdatedAt: Scalars['DateTime']['output'];
   id: Scalars['ID']['output'];
-  metadata?: Maybe<Scalars['JSON']['output']>;
+  languageCode: LanguageCode;
   name: Scalars['String']['output'];
 };
 
@@ -1993,10 +1997,10 @@ export enum GlobalSearchSortDirection {
 }
 
 export enum GlobalSearchSortField {
-  CREATED_AT = 'CREATED_AT',
-  ID = 'ID',
-  NAME = 'NAME',
-  UPDATED_AT = 'UPDATED_AT'
+  entityCreatedAt = 'entityCreatedAt',
+  entityUpdatedAt = 'entityUpdatedAt',
+  id = 'id',
+  name = 'name'
 }
 
 export type GlobalSettings = {
@@ -3030,6 +3034,8 @@ export type Mutation = {
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
   transitionPaymentToState: TransitionPaymentToStateResult;
+  triggerGlobalSearchBuildIndex: Scalars['Boolean']['output'];
+  triggerGlobalSearchRebuildIndex: Scalars['Boolean']['output'];
   /** Unsets the billing address for a draft Order */
   unsetDraftOrderBillingAddress: Order;
   /** Unsets the shipping address for a draft Order */

+ 13 - 7
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1931,7 +1931,7 @@ export enum GlobalFlag {
 export type GlobalSearchInput = {
     enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
     entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
-    query: Scalars['String']['input'];
+    query?: InputMaybe<Scalars['String']['input']>;
     skip?: InputMaybe<Scalars['Int']['input']>;
     sortDirection?: InputMaybe<GlobalSearchSortDirection>;
     sortField?: InputMaybe<GlobalSearchSortField>;
@@ -1944,9 +1944,13 @@ export type GlobalSearchResult = {
 };
 
 export type GlobalSearchResultItem = {
-    description?: Maybe<Scalars['String']['output']>;
+    data?: Maybe<Scalars['JSON']['output']>;
+    entityCreatedAt: Scalars['DateTime']['output'];
+    entityId: Scalars['ID']['output'];
+    entityType: Scalars['String']['output'];
+    entityUpdatedAt: Scalars['DateTime']['output'];
     id: Scalars['ID']['output'];
-    metadata?: Maybe<Scalars['JSON']['output']>;
+    languageCode: LanguageCode;
     name: Scalars['String']['output'];
 };
 
@@ -1956,10 +1960,10 @@ export enum GlobalSearchSortDirection {
 }
 
 export enum GlobalSearchSortField {
-    CREATED_AT = 'CREATED_AT',
-    ID = 'ID',
-    NAME = 'NAME',
-    UPDATED_AT = 'UPDATED_AT',
+    entityCreatedAt = 'entityCreatedAt',
+    entityUpdatedAt = 'entityUpdatedAt',
+    id = 'id',
+    name = 'name',
 }
 
 export type GlobalSettings = {
@@ -2977,6 +2981,8 @@ export type Mutation = {
     transitionFulfillmentToState: TransitionFulfillmentToStateResult;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionPaymentToState: TransitionPaymentToStateResult;
+    triggerGlobalSearchBuildIndex: Scalars['Boolean']['output'];
+    triggerGlobalSearchRebuildIndex: Scalars['Boolean']['output'];
     /** Unsets the billing address for a draft Order */
     unsetDraftOrderBillingAddress: Order;
     /** Unsets the shipping address for a draft Order */

+ 16 - 7
packages/dashboard-plugin/src/api/api-extensions.ts

@@ -4,8 +4,12 @@ export const adminApiExtensions = gql`
     type GlobalSearchResultItem {
         id: ID!
         name: String!
-        description: String
-        metadata: JSON
+        data: JSON
+        entityType: String!
+        entityId: ID!
+        entityCreatedAt: DateTime!
+        entityUpdatedAt: DateTime!
+        languageCode: LanguageCode!
     }
 
     type GlobalSearchResult {
@@ -14,10 +18,10 @@ export const adminApiExtensions = gql`
     }
 
     enum GlobalSearchSortField {
-        ID
-        NAME
-        CREATED_AT
-        UPDATED_AT
+        id
+        name
+        entityCreatedAt
+        entityUpdatedAt
     }
 
     enum GlobalSearchSortDirection {
@@ -26,7 +30,7 @@ export const adminApiExtensions = gql`
     }
 
     input GlobalSearchInput {
-        query: String!
+        query: String
         enabledOnly: Boolean
         entityTypes: [String!]
         sortField: GlobalSearchSortField
@@ -38,4 +42,9 @@ export const adminApiExtensions = gql`
     extend type Query {
         globalSearch(input: GlobalSearchInput!): GlobalSearchResult!
     }
+
+    extend type Mutation {
+        triggerGlobalSearchBuildIndex: Boolean!
+        triggerGlobalSearchRebuildIndex: Boolean!
+    }
 `;

+ 7 - 3
packages/dashboard-plugin/src/api/global-search.resolver.ts

@@ -1,5 +1,9 @@
 import { Args, Query, Resolver } from '@nestjs/graphql';
-import { GlobalSearchInput, GlobalSearchResult } from '@vendure/common/lib/generated-types';
+import {
+    GlobalSearchInput,
+    GlobalSearchResult,
+    QueryGlobalSearchArgs,
+} from '@vendure/common/lib/generated-types';
 import { Ctx, RequestContext } from '@vendure/core';
 
 import { SearchService } from '../service/search.service';
@@ -9,7 +13,7 @@ export class GlobalSearchResolver {
     constructor(private readonly searchService: SearchService) {}
 
     @Query()
-    globalSearch(@Ctx() ctx: RequestContext, @Args() args: GlobalSearchInput): Promise<GlobalSearchResult[]> {
-        return this.searchService.search(ctx, args);
+    globalSearch(@Ctx() ctx: RequestContext, @Args() args: QueryGlobalSearchArgs) {
+        return this.searchService.search(ctx, args.input);
     }
 }

+ 21 - 0
packages/dashboard-plugin/src/api/index.resolver.ts

@@ -0,0 +1,21 @@
+import { Mutation, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext } from '@vendure/core';
+
+import { IndexingService } from '../service/indexing.service';
+
+@Resolver()
+export class IndexResolver {
+    constructor(private readonly indexingService: IndexingService) {}
+
+    @Mutation(() => Boolean)
+    async triggerGlobalSearchBuildIndex(@Ctx() ctx: RequestContext) {
+        await this.indexingService.triggerBuildIndex(ctx);
+        return true;
+    }
+
+    @Mutation(() => Boolean)
+    async triggerGlobalSearchRebuildIndex(@Ctx() ctx: RequestContext) {
+        await this.indexingService.triggerRebuildIndex(ctx);
+        return true;
+    }
+}

+ 39 - 0
packages/dashboard-plugin/src/constants.ts

@@ -1,2 +1,41 @@
 export const PLUGIN_INIT_OPTIONS = Symbol('DASHBOARD_PLUGIN_INIT_OPTIONS');
 export const loggerCtx = 'DashboardPlugin';
+
+export const notIndexableEntities = [
+    'GlobalSearchIndexItem',
+    'AuthenticationMethod',
+    'NativeAuthenticationMethod',
+    'Tag',
+    'OrderLineReference',
+    'FulfillmentLine',
+    'Refund',
+    'RefundLine',
+    'Payment',
+    'Surcharge',
+    'OrderModification',
+    'ShippingLine',
+    'StockMovement',
+    'Allocation',
+    'Cancellation',
+    'ExternalAuthenticationMethod',
+    'GlobalSettings',
+    'Session',
+    'AnonymousSession',
+    'StockAdjustment',
+    'Release',
+    'HistoryEntry',
+    'OrderHistoryEntry',
+    'CustomerHistoryEntry',
+    'SearchIndexItem',
+    'JobRecord',
+    'AuthenticatedSession',
+    'ScheduledTaskRecord',
+    'ProductOption',
+    'ProductOptionGroup', // TODO: Remove when https://github.com/vendure-ecommerce/vendure/issues/3489 is done
+    'CollectionAsset',
+    'FacetValue',
+    'OrderLine',
+    'ProductAsset',
+    'ProductVariantPrice',
+    'StockLevel',
+];

+ 4 - 1
packages/dashboard-plugin/src/entities/global-search-index-item.ts

@@ -1,5 +1,5 @@
 import { ID } from '@vendure/common/lib/shared-types';
-import { EntityId, VendureEntity } from '@vendure/core';
+import { EntityId, LanguageCode, VendureEntity } from '@vendure/core';
 import { Column, Entity, Index } from 'typeorm';
 
 @Entity()
@@ -21,6 +21,9 @@ export class GlobalSearchIndexItem extends VendureEntity {
     @Column()
     name: string;
 
+    @Column()
+    languageCode: LanguageCode;
+
     @Column({ nullable: true })
     entityCreatedAt: Date;
 

+ 143 - 43
packages/dashboard-plugin/src/indexing-strategy/db-indexing-strategy.ts

@@ -1,7 +1,19 @@
 import { Injectable } from '@nestjs/common';
-import { Injector, RequestContext, TransactionalConnection, VendureEntity } from '@vendure/core';
-import { getMetadataArgsStorage, EntityMetadata } from 'typeorm';
-
+import {
+    EntityHydrator,
+    GlobalSettingsService,
+    Injector,
+    LanguageCode,
+    Logger,
+    RequestContext,
+    RequestContextService,
+    TransactionalConnection,
+    Translatable,
+    TranslatorService,
+    VendureEntity,
+} from '@vendure/core';
+
+import { loggerCtx, notIndexableEntities } from '../constants';
 import { GlobalSearchIndexItem } from '../entities/global-search-index-item';
 
 import { GlobalIndexingStrategy } from './global-indexing-strategy';
@@ -9,19 +21,28 @@ import { GlobalIndexingStrategy } from './global-indexing-strategy';
 @Injectable()
 export class DbIndexingStrategy implements GlobalIndexingStrategy {
     private connection: TransactionalConnection;
+    private globalSettingsService: GlobalSettingsService;
+    private requestContextService: RequestContextService;
+    private entityHydrator: EntityHydrator;
+    private translator: TranslatorService;
 
-    init(injector: Injector): void | Promise<void> {
+    init(injector: Injector) {
         this.connection = injector.get(TransactionalConnection);
+        this.globalSettingsService = injector.get(GlobalSettingsService);
+        this.requestContextService = injector.get(RequestContextService);
+        this.entityHydrator = injector.get(EntityHydrator);
+        this.translator = injector.get(TranslatorService);
     }
 
-    async removeFromIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void> {
+    async removeFromIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<boolean> {
         await this.connection.getRepository(ctx, GlobalSearchIndexItem).delete({
             entityType,
             entityId,
         });
+        return true;
     }
 
-    async updateIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void> {
+    async updateIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<boolean> {
         const entity = (await this.connection.getRepository(ctx, entityType).findOne({
             where: {
                 id: entityId,
@@ -37,7 +58,7 @@ export class DbIndexingStrategy implements GlobalIndexingStrategy {
                 {
                     entityType,
                     entityId: entity.id,
-                    name: this.getEntityName(entity),
+                    name: String(this.getEntityName(entity)),
                     data: JSON.stringify(this.getEntityData(entity)),
                     entityCreatedAt: entity.createdAt,
                     entityUpdatedAt: entity.updatedAt,
@@ -45,73 +66,152 @@ export class DbIndexingStrategy implements GlobalIndexingStrategy {
             ],
             ['entityType', 'entityId'],
         );
+        return true;
     }
 
-    async rebuildIndex(ctx: RequestContext): Promise<void> {
+    async rebuildIndex(ctx: RequestContext): Promise<boolean> {
         // First clear the existing index
         await this.connection.getRepository(ctx, GlobalSearchIndexItem).clear();
 
         // Then rebuild it from scratch
-        await this.buildIndex(ctx);
+        return this.buildIndex(ctx);
     }
 
-    async buildIndex(ctx: RequestContext): Promise<void> {
+    async buildIndex(ctx: RequestContext): Promise<boolean> {
         const entityMetadatas = this.connection.rawConnection.entityMetadatas;
+        let entitiesIndexed = 0;
 
         for (const entityMetadata of entityMetadatas) {
             const entityType = entityMetadata.targetName;
-            const repository = this.connection.getRepository(
-                ctx,
-                entityMetadata.target as typeof VendureEntity,
-            );
 
-            // Get all entities of this type
-            const entities = await repository.find();
+            if (entityType === '') {
+                Logger.info(`Skipping because it is not a valid entity`, loggerCtx);
+                continue;
+            }
 
-            // Create index items for each entity
-            const indexItems = entities.map(entity => {
-                const indexItem = new GlobalSearchIndexItem({
-                    entityType,
-                    entityId: entity.id,
-                    name: this.getEntityName(entity),
-                    data: JSON.stringify(this.getEntityData(entity)),
-                    entityCreatedAt: entity.createdAt,
-                    entityUpdatedAt: entity.updatedAt,
-                });
-                return indexItem;
-            });
+            if (notIndexableEntities.includes(entityType)) {
+                Logger.info(`Skipping ${entityType} because it is not indexable`, loggerCtx);
+                continue;
+            }
 
-            // Save all index items for this entity type
-            if (indexItems.length > 0) {
-                await this.connection.getRepository(ctx, GlobalSearchIndexItem).save(indexItems);
+            if (entityType.includes('Translation')) {
+                Logger.info(`Skipping ${entityType} because it is a translation`, loggerCtx);
+                continue;
             }
+
+            await this.loopAvailableLanguages(ctx, async languageCode => {
+                Logger.info(`Processing ${entityType} for language ${languageCode}`, loggerCtx);
+                const languageAwareCtx = await this.requestContextService.create({
+                    languageCode,
+                    apiType: ctx.apiType,
+                });
+
+                const repository = this.connection.getRepository(
+                    languageAwareCtx,
+                    entityMetadata.target as typeof VendureEntity,
+                );
+
+                // Process entities in batches to avoid memory issues
+                const pageSize = 100;
+                let page = 0;
+                let hasMore = true;
+                const indexItems: GlobalSearchIndexItem[] = [];
+
+                while (hasMore) {
+                    // Get entities in batches
+                    const entities = await repository.find({
+                        skip: page * pageSize,
+                        take: pageSize,
+                    });
+
+                    Logger.info(`Processing page ${page} of ${entityType}`, loggerCtx);
+                    Logger.debug(`Found ${entities.length} for ${entityType} on page ${page}`, loggerCtx);
+
+                    // If we got fewer entities than the page size, we've reached the end
+                    if (entities.length < pageSize) {
+                        hasMore = false;
+                    }
+
+                    // Create index items for each entity in this batch
+                    const batchIndexItems = await Promise.all(
+                        entities.map(async entity => {
+                            if ('translations' in entity) {
+                                entity = this.translator.translate(
+                                    entity as Translatable & VendureEntity,
+                                    languageAwareCtx,
+                                );
+                            }
+
+                            return new GlobalSearchIndexItem({
+                                entityType,
+                                entityId: entity.id,
+                                name: String(this.getEntityName(entity)),
+                                data: JSON.stringify(this.getEntityData(entity)),
+                                entityCreatedAt: entity.createdAt,
+                                entityUpdatedAt: entity.updatedAt,
+                                languageCode,
+                            });
+                        }),
+                    );
+
+                    entitiesIndexed += batchIndexItems.length;
+
+                    // Add to our collection
+                    indexItems.push(...batchIndexItems);
+
+                    // Move to next page
+                    page++;
+                }
+
+                // Save all index items for this entity type
+                if (indexItems.length > 0) {
+                    await this.connection.getRepository(ctx, GlobalSearchIndexItem).save(indexItems);
+                }
+            });
         }
+
+        Logger.info(`Indexed ${entitiesIndexed} entities`, loggerCtx);
+
+        return true;
     }
 
-    private getEntityName(entity: VendureEntity): string {
-        // Try to get a meaningful name for the entity
-        const entityAny = entity as any;
-        if (entityAny.name) {
-            return entityAny.name;
+    private getEntityName(entity: VendureEntity): string | number {
+        if ('name' in entity && typeof entity.name === 'string') {
+            return entity.name;
         }
-        if (entityAny.title) {
-            return entityAny.title;
+        if ('title' in entity && typeof entity.title === 'string') {
+            return entity.title;
         }
-        if (entityAny.code) {
-            return entityAny.code;
+        if ('code' in entity && typeof entity.code === 'string') {
+            return entity.code;
         }
-        return entity.id.toString();
+
+        return entity.id;
     }
 
     private getEntityData(entity: VendureEntity): Record<string, unknown> {
-        // Get all properties of the entity that are not functions or undefined
         const data: Record<string, unknown> = {};
         const entityAny = entity as any;
-        for (const key in entityAny) {
+        const metadata = this.connection.rawConnection.getMetadata(entity.constructor);
+
+        // Get all property names that are not relations
+        const nonRelationProperties = metadata.columns.map(col => col.propertyName);
+
+        for (const key of nonRelationProperties) {
+            if (key === 'createdAt' || key === 'updatedAt') {
+                continue;
+            }
             if (typeof entityAny[key] !== 'function' && entityAny[key] !== undefined) {
                 data[key] = entityAny[key];
             }
         }
         return data;
     }
+
+    async loopAvailableLanguages(ctx: RequestContext, cb: (languageCode: LanguageCode) => Promise<void>) {
+        const settings = await this.globalSettingsService.getSettings(ctx);
+        for (const languageCode of settings.availableLanguages) {
+            await cb(languageCode);
+        }
+    }
 }

+ 4 - 4
packages/dashboard-plugin/src/indexing-strategy/global-indexing-strategy.ts

@@ -5,20 +5,20 @@ export interface GlobalIndexingStrategy extends InjectableStrategy {
     /**
      * Build the initial search index
      */
-    buildIndex(ctx: RequestContext): Promise<void>;
+    buildIndex(ctx: RequestContext): Promise<boolean>;
 
     /**
      * Update the index with new data
      */
-    updateIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void>;
+    updateIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<boolean>;
 
     /**
      * Remove entities from the index
      */
-    removeFromIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void>;
+    removeFromIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<boolean>;
 
     /**
      * Rebuild the entire index
      */
-    rebuildIndex(ctx: RequestContext): Promise<void>;
+    rebuildIndex(ctx: RequestContext): Promise<boolean>;
 }

+ 34 - 3
packages/dashboard-plugin/src/plugin.ts

@@ -1,7 +1,10 @@
-import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+import { ModuleRef } from '@nestjs/core';
+import { Injector, PluginCommonModule, VendurePlugin } from '@vendure/core';
 
 import { adminApiExtensions } from './api/api-extensions';
 import { GlobalSearchResolver } from './api/global-search.resolver';
+import { IndexResolver } from './api/index.resolver';
 import { PLUGIN_INIT_OPTIONS } from './constants';
 import { GlobalSearchIndexItem } from './entities/global-search-index-item';
 import { IndexingService } from './service/indexing.service';
@@ -39,7 +42,7 @@ import { DashboardPluginOptions } from './types';
     ],
     adminApiExtensions: {
         schema: adminApiExtensions,
-        resolvers: [GlobalSearchResolver],
+        resolvers: [GlobalSearchResolver, IndexResolver],
     },
     entities: [GlobalSearchIndexItem],
     configuration: config => {
@@ -49,13 +52,41 @@ import { DashboardPluginOptions } from './types';
     },
     compatibility: '^3.0.0',
 })
-export class DashboardPlugin {
+export class DashboardPlugin implements OnModuleInit, OnModuleDestroy {
     static options: DashboardPluginOptions;
 
+    constructor(private readonly moduleRef: ModuleRef) {}
+
     static init(options: DashboardPluginOptions) {
         this.options = {
             ...options,
         };
         return DashboardPlugin;
     }
+
+    async onModuleInit() {
+        const injector = new Injector(this.moduleRef);
+        const indexing = DashboardPlugin.options.indexingStrategy;
+        const search = DashboardPlugin.options.searchStrategy;
+
+        if (typeof indexing.init === 'function') {
+            await indexing.init(injector);
+        }
+
+        if (typeof search.init === 'function') {
+            await search.init(injector);
+        }
+    }
+
+    async onModuleDestroy() {
+        const indexing = DashboardPlugin.options.indexingStrategy;
+        const search = DashboardPlugin.options.searchStrategy;
+        if (typeof indexing.destroy === 'function') {
+            await indexing.destroy();
+        }
+
+        if (typeof search.destroy === 'function') {
+            await search.destroy();
+        }
+    }
 }

+ 50 - 10
packages/dashboard-plugin/src/search-strategy/db-search-strategy.ts

@@ -1,21 +1,61 @@
 import { Injectable } from '@nestjs/common';
-import { GlobalSearchInput, GlobalSearchResult } from '@vendure/common/lib/generated-types';
-import { Injector, RequestContext, TransactionalConnection } from '@vendure/core';
+import { GlobalSearchInput, GlobalSearchResultItem } from '@vendure/common/lib/generated-types';
+import { Injector, ListQueryBuilder, Logger, PaginatedList, RequestContext } from '@vendure/core';
+
+import { loggerCtx } from '../constants';
+import { GlobalSearchIndexItem } from '../entities/global-search-index-item';
 
 import { GlobalSearchStrategy } from './global-search-strategy';
 
 @Injectable()
 export class DbSearchStrategy implements GlobalSearchStrategy {
-    private connection: TransactionalConnection;
-
+    private listQueryBuilder: ListQueryBuilder;
     init(injector: Injector): void | Promise<void> {
-        this.connection = injector.get(TransactionalConnection);
+        this.listQueryBuilder = injector.get(ListQueryBuilder);
     }
 
-    getSearchResults(ctx: RequestContext, input: GlobalSearchInput): Promise<GlobalSearchResult[]> {
-        throw new Error('Method not implemented.');
-    }
-    getTotalCount(ctx: RequestContext, input: GlobalSearchInput): Promise<number> {
-        throw new Error('Method not implemented.');
+    async getSearchResults(
+        ctx: RequestContext,
+        input: GlobalSearchInput,
+    ): Promise<PaginatedList<GlobalSearchResultItem>> {
+        const filter = {
+            _or: [
+                {
+                    name: {
+                        contains: input.query,
+                    },
+                },
+                {
+                    data: {
+                        contains: input.query,
+                    },
+                },
+            ],
+            languageCode: {
+                eq: ctx.languageCode,
+            },
+            ...(input.enabledOnly ? { enabled: input.enabledOnly } : {}),
+            ...(input.entityTypes && input.entityTypes.length > 0
+                ? { entityType: { in: input.entityTypes } }
+                : {}),
+        };
+
+        Logger.info(`Filter: ${JSON.stringify(filter)}`, loggerCtx);
+
+        const [items, totalItems] = await this.listQueryBuilder
+            .build(GlobalSearchIndexItem, {
+                take: input.take,
+                skip: input.skip,
+                sort: {
+                    [input.sortField ?? 'entityUpdatedAt']: input.sortDirection ?? 'DESC',
+                },
+                filter,
+            })
+            .getManyAndCount();
+
+        return {
+            items,
+            totalItems,
+        };
     }
 }

+ 10 - 4
packages/dashboard-plugin/src/search-strategy/global-search-strategy.ts

@@ -1,7 +1,13 @@
-import { GlobalSearchInput, GlobalSearchResult } from '@vendure/common/lib/generated-types';
-import { InjectableStrategy, RequestContext } from '@vendure/core';
+import {
+    GlobalSearchInput,
+    GlobalSearchResult,
+    GlobalSearchResultItem,
+} from '@vendure/common/lib/generated-types';
+import { InjectableStrategy, PaginatedList, RequestContext } from '@vendure/core';
 
 export interface GlobalSearchStrategy extends InjectableStrategy {
-    getSearchResults(ctx: RequestContext, input: GlobalSearchInput): Promise<GlobalSearchResult[]>;
-    getTotalCount(ctx: RequestContext, input: GlobalSearchInput): Promise<number>;
+    getSearchResults(
+        ctx: RequestContext,
+        input: GlobalSearchInput,
+    ): Promise<PaginatedList<GlobalSearchResultItem>>;
 }

+ 13 - 6
packages/dashboard-plugin/src/service/indexing.service.ts

@@ -1,7 +1,7 @@
 import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
-import { JobQueue, RequestContext, JobQueueService, SerializedRequestContext } from '@vendure/core';
+import { JobQueue, RequestContext, JobQueueService, SerializedRequestContext, Logger } from '@vendure/core';
 
-import { PLUGIN_INIT_OPTIONS } from '../constants';
+import { loggerCtx, PLUGIN_INIT_OPTIONS } from '../constants';
 import { DashboardPluginOptions } from '../types';
 
 @Injectable()
@@ -32,10 +32,15 @@ export class IndexingService implements OnModuleInit {
     }
 
     async triggerBuildIndex(ctx: RequestContext) {
-        await this.jobQueue.add({
-            operation: 'build',
-            ctx: ctx.serialize(),
-        });
+        await this.jobQueue.add(
+            {
+                operation: 'build',
+                ctx: ctx.serialize(),
+            },
+            {
+                retries: 3,
+            },
+        );
     }
 
     async triggerRebuildIndex(ctx: RequestContext) {
@@ -47,11 +52,13 @@ export class IndexingService implements OnModuleInit {
 
     async buildIndex(ctx: RequestContext) {
         const indexingStrategy = this.options.indexingStrategy;
+        Logger.info(`Building global search index with ${indexingStrategy.constructor.name}`, loggerCtx);
         await indexingStrategy.buildIndex(ctx);
     }
 
     async rebuildIndex(ctx: RequestContext) {
         const indexingStrategy = this.options.indexingStrategy;
+        Logger.info(`Rebuilding global search index with ${indexingStrategy.constructor.name}`, loggerCtx);
         await indexingStrategy.rebuildIndex(ctx);
     }
 

+ 7 - 5
packages/dev-server/dev-config.ts

@@ -5,23 +5,21 @@ import { ADMIN_API_PATH, API_PORT, SHOP_API_PATH } from '@vendure/common/lib/sha
 import {
     DefaultJobQueuePlugin,
     DefaultLogger,
+    DefaultSchedulerPlugin,
     DefaultSearchPlugin,
     dummyPaymentHandler,
     LanguageCode,
     LogLevel,
-    DefaultSchedulerPlugin,
     VendureConfig,
-    cleanSessionsTask,
 } from '@vendure/core';
 import { ScheduledTask } from '@vendure/core/dist/scheduler/scheduled-task';
+import { DashboardPlugin, DbIndexingStrategy, DbSearchStrategy } from '@vendure/dashboard-plugin';
 import { defaultEmailHandlers, EmailPlugin, FileBasedTemplateLoader } from '@vendure/email-plugin';
-import 'dotenv/config';
 import { GraphiqlPlugin } from '@vendure/graphiql-plugin';
+import 'dotenv/config';
 import path from 'path';
 import { DataSourceOptions } from 'typeorm';
 
-import { MultivendorPlugin } from './example-plugins/multivendor-plugin/multivendor.plugin';
-
 /**
  * Config settings used during development
  */
@@ -170,6 +168,10 @@ export const devConfig: VendureConfig = {
             //     devMode: true,
             // }),
         }),
+        DashboardPlugin.init({
+            searchStrategy: new DbSearchStrategy(),
+            indexingStrategy: new DbIndexingStrategy(),
+        }),
     ],
 };
 

+ 13 - 7
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1931,7 +1931,7 @@ export enum GlobalFlag {
 export type GlobalSearchInput = {
     enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
     entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
-    query: Scalars['String']['input'];
+    query?: InputMaybe<Scalars['String']['input']>;
     skip?: InputMaybe<Scalars['Int']['input']>;
     sortDirection?: InputMaybe<GlobalSearchSortDirection>;
     sortField?: InputMaybe<GlobalSearchSortField>;
@@ -1944,9 +1944,13 @@ export type GlobalSearchResult = {
 };
 
 export type GlobalSearchResultItem = {
-    description?: Maybe<Scalars['String']['output']>;
+    data?: Maybe<Scalars['JSON']['output']>;
+    entityCreatedAt: Scalars['DateTime']['output'];
+    entityId: Scalars['ID']['output'];
+    entityType: Scalars['String']['output'];
+    entityUpdatedAt: Scalars['DateTime']['output'];
     id: Scalars['ID']['output'];
-    metadata?: Maybe<Scalars['JSON']['output']>;
+    languageCode: LanguageCode;
     name: Scalars['String']['output'];
 };
 
@@ -1956,10 +1960,10 @@ export enum GlobalSearchSortDirection {
 }
 
 export enum GlobalSearchSortField {
-    CREATED_AT = 'CREATED_AT',
-    ID = 'ID',
-    NAME = 'NAME',
-    UPDATED_AT = 'UPDATED_AT',
+    entityCreatedAt = 'entityCreatedAt',
+    entityUpdatedAt = 'entityUpdatedAt',
+    id = 'id',
+    name = 'name',
 }
 
 export type GlobalSettings = {
@@ -2977,6 +2981,8 @@ export type Mutation = {
     transitionFulfillmentToState: TransitionFulfillmentToStateResult;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionPaymentToState: TransitionPaymentToStateResult;
+    triggerGlobalSearchBuildIndex: Scalars['Boolean']['output'];
+    triggerGlobalSearchRebuildIndex: Scalars['Boolean']['output'];
     /** Unsets the billing address for a draft Order */
     unsetDraftOrderBillingAddress: Order;
     /** Unsets the shipping address for a draft Order */

+ 13 - 7
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -1936,7 +1936,7 @@ export enum GlobalFlag {
 export type GlobalSearchInput = {
     enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
     entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
-    query: Scalars['String']['input'];
+    query?: InputMaybe<Scalars['String']['input']>;
     skip?: InputMaybe<Scalars['Int']['input']>;
     sortDirection?: InputMaybe<GlobalSearchSortDirection>;
     sortField?: InputMaybe<GlobalSearchSortField>;
@@ -1949,9 +1949,13 @@ export type GlobalSearchResult = {
 };
 
 export type GlobalSearchResultItem = {
-    description?: Maybe<Scalars['String']['output']>;
+    data?: Maybe<Scalars['JSON']['output']>;
+    entityCreatedAt: Scalars['DateTime']['output'];
+    entityId: Scalars['ID']['output'];
+    entityType: Scalars['String']['output'];
+    entityUpdatedAt: Scalars['DateTime']['output'];
     id: Scalars['ID']['output'];
-    metadata?: Maybe<Scalars['JSON']['output']>;
+    languageCode: LanguageCode;
     name: Scalars['String']['output'];
 };
 
@@ -1961,10 +1965,10 @@ export enum GlobalSearchSortDirection {
 }
 
 export enum GlobalSearchSortField {
-    CREATED_AT = 'CREATED_AT',
-    ID = 'ID',
-    NAME = 'NAME',
-    UPDATED_AT = 'UPDATED_AT',
+    entityCreatedAt = 'entityCreatedAt',
+    entityUpdatedAt = 'entityUpdatedAt',
+    id = 'id',
+    name = 'name',
 }
 
 export type GlobalSettings = {
@@ -3043,6 +3047,8 @@ export type Mutation = {
     transitionFulfillmentToState: TransitionFulfillmentToStateResult;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionPaymentToState: TransitionPaymentToStateResult;
+    triggerGlobalSearchBuildIndex: Scalars['Boolean']['output'];
+    triggerGlobalSearchRebuildIndex: Scalars['Boolean']['output'];
     /** Unsets the billing address for a draft Order */
     unsetDraftOrderBillingAddress: Order;
     /** Unsets the shipping address for a draft Order */

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


Some files were not shown because too many files changed in this diff