소스 검색

feat(dashboard-plugin): Implement basic global search functionality

David Höck 9 달 전
부모
커밋
9c75ed9879
26개의 변경된 파일1043개의 추가작업 그리고 76개의 파일을 삭제
  1. 408 68
      package-lock.json
  2. 42 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  3. 39 0
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  4. 42 0
      packages/common/src/generated-types.ts
  5. 39 0
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  6. 2 3
      packages/dashboard-plugin/package.json
  7. 41 0
      packages/dashboard-plugin/src/api/api-extensions.ts
  8. 15 0
      packages/dashboard-plugin/src/api/global-search.resolver.ts
  9. 29 0
      packages/dashboard-plugin/src/entities/global-search-index-item.ts
  10. 4 0
      packages/dashboard-plugin/src/index.ts
  11. 117 0
      packages/dashboard-plugin/src/indexing-strategy/db-indexing-strategy.ts
  12. 24 0
      packages/dashboard-plugin/src/indexing-strategy/global-indexing-strategy.ts
  13. 2 0
      packages/dashboard-plugin/src/indexing-strategy/index.ts
  14. 14 2
      packages/dashboard-plugin/src/plugin.ts
  15. 21 0
      packages/dashboard-plugin/src/search-strategy/db-search-strategy.ts
  16. 7 0
      packages/dashboard-plugin/src/search-strategy/global-search-strategy.ts
  17. 2 0
      packages/dashboard-plugin/src/search-strategy/index.ts
  18. 67 0
      packages/dashboard-plugin/src/service/indexing.service.ts
  19. 20 0
      packages/dashboard-plugin/src/service/search.service.ts
  20. 14 0
      packages/dashboard-plugin/src/tasks/build-index.task.ts
  21. 7 1
      packages/dashboard-plugin/src/types.ts
  22. 1 1
      packages/dashboard-plugin/tsconfig.build.json
  23. 39 0
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  24. 39 0
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  25. 0 0
      schema-admin.json
  26. 8 1
      scripts/codegen/download-introspection-schema.ts

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 408 - 68
package-lock.json


+ 42 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1975,6 +1975,42 @@ export enum GlobalFlag {
   TRUE = 'TRUE'
 }
 
+export type GlobalSearchInput = {
+  enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
+  entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
+  query: Scalars['String']['input'];
+  skip?: InputMaybe<Scalars['Int']['input']>;
+  sortDirection?: InputMaybe<GlobalSearchSortDirection>;
+  sortField?: InputMaybe<GlobalSearchSortField>;
+  take?: InputMaybe<Scalars['Int']['input']>;
+};
+
+export type GlobalSearchResult = {
+  __typename?: 'GlobalSearchResult';
+  items: Array<GlobalSearchResultItem>;
+  totalItems: Scalars['Int']['output'];
+};
+
+export type GlobalSearchResultItem = {
+  __typename?: 'GlobalSearchResultItem';
+  description?: Maybe<Scalars['String']['output']>;
+  id: Scalars['ID']['output'];
+  metadata?: Maybe<Scalars['JSON']['output']>;
+  name: Scalars['String']['output'];
+};
+
+export enum GlobalSearchSortDirection {
+  ASC = 'ASC',
+  DESC = 'DESC'
+}
+
+export enum GlobalSearchSortField {
+  CREATED_AT = 'CREATED_AT',
+  ID = 'ID',
+  NAME = 'NAME',
+  UPDATED_AT = 'UPDATED_AT'
+}
+
 export type GlobalSettings = {
   __typename?: 'GlobalSettings';
   availableLanguages: Array<LanguageCode>;
@@ -5198,6 +5234,7 @@ export type Query = {
   facetValues: FacetValueList;
   facets: FacetList;
   fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
+  globalSearch: GlobalSearchResult;
   globalSettings: GlobalSettings;
   job?: Maybe<Job>;
   jobBufferSize: Array<JobBufferSize>;
@@ -5351,6 +5388,11 @@ export type QueryFacetsArgs = {
 };
 
 
+export type QueryGlobalSearchArgs = {
+  input: GlobalSearchInput;
+};
+
+
 export type QueryJobArgs = {
   jobId: Scalars['ID']['input'];
 };

+ 39 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -1928,6 +1928,40 @@ export enum GlobalFlag {
     TRUE = 'TRUE',
 }
 
+export type GlobalSearchInput = {
+    enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
+    entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
+    query: Scalars['String']['input'];
+    skip?: InputMaybe<Scalars['Int']['input']>;
+    sortDirection?: InputMaybe<GlobalSearchSortDirection>;
+    sortField?: InputMaybe<GlobalSearchSortField>;
+    take?: InputMaybe<Scalars['Int']['input']>;
+};
+
+export type GlobalSearchResult = {
+    items: Array<GlobalSearchResultItem>;
+    totalItems: Scalars['Int']['output'];
+};
+
+export type GlobalSearchResultItem = {
+    description?: Maybe<Scalars['String']['output']>;
+    id: Scalars['ID']['output'];
+    metadata?: Maybe<Scalars['JSON']['output']>;
+    name: Scalars['String']['output'];
+};
+
+export enum GlobalSearchSortDirection {
+    ASC = 'ASC',
+    DESC = 'DESC',
+}
+
+export enum GlobalSearchSortField {
+    CREATED_AT = 'CREATED_AT',
+    ID = 'ID',
+    NAME = 'NAME',
+    UPDATED_AT = 'UPDATED_AT',
+}
+
 export type GlobalSettings = {
     availableLanguages: Array<LanguageCode>;
     createdAt: Scalars['DateTime']['output'];
@@ -4860,6 +4894,7 @@ export type Query = {
     facetValues: FacetValueList;
     facets: FacetList;
     fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
+    globalSearch: GlobalSearchResult;
     globalSettings: GlobalSettings;
     job?: Maybe<Job>;
     jobBufferSize: Array<JobBufferSize>;
@@ -4991,6 +5026,10 @@ export type QueryFacetsArgs = {
     options?: InputMaybe<FacetListOptions>;
 };
 
+export type QueryGlobalSearchArgs = {
+    input: GlobalSearchInput;
+};
+
 export type QueryJobArgs = {
     jobId: Scalars['ID']['input'];
 };

+ 42 - 0
packages/common/src/generated-types.ts

@@ -1963,6 +1963,42 @@ export enum GlobalFlag {
   TRUE = 'TRUE'
 }
 
+export type GlobalSearchInput = {
+  enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
+  entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
+  query: Scalars['String']['input'];
+  skip?: InputMaybe<Scalars['Int']['input']>;
+  sortDirection?: InputMaybe<GlobalSearchSortDirection>;
+  sortField?: InputMaybe<GlobalSearchSortField>;
+  take?: InputMaybe<Scalars['Int']['input']>;
+};
+
+export type GlobalSearchResult = {
+  __typename?: 'GlobalSearchResult';
+  items: Array<GlobalSearchResultItem>;
+  totalItems: Scalars['Int']['output'];
+};
+
+export type GlobalSearchResultItem = {
+  __typename?: 'GlobalSearchResultItem';
+  description?: Maybe<Scalars['String']['output']>;
+  id: Scalars['ID']['output'];
+  metadata?: Maybe<Scalars['JSON']['output']>;
+  name: Scalars['String']['output'];
+};
+
+export enum GlobalSearchSortDirection {
+  ASC = 'ASC',
+  DESC = 'DESC'
+}
+
+export enum GlobalSearchSortField {
+  CREATED_AT = 'CREATED_AT',
+  ID = 'ID',
+  NAME = 'NAME',
+  UPDATED_AT = 'UPDATED_AT'
+}
+
 export type GlobalSettings = {
   __typename?: 'GlobalSettings';
   availableLanguages: Array<LanguageCode>;
@@ -5123,6 +5159,7 @@ export type Query = {
   facetValues: FacetValueList;
   facets: FacetList;
   fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
+  globalSearch: GlobalSearchResult;
   globalSettings: GlobalSettings;
   job?: Maybe<Job>;
   jobBufferSize: Array<JobBufferSize>;
@@ -5273,6 +5310,11 @@ export type QueryFacetsArgs = {
 };
 
 
+export type QueryGlobalSearchArgs = {
+  input: GlobalSearchInput;
+};
+
+
 export type QueryJobArgs = {
   jobId: Scalars['ID']['input'];
 };

+ 39 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1928,6 +1928,40 @@ export enum GlobalFlag {
     TRUE = 'TRUE',
 }
 
+export type GlobalSearchInput = {
+    enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
+    entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
+    query: Scalars['String']['input'];
+    skip?: InputMaybe<Scalars['Int']['input']>;
+    sortDirection?: InputMaybe<GlobalSearchSortDirection>;
+    sortField?: InputMaybe<GlobalSearchSortField>;
+    take?: InputMaybe<Scalars['Int']['input']>;
+};
+
+export type GlobalSearchResult = {
+    items: Array<GlobalSearchResultItem>;
+    totalItems: Scalars['Int']['output'];
+};
+
+export type GlobalSearchResultItem = {
+    description?: Maybe<Scalars['String']['output']>;
+    id: Scalars['ID']['output'];
+    metadata?: Maybe<Scalars['JSON']['output']>;
+    name: Scalars['String']['output'];
+};
+
+export enum GlobalSearchSortDirection {
+    ASC = 'ASC',
+    DESC = 'DESC',
+}
+
+export enum GlobalSearchSortField {
+    CREATED_AT = 'CREATED_AT',
+    ID = 'ID',
+    NAME = 'NAME',
+    UPDATED_AT = 'UPDATED_AT',
+}
+
 export type GlobalSettings = {
     availableLanguages: Array<LanguageCode>;
     createdAt: Scalars['DateTime']['output'];
@@ -4860,6 +4894,7 @@ export type Query = {
     facetValues: FacetValueList;
     facets: FacetList;
     fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
+    globalSearch: GlobalSearchResult;
     globalSettings: GlobalSettings;
     job?: Maybe<Job>;
     jobBufferSize: Array<JobBufferSize>;
@@ -4991,6 +5026,10 @@ export type QueryFacetsArgs = {
     options?: InputMaybe<FacetListOptions>;
 };
 
+export type QueryGlobalSearchArgs = {
+    input: GlobalSearchInput;
+};
+
 export type QueryJobArgs = {
     jobId: Scalars['ID']['input'];
 };

+ 2 - 3
packages/dashboard-plugin/package.json

@@ -12,10 +12,9 @@
     },
     "license": "GPL-3.0-or-later",
     "scripts": {
-        "build": "rimraf dist && node -r ts-node/register build.ts && npm run compile",
+        "build": "rimraf dist && tsc -p ./tsconfig.build.json",
         "watch": "tsc -p ./tsconfig.build.json --watch",
-        "lint": "eslint --fix .",
-        "compile": "tsc -p ./tsconfig.build.json"
+        "lint": "eslint --fix ."
     },
     "homepage": "https://www.vendure.io",
     "funding": "https://github.com/sponsors/michaelbromley",

+ 41 - 0
packages/dashboard-plugin/src/api/api-extensions.ts

@@ -0,0 +1,41 @@
+import gql from 'graphql-tag';
+
+export const adminApiExtensions = gql`
+    type GlobalSearchResultItem {
+        id: ID!
+        name: String!
+        description: String
+        metadata: JSON
+    }
+
+    type GlobalSearchResult {
+        items: [GlobalSearchResultItem!]!
+        totalItems: Int!
+    }
+
+    enum GlobalSearchSortField {
+        ID
+        NAME
+        CREATED_AT
+        UPDATED_AT
+    }
+
+    enum GlobalSearchSortDirection {
+        ASC
+        DESC
+    }
+
+    input GlobalSearchInput {
+        query: String!
+        enabledOnly: Boolean
+        entityTypes: [String!]
+        sortField: GlobalSearchSortField
+        sortDirection: GlobalSearchSortDirection
+        take: Int
+        skip: Int
+    }
+
+    extend type Query {
+        globalSearch(input: GlobalSearchInput!): GlobalSearchResult!
+    }
+`;

+ 15 - 0
packages/dashboard-plugin/src/api/global-search.resolver.ts

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

+ 29 - 0
packages/dashboard-plugin/src/entities/global-search-index-item.ts

@@ -0,0 +1,29 @@
+import { ID } from '@vendure/common/lib/shared-types';
+import { EntityId, VendureEntity } from '@vendure/core';
+import { Column, Entity, Index } from 'typeorm';
+
+@Entity()
+export class GlobalSearchIndexItem extends VendureEntity {
+    constructor(input?: Partial<GlobalSearchIndexItem>) {
+        super(input);
+    }
+
+    @EntityId()
+    entityId: ID;
+
+    @Index()
+    @Column()
+    entityType: string;
+
+    @Column('text')
+    data: string;
+
+    @Column()
+    name: string;
+
+    @Column({ nullable: true })
+    entityCreatedAt: Date;
+
+    @Column({ nullable: true })
+    entityUpdatedAt: Date;
+}

+ 4 - 0
packages/dashboard-plugin/src/index.ts

@@ -1,2 +1,6 @@
 export * from './plugin';
 export * from './types';
+export * from './service/indexing.service';
+export * from './service/search.service';
+export * from './search-strategy';
+export * from './indexing-strategy';

+ 117 - 0
packages/dashboard-plugin/src/indexing-strategy/db-indexing-strategy.ts

@@ -0,0 +1,117 @@
+import { Injectable } from '@nestjs/common';
+import { Injector, RequestContext, TransactionalConnection, VendureEntity } from '@vendure/core';
+import { getMetadataArgsStorage, EntityMetadata } from 'typeorm';
+
+import { GlobalSearchIndexItem } from '../entities/global-search-index-item';
+
+import { GlobalIndexingStrategy } from './global-indexing-strategy';
+
+@Injectable()
+export class DbIndexingStrategy implements GlobalIndexingStrategy {
+    private connection: TransactionalConnection;
+
+    init(injector: Injector): void | Promise<void> {
+        this.connection = injector.get(TransactionalConnection);
+    }
+
+    async removeFromIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void> {
+        await this.connection.getRepository(ctx, GlobalSearchIndexItem).delete({
+            entityType,
+            entityId,
+        });
+    }
+
+    async updateIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void> {
+        const entity = (await this.connection.getRepository(ctx, entityType).findOne({
+            where: {
+                id: entityId,
+            },
+        })) as VendureEntity;
+
+        if (!entity) {
+            throw new Error(`Entity with id ${entityId} not found`);
+        }
+
+        await this.connection.getRepository(ctx, GlobalSearchIndexItem).upsert(
+            [
+                {
+                    entityType,
+                    entityId: entity.id,
+                    name: this.getEntityName(entity),
+                    data: JSON.stringify(this.getEntityData(entity)),
+                    entityCreatedAt: entity.createdAt,
+                    entityUpdatedAt: entity.updatedAt,
+                },
+            ],
+            ['entityType', 'entityId'],
+        );
+    }
+
+    async rebuildIndex(ctx: RequestContext): Promise<void> {
+        // First clear the existing index
+        await this.connection.getRepository(ctx, GlobalSearchIndexItem).clear();
+
+        // Then rebuild it from scratch
+        await this.buildIndex(ctx);
+    }
+
+    async buildIndex(ctx: RequestContext): Promise<void> {
+        const entityMetadatas = this.connection.rawConnection.entityMetadatas;
+
+        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();
+
+            // 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;
+            });
+
+            // Save all index items for this entity type
+            if (indexItems.length > 0) {
+                await this.connection.getRepository(ctx, GlobalSearchIndexItem).save(indexItems);
+            }
+        }
+    }
+
+    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;
+        }
+        if (entityAny.title) {
+            return entityAny.title;
+        }
+        if (entityAny.code) {
+            return entityAny.code;
+        }
+        return entity.id.toString();
+    }
+
+    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) {
+            if (typeof entityAny[key] !== 'function' && entityAny[key] !== undefined) {
+                data[key] = entityAny[key];
+            }
+        }
+        return data;
+    }
+}

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

@@ -0,0 +1,24 @@
+import { GlobalSearchInput } from '@vendure/common/lib/generated-types';
+import { InjectableStrategy, RequestContext } from '@vendure/core';
+
+export interface GlobalIndexingStrategy extends InjectableStrategy {
+    /**
+     * Build the initial search index
+     */
+    buildIndex(ctx: RequestContext): Promise<void>;
+
+    /**
+     * Update the index with new data
+     */
+    updateIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void>;
+
+    /**
+     * Remove entities from the index
+     */
+    removeFromIndex(ctx: RequestContext, entityType: string, entityId: string): Promise<void>;
+
+    /**
+     * Rebuild the entire index
+     */
+    rebuildIndex(ctx: RequestContext): Promise<void>;
+}

+ 2 - 0
packages/dashboard-plugin/src/indexing-strategy/index.ts

@@ -0,0 +1,2 @@
+export * from './global-indexing-strategy';
+export * from './db-indexing-strategy';

+ 14 - 2
packages/dashboard-plugin/src/plugin.ts

@@ -1,6 +1,11 @@
 import { PluginCommonModule, VendurePlugin } from '@vendure/core';
 
-import { PLUGIN_INIT_OPTIONS, loggerCtx } from './constants';
+import { adminApiExtensions } from './api/api-extensions';
+import { GlobalSearchResolver } from './api/global-search.resolver';
+import { PLUGIN_INIT_OPTIONS } from './constants';
+import { GlobalSearchIndexItem } from './entities/global-search-index-item';
+import { IndexingService } from './service/indexing.service';
+import { SearchService } from './service/search.service';
 import { DashboardPluginOptions } from './types';
 
 /**
@@ -29,7 +34,14 @@ import { DashboardPluginOptions } from './types';
             provide: PLUGIN_INIT_OPTIONS,
             useFactory: () => DashboardPlugin.options,
         },
+        SearchService,
+        IndexingService,
     ],
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [GlobalSearchResolver],
+    },
+    entities: [GlobalSearchIndexItem],
     configuration: config => {
         // Plugin configuration logic here
         return config;
@@ -39,7 +51,7 @@ import { DashboardPluginOptions } from './types';
 export class DashboardPlugin {
     static options: DashboardPluginOptions;
 
-    static init(options: DashboardPluginOptions = {}) {
+    static init(options: DashboardPluginOptions) {
         this.options = {
             ...options,
         };

+ 21 - 0
packages/dashboard-plugin/src/search-strategy/db-search-strategy.ts

@@ -0,0 +1,21 @@
+import { Injectable } from '@nestjs/common';
+import { GlobalSearchInput, GlobalSearchResult } from '@vendure/common/lib/generated-types';
+import { Injector, RequestContext, TransactionalConnection } from '@vendure/core';
+
+import { GlobalSearchStrategy } from './global-search-strategy';
+
+@Injectable()
+export class DbSearchStrategy implements GlobalSearchStrategy {
+    private connection: TransactionalConnection;
+
+    init(injector: Injector): void | Promise<void> {
+        this.connection = injector.get(TransactionalConnection);
+    }
+
+    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.');
+    }
+}

+ 7 - 0
packages/dashboard-plugin/src/search-strategy/global-search-strategy.ts

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

+ 2 - 0
packages/dashboard-plugin/src/search-strategy/index.ts

@@ -0,0 +1,2 @@
+export * from './global-search-strategy';
+export * from './db-search-strategy';

+ 67 - 0
packages/dashboard-plugin/src/service/indexing.service.ts

@@ -0,0 +1,67 @@
+import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
+import { JobQueue, RequestContext, JobQueueService, SerializedRequestContext } from '@vendure/core';
+
+import { PLUGIN_INIT_OPTIONS } from '../constants';
+import { DashboardPluginOptions } from '../types';
+
+@Injectable()
+export class IndexingService implements OnModuleInit {
+    private jobQueue: JobQueue<{ operation: 'build' | 'rebuild'; ctx: SerializedRequestContext }>;
+
+    constructor(
+        @Inject(PLUGIN_INIT_OPTIONS) private readonly options: DashboardPluginOptions,
+        private readonly jobQueueService: JobQueueService,
+    ) {}
+
+    async onModuleInit() {
+        this.jobQueue = await this.jobQueueService.createQueue({
+            name: 'global-search-index',
+            process: async job => {
+                const { operation } = job.data;
+                const ctx = RequestContext.deserialize(job.data.ctx);
+
+                if (operation === 'build') {
+                    await this.buildIndex(ctx);
+                } else if (operation === 'rebuild') {
+                    await this.rebuildIndex(ctx);
+                } else {
+                    throw new Error(`Unknown operation: ${String(operation)}`);
+                }
+            },
+        });
+    }
+
+    async triggerBuildIndex(ctx: RequestContext) {
+        await this.jobQueue.add({
+            operation: 'build',
+            ctx: ctx.serialize(),
+        });
+    }
+
+    async triggerRebuildIndex(ctx: RequestContext) {
+        await this.jobQueue.add({
+            operation: 'rebuild',
+            ctx: ctx.serialize(),
+        });
+    }
+
+    async buildIndex(ctx: RequestContext) {
+        const indexingStrategy = this.options.indexingStrategy;
+        await indexingStrategy.buildIndex(ctx);
+    }
+
+    async rebuildIndex(ctx: RequestContext) {
+        const indexingStrategy = this.options.indexingStrategy;
+        await indexingStrategy.rebuildIndex(ctx);
+    }
+
+    async updateIndex(ctx: RequestContext, entityType: string, entityId: string) {
+        const indexingStrategy = this.options.indexingStrategy;
+        await indexingStrategy.updateIndex(ctx, entityType, entityId);
+    }
+
+    async removeFromIndex(ctx: RequestContext, entityType: string, entityId: string) {
+        const indexingStrategy = this.options.indexingStrategy;
+        await indexingStrategy.removeFromIndex(ctx, entityType, entityId);
+    }
+}

+ 20 - 0
packages/dashboard-plugin/src/service/search.service.ts

@@ -0,0 +1,20 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { GlobalSearchInput } from '@vendure/common/lib/generated-types';
+import { RequestContext } from '@vendure/core';
+
+import { PLUGIN_INIT_OPTIONS } from '../constants';
+import { GlobalSearchStrategy } from '../search-strategy/global-search-strategy';
+import { DashboardPluginOptions } from '../types';
+
+@Injectable()
+export class SearchService {
+    private readonly searchStrategy: GlobalSearchStrategy;
+
+    constructor(@Inject(PLUGIN_INIT_OPTIONS) private readonly options: DashboardPluginOptions) {
+        this.searchStrategy = options.searchStrategy;
+    }
+
+    async search(ctx: RequestContext, input: GlobalSearchInput) {
+        return this.searchStrategy.getSearchResults(ctx, input);
+    }
+}

+ 14 - 0
packages/dashboard-plugin/src/tasks/build-index.task.ts

@@ -0,0 +1,14 @@
+import { Injectable } from '@nestjs/common';
+import { Injector, ScheduledTask } from '@vendure/core';
+
+import { IndexingService } from '../service/indexing.service';
+
+export const buildIndexTask = new ScheduledTask({
+    id: 'global-search-build-index',
+    description: 'Builds the global search index',
+    schedule: cron => cron.everyDayAt(2, 0),
+    execute: async (injector: Injector, params) => {
+        const indexingService = injector.get(IndexingService);
+        await indexingService.triggerBuildIndex(params.ctx);
+    },
+});

+ 7 - 1
packages/dashboard-plugin/src/types.ts

@@ -1,4 +1,10 @@
+import { GlobalIndexingStrategy } from './indexing-strategy/global-indexing-strategy';
+import { GlobalSearchStrategy } from './search-strategy/global-search-strategy';
+
 /**
  * Configuration options for the DashboardPlugin
  */
-export interface DashboardPluginOptions {}
+export interface DashboardPluginOptions {
+    searchStrategy: GlobalSearchStrategy;
+    indexingStrategy: GlobalIndexingStrategy;
+}

+ 1 - 1
packages/dashboard-plugin/tsconfig.build.json

@@ -3,5 +3,5 @@
     "compilerOptions": {
         "outDir": "./dist"
     },
-    "files": ["./index.ts"]
+    "files": ["./src/index.ts"]
 }

+ 39 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -1928,6 +1928,40 @@ export enum GlobalFlag {
     TRUE = 'TRUE',
 }
 
+export type GlobalSearchInput = {
+    enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
+    entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
+    query: Scalars['String']['input'];
+    skip?: InputMaybe<Scalars['Int']['input']>;
+    sortDirection?: InputMaybe<GlobalSearchSortDirection>;
+    sortField?: InputMaybe<GlobalSearchSortField>;
+    take?: InputMaybe<Scalars['Int']['input']>;
+};
+
+export type GlobalSearchResult = {
+    items: Array<GlobalSearchResultItem>;
+    totalItems: Scalars['Int']['output'];
+};
+
+export type GlobalSearchResultItem = {
+    description?: Maybe<Scalars['String']['output']>;
+    id: Scalars['ID']['output'];
+    metadata?: Maybe<Scalars['JSON']['output']>;
+    name: Scalars['String']['output'];
+};
+
+export enum GlobalSearchSortDirection {
+    ASC = 'ASC',
+    DESC = 'DESC',
+}
+
+export enum GlobalSearchSortField {
+    CREATED_AT = 'CREATED_AT',
+    ID = 'ID',
+    NAME = 'NAME',
+    UPDATED_AT = 'UPDATED_AT',
+}
+
 export type GlobalSettings = {
     availableLanguages: Array<LanguageCode>;
     createdAt: Scalars['DateTime']['output'];
@@ -4860,6 +4894,7 @@ export type Query = {
     facetValues: FacetValueList;
     facets: FacetList;
     fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
+    globalSearch: GlobalSearchResult;
     globalSettings: GlobalSettings;
     job?: Maybe<Job>;
     jobBufferSize: Array<JobBufferSize>;
@@ -4991,6 +5026,10 @@ export type QueryFacetsArgs = {
     options?: InputMaybe<FacetListOptions>;
 };
 
+export type QueryGlobalSearchArgs = {
+    input: GlobalSearchInput;
+};
+
 export type QueryJobArgs = {
     jobId: Scalars['ID']['input'];
 };

+ 39 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -1933,6 +1933,40 @@ export enum GlobalFlag {
     TRUE = 'TRUE',
 }
 
+export type GlobalSearchInput = {
+    enabledOnly?: InputMaybe<Scalars['Boolean']['input']>;
+    entityTypes?: InputMaybe<Array<Scalars['String']['input']>>;
+    query: Scalars['String']['input'];
+    skip?: InputMaybe<Scalars['Int']['input']>;
+    sortDirection?: InputMaybe<GlobalSearchSortDirection>;
+    sortField?: InputMaybe<GlobalSearchSortField>;
+    take?: InputMaybe<Scalars['Int']['input']>;
+};
+
+export type GlobalSearchResult = {
+    items: Array<GlobalSearchResultItem>;
+    totalItems: Scalars['Int']['output'];
+};
+
+export type GlobalSearchResultItem = {
+    description?: Maybe<Scalars['String']['output']>;
+    id: Scalars['ID']['output'];
+    metadata?: Maybe<Scalars['JSON']['output']>;
+    name: Scalars['String']['output'];
+};
+
+export enum GlobalSearchSortDirection {
+    ASC = 'ASC',
+    DESC = 'DESC',
+}
+
+export enum GlobalSearchSortField {
+    CREATED_AT = 'CREATED_AT',
+    ID = 'ID',
+    NAME = 'NAME',
+    UPDATED_AT = 'UPDATED_AT',
+}
+
 export type GlobalSettings = {
     availableLanguages: Array<LanguageCode>;
     createdAt: Scalars['DateTime']['output'];
@@ -4930,6 +4964,7 @@ export type Query = {
     facetValues: FacetValueList;
     facets: FacetList;
     fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
+    globalSearch: GlobalSearchResult;
     globalSettings: GlobalSettings;
     job?: Maybe<Job>;
     jobBufferSize: Array<JobBufferSize>;
@@ -5062,6 +5097,10 @@ export type QueryFacetsArgs = {
     options?: InputMaybe<FacetListOptions>;
 };
 
+export type QueryGlobalSearchArgs = {
+    input: GlobalSearchInput;
+};
+
 export type QueryJobArgs = {
     jobId: Scalars['ID']['input'];
 };

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
schema-admin.json


+ 8 - 1
scripts/codegen/download-introspection-schema.ts

@@ -2,6 +2,7 @@
 import { INestApplication } from '@nestjs/common';
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import { bootstrap, VendureConfig } from '@vendure/core';
+import { DashboardPlugin, DbIndexingStrategy, DbSearchStrategy } from '@vendure/dashboard-plugin';
 import fs from 'fs';
 import { getIntrospectionQuery } from 'graphql';
 import http from 'http';
@@ -27,7 +28,13 @@ export const config: VendureConfig = {
     paymentOptions: {
         paymentMethodHandlers: [],
     },
-    plugins: [AdminUiPlugin],
+    plugins: [
+        AdminUiPlugin,
+        DashboardPlugin.init({
+            searchStrategy: new DbSearchStrategy(),
+            indexingStrategy: new DbIndexingStrategy(),
+        }),
+    ],
 };
 
 let appPromise: Promise<INestApplication>;

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.