|
|
@@ -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);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|