Browse Source

feat(dashboard): Wip search indexing

David Höck 5 months ago
parent
commit
8fcd29098d

+ 1 - 0
packages/dashboard/plugin/constants.ts

@@ -1,5 +1,6 @@
 import { join } from 'path';
 
+export const DASHBOARD_PLUGIN_OPTIONS = Symbol('DASHBOARD_PLUGIN_OPTIONS');
 export const DEFAULT_APP_PATH = join(__dirname, 'dist');
 export const loggerCtx = 'DashboardPlugin';
 export const defaultLanguage = 'en';

+ 18 - 25
packages/dashboard/plugin/dashboard.plugin.ts

@@ -15,30 +15,11 @@ import path from 'path';
 
 import { adminApiExtensions } from './api/api-extensions.js';
 import { MetricsResolver } from './api/metrics.resolver.js';
-import { DEFAULT_APP_PATH, loggerCtx } from './constants.js';
+import { DASHBOARD_PLUGIN_OPTIONS, DEFAULT_APP_PATH, loggerCtx } from './constants.js';
+import { ProductDataMapper } from './entity-data-mapper/product.data-mapper';
+import { DbIndexingStrategy } from './search-index/db-indexing.strategy';
 import { MetricsService } from './service/metrics.service.js';
-
-/**
- * @description
- * Configuration options for the {@link DashboardPlugin}.
- *
- * @docsCategory core plugins/DashboardPlugin
- */
-export interface DashboardPluginOptions {
-    /**
-     * @description
-     * The route to the Dashboard UI.
-     *
-     * @default 'dashboard'
-     */
-    route: string;
-    /**
-     * @description
-     * The path to the dashboard UI app dist directory. By default, the built-in dashboard UI
-     * will be served. This can be overridden with a custom build of the dashboard.
-     */
-    appDir: string;
-}
+import { DashboardPluginOptions } from './types';
 
 /**
  * @description
@@ -104,7 +85,10 @@ export interface DashboardPluginOptions {
         schema: adminApiExtensions,
         resolvers: [MetricsResolver],
     },
-    providers: [MetricsService],
+    providers: [
+        { provide: DASHBOARD_PLUGIN_OPTIONS, useFactory: () => DashboardPlugin.options },
+        MetricsService,
+    ],
     configuration: config => {
         config.settingsStoreFields['vendure.dashboard'] = [
             {
@@ -126,7 +110,16 @@ export class DashboardPlugin implements NestModule {
      * Set the plugin options
      */
     static init(options: DashboardPluginOptions): Type<DashboardPlugin> {
-        this.options = options;
+        this.options = {
+            ...options,
+            globalSearch: {
+                indexingStrategy: options.globalSearch?.indexingStrategy ?? new DbIndexingStrategy(),
+                entityDataMappers: {
+                    Product: new ProductDataMapper(),
+                    ...options.globalSearch?.entityDataMappers,
+                },
+            },
+        };
         return DashboardPlugin;
     }
 

+ 36 - 0
packages/dashboard/plugin/data-processor/base-data-processor.ts

@@ -0,0 +1,36 @@
+import { Injector } from '@vendure/core';
+import { Serializable } from 'node:child_process';
+
+import { DASHBOARD_PLUGIN_OPTIONS } from '../constants';
+import { SearchIndexingStrategy } from '../search-index/search-indexing.strategy';
+
+import { DataProcessorInterface } from './data-processor.interface';
+
+export abstract class BaseDataProcessor implements DataProcessorInterface {
+    protected searchIndexingStrategy: SearchIndexingStrategy;
+
+    init(injector: Injector) {
+        const options = injector.get(DASHBOARD_PLUGIN_OPTIONS);
+        this.searchIndexingStrategy = options.globalSearch?.indexingStrategy as SearchIndexingStrategy;
+    }
+
+    getBatchSize(): number {
+        throw new Error('Not implemented');
+    }
+
+    getTotalResults(): Promise<number> {
+        throw new Error('Not implemented');
+    }
+
+    processOne(id: string): Promise<void> {
+        throw new Error('Not implemented');
+    }
+
+    processBatch(
+        skip: number,
+        limit: number,
+        metadata: Record<string, Serializable> | undefined,
+    ): AsyncGenerator<void> {
+        throw new Error('Not implemented');
+    }
+}

+ 37 - 0
packages/dashboard/plugin/data-processor/data-processor.interface.ts

@@ -0,0 +1,37 @@
+import { Injector } from '@vendure/core';
+import { Serializable } from 'node:child_process';
+
+export interface DataProcessorInterface {
+    init(injector: Injector): void;
+
+    /**
+     * @description
+     * Returns the total number of results that can be expected from this search index.
+     * It will be used to determine the number of batches that will be processed
+     */
+    getTotalResults(): Promise<number>;
+
+    /**
+     * @description
+     * Returns the number of results that should be processed within a single batch
+     */
+    getBatchSize(): number;
+
+    /**
+     * @description Processes a batch of results
+     * @param skip
+     * @param limit
+     * @param metadata
+     */
+    processBatch(
+        skip: number,
+        limit: number,
+        metadata: Record<string, Serializable> | undefined,
+    ): AsyncGenerator<void>;
+
+    /**
+     * @description Processes a single result by its ID
+     * @param id
+     */
+    processOne(id: string): Promise<void>;
+}

+ 10 - 0
packages/dashboard/plugin/data-processor/entities.data-processor.ts

@@ -0,0 +1,10 @@
+import { Injector } from '@vendure/core';
+
+import { BaseDataProcessor } from './base-data-processor';
+
+export class EntitiesDataProcessor extends BaseDataProcessor {
+    init(injector: Injector) {
+        super.init(injector);
+        // add necessary DI
+    }
+}

+ 7 - 0
packages/dashboard/plugin/entity-data-mapper/entity-data-mapper.interface.ts

@@ -0,0 +1,7 @@
+import { VendureEntity } from '@vendure/core';
+
+import { SearchIndexItem } from '../types';
+
+export interface EntityDataMapper {
+    map(entity: VendureEntity): Promise<Partial<SearchIndexItem>> | Partial<SearchIndexItem>;
+}

+ 22 - 0
packages/dashboard/plugin/entity-data-mapper/product.data-mapper.ts

@@ -0,0 +1,22 @@
+import { Product } from '@vendure/core';
+
+import { EntitySearchIndexItem } from '../types';
+
+import { VendureEntityDataMapper } from './vendure-entity.data-mapper';
+
+export class ProductDataMapper extends VendureEntityDataMapper {
+    map(entity: Product): Partial<EntitySearchIndexItem> {
+        const product = super.map(entity);
+
+        return {
+            ...product,
+            entityName: Product.name,
+            thumbnailUrl: entity.featuredAsset ? entity.featuredAsset.preview : undefined,
+            title: entity.name,
+            description: entity.description,
+            metadata: {
+                enabled: entity.enabled,
+            },
+        };
+    }
+}

+ 14 - 0
packages/dashboard/plugin/entity-data-mapper/vendure-entity.data-mapper.ts

@@ -0,0 +1,14 @@
+import { VendureEntity } from '@vendure/core';
+
+import { EntitySearchIndexItem } from '../types';
+
+import { EntityDataMapper } from './entity-data-mapper.interface';
+
+export abstract class VendureEntityDataMapper implements EntityDataMapper {
+    map(entity: VendureEntity): Partial<EntitySearchIndexItem> {
+        return {
+            entityId: entity.id,
+            lastModified: entity.updatedAt ?? entity.createdAt,
+        };
+    }
+}

+ 13 - 0
packages/dashboard/plugin/search-index/db-indexing.strategy.ts

@@ -0,0 +1,13 @@
+import { SearchIndexItem } from '../types';
+
+import { SearchIndexingStrategy } from './search-indexing.strategy';
+
+export class DbIndexingStrategy implements SearchIndexingStrategy {
+    persist(items: SearchIndexItem[]): Promise<void> {
+        return Promise.resolve(undefined);
+    }
+
+    remove(id: string): Promise<void> {
+        return Promise.resolve(undefined);
+    }
+}

+ 15 - 0
packages/dashboard/plugin/search-index/search-indexing.strategy.ts

@@ -0,0 +1,15 @@
+import { SearchIndexItem } from '../types';
+
+export interface SearchIndexingStrategy {
+    /**
+     * Persists the given items to the search index
+     * @param items
+     */
+    persist(items: SearchIndexItem[]): Promise<void>;
+
+    /**
+     * Removes the given item ID from the search index
+     * @param id
+     */
+    remove(id: string): Promise<void>;
+}

+ 64 - 0
packages/dashboard/plugin/types.ts

@@ -1,3 +1,40 @@
+import { ID } from '@vendure/common/lib/shared-types';
+import { CustomFields } from '@vendure/core';
+
+import { EntityDataMapper } from './entity-data-mapper/entity-data-mapper.interface';
+import { SearchIndexingStrategy } from './search-index/search-indexing.strategy';
+
+/**
+ * @description
+ * Configuration options for the {@link DashboardPlugin}.
+ *
+ * @docsCategory core plugins/DashboardPlugin
+ */
+export interface DashboardPluginOptions {
+    /**
+     * @description
+     * The route to the Dashboard UI.
+     *
+     * @default 'dashboard'
+     */
+    route: string;
+    /**
+     * @description
+     * The path to the dashboard UI app dist directory. By default, the built-in dashboard UI
+     * will be served. This can be overridden with a custom build of the dashboard.
+     */
+    appDir: string;
+
+    /**
+     * @description
+     * Configuration of the global search feature in the dashboard UI
+     */
+    globalSearch?: {
+        indexingStrategy?: SearchIndexingStrategy;
+        entityDataMappers?: Record<keyof CustomFields | string, EntityDataMapper>;
+    };
+}
+
 export type MetricSummary = {
     interval: MetricInterval;
     type: MetricType;
@@ -25,3 +62,30 @@ export interface MetricSummaryInput {
     types: MetricType[];
     refresh?: boolean;
 }
+
+export interface SearchIndexItem {
+    id?: string;
+    title: string;
+    type: 'entity' | 'plugin' | 'docs' | 'article';
+    subtitle?: string;
+    description?: string;
+    thumbnailUrl?: string;
+    metadata?: Record<string, any>;
+    lastModified?: Date | string;
+}
+
+/**
+ * @description The index items for custom and built-in Vendure entities
+ */
+export interface EntitySearchIndexItem extends SearchIndexItem {
+    entityId: ID;
+    entityName: string;
+}
+
+/**
+ * @description The index items for external urls like blog articles, docs or plugins.
+ */
+export interface ExternalUrlSearchIndexItem extends SearchIndexItem {
+    externalId: string;
+    url: string;
+}