Search (#7237)
Steps to test 1. Run metadata migrations 2. Run sync-metadata on your workspace 3. Enable the following feature flags: IS_SEARCH_ENABLED IS_QUERY_RUNNER_TWENTY_ORM_ENABLED IS_WORKSPACE_MIGRATED_FOR_SEARCH 4. Type Cmd + K and search anything
This commit is contained in:
@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
WorkspaceMigrationEntity,
|
||||
WorkspaceMigrationIndexActionType,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationIndexFactory {
|
||||
@ -94,6 +94,7 @@ export class WorkspaceMigrationIndexFactory {
|
||||
|
||||
return fieldMetadata.name;
|
||||
}),
|
||||
type: indexMetadata.indexType,
|
||||
}));
|
||||
|
||||
workspaceMigrations.push({
|
||||
|
||||
@ -26,6 +26,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service';
|
||||
import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service';
|
||||
import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
|
||||
@ -194,13 +195,21 @@ export class WorkspaceMigrationRunnerService {
|
||||
for (const index of indexes) {
|
||||
switch (index.action) {
|
||||
case WorkspaceMigrationIndexActionType.CREATE:
|
||||
await queryRunner.createIndex(
|
||||
`${schemaName}.${tableName}`,
|
||||
new TableIndex({
|
||||
name: index.name,
|
||||
columnNames: index.columns,
|
||||
}),
|
||||
);
|
||||
if (isDefined(index.type)) {
|
||||
const quotedColumns = index.columns.map((column) => `"${column}"`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX "${index.name}" ON "${schemaName}"."${tableName}" USING ${index.type} (${quotedColumns.join(', ')})
|
||||
`);
|
||||
} else {
|
||||
await queryRunner.createIndex(
|
||||
`${schemaName}.${tableName}`,
|
||||
new TableIndex({
|
||||
name: index.name,
|
||||
columnNames: index.columns,
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case WorkspaceMigrationIndexActionType.DROP:
|
||||
try {
|
||||
@ -380,6 +389,8 @@ export class WorkspaceMigrationRunnerService {
|
||||
enumName: enumName,
|
||||
isArray: migrationColumn.isArray,
|
||||
isNullable: migrationColumn.isNullable,
|
||||
asExpression: migrationColumn.asExpression,
|
||||
generatedType: migrationColumn.generatedType,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,6 +24,8 @@ const commonFieldPropertiesToIgnore = [
|
||||
'settings',
|
||||
'joinColumn',
|
||||
'gate',
|
||||
'asExpression',
|
||||
'generatedType',
|
||||
];
|
||||
|
||||
const fieldPropertiesToStringify = ['defaultValue'] as const;
|
||||
|
||||
@ -135,6 +135,7 @@ export const COMPANY_STANDARD_FIELD_IDS = {
|
||||
favorites: '20202020-4d1d-41ac-b13b-621631298d55',
|
||||
attachments: '20202020-c1b5-4120-b0f0-987ca401ed53',
|
||||
timelineActivities: '20202020-0414-4daf-9c0d-64fe7b27f89f',
|
||||
searchVector: '85c71601-72f9-4b7b-b343-d46100b2c74d',
|
||||
};
|
||||
|
||||
export const CONNECTED_ACCOUNT_STANDARD_FIELD_IDS = {
|
||||
@ -300,6 +301,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = {
|
||||
noteTargets: '20202020-dd3f-42d5-a382-db58aabf43d3',
|
||||
attachments: '20202020-87c7-4118-83d6-2f4031005209',
|
||||
timelineActivities: '20202020-30e2-421f-96c7-19c69d1cf631',
|
||||
searchVector: '428a0da5-4b2e-4ce3-b695-89a8b384e6e3',
|
||||
};
|
||||
|
||||
export const PERSON_STANDARD_FIELD_IDS = {
|
||||
@ -325,6 +327,7 @@ export const PERSON_STANDARD_FIELD_IDS = {
|
||||
messageParticipants: '20202020-498e-4c61-8158-fa04f0638334',
|
||||
calendarEventParticipants: '20202020-52ee-45e9-a702-b64b3753e3a9',
|
||||
timelineActivities: '20202020-a43e-4873-9c23-e522de906ce5',
|
||||
searchVector: '57d1d7ad-fa10-44fc-82f3-ad0959ec2534',
|
||||
};
|
||||
|
||||
export const TASK_STANDARD_FIELD_IDS = {
|
||||
@ -463,4 +466,5 @@ export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = {
|
||||
favorites: '20202020-a4a7-4686-b296-1c6c3482ee21',
|
||||
attachments: '20202020-8d59-46ca-b7b2-73d167712134',
|
||||
timelineActivities: '20202020-f1ef-4ba4-8f33-1a4577afa477',
|
||||
searchVector: '70e56537-18ef-4811-b1c7-0a444006b815',
|
||||
};
|
||||
|
||||
@ -166,6 +166,8 @@ export class StandardFieldFactory {
|
||||
isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false,
|
||||
isSystem: workspaceFieldMetadataArgs.isSystem ?? false,
|
||||
isActive: workspaceFieldMetadataArgs.isActive ?? true,
|
||||
asExpression: workspaceFieldMetadataArgs.asExpression,
|
||||
generatedType: workspaceFieldMetadataArgs.generatedType,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -5,9 +5,12 @@ import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-syn
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
|
||||
import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util';
|
||||
|
||||
@Injectable()
|
||||
@ -15,23 +18,37 @@ export class StandardIndexFactory {
|
||||
create(
|
||||
standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[],
|
||||
context: WorkspaceSyncContext,
|
||||
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||
): Partial<IndexMetadataEntity>[] {
|
||||
return standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) =>
|
||||
this.createIndexMetadata(
|
||||
standardObjectMetadata,
|
||||
const standardIndexOnStandardObjects =
|
||||
standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) =>
|
||||
this.createStandardIndexMetadataForStandardObject(
|
||||
standardObjectMetadata,
|
||||
context,
|
||||
originalStandardObjectMetadataMap,
|
||||
workspaceFeatureFlagsMap,
|
||||
),
|
||||
);
|
||||
|
||||
const standardIndexesOnCustomObjects =
|
||||
this.createStandardIndexMetadataForCustomObject(
|
||||
context,
|
||||
originalObjectMetadataMap,
|
||||
originalCustomObjectMetadataMap,
|
||||
workspaceFeatureFlagsMap,
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
return [
|
||||
standardIndexOnStandardObjects,
|
||||
standardIndexesOnCustomObjects,
|
||||
].flat();
|
||||
}
|
||||
|
||||
private createIndexMetadata(
|
||||
private createStandardIndexMetadataForStandardObject(
|
||||
target: typeof BaseWorkspaceEntity,
|
||||
context: WorkspaceSyncContext,
|
||||
originalObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||
): Partial<IndexMetadataEntity>[] {
|
||||
const workspaceEntity = metadataArgsStorage.filterEntities(target);
|
||||
@ -58,7 +75,7 @@ export class StandardIndexFactory {
|
||||
return workspaceIndexMetadataArgsCollection.map(
|
||||
(workspaceIndexMetadataArgs) => {
|
||||
const objectMetadata =
|
||||
originalObjectMetadataMap[workspaceEntity.nameSingular];
|
||||
originalStandardObjectMetadataMap[workspaceEntity.nameSingular];
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new Error(
|
||||
@ -71,10 +88,55 @@ export class StandardIndexFactory {
|
||||
objectMetadataId: objectMetadata.id,
|
||||
name: workspaceIndexMetadataArgs.name,
|
||||
columns: workspaceIndexMetadataArgs.columns,
|
||||
isCustom: false,
|
||||
indexType: workspaceIndexMetadataArgs.type,
|
||||
};
|
||||
|
||||
return indexMetadata;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private createStandardIndexMetadataForCustomObject(
|
||||
context: WorkspaceSyncContext,
|
||||
originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
workspaceFeatureFlagsMap: FeatureFlagMap,
|
||||
): Partial<IndexMetadataEntity>[] {
|
||||
const target = CustomWorkspaceEntity;
|
||||
const workspaceEntity = metadataArgsStorage.filterExtendedEntities(target);
|
||||
|
||||
if (!workspaceEntity) {
|
||||
throw new Error(
|
||||
`Object metadata decorator not found, can't parse ${target.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceIndexMetadataArgsCollection = metadataArgsStorage
|
||||
.filterIndexes(target)
|
||||
.filter((workspaceIndexMetadataArgs) => {
|
||||
return !isGatedAndNotEnabled(
|
||||
workspaceIndexMetadataArgs.gate,
|
||||
workspaceFeatureFlagsMap,
|
||||
);
|
||||
});
|
||||
|
||||
return Object.entries(originalCustomObjectMetadataMap).flatMap(
|
||||
([customObjectName, customObjectMetadata]) => {
|
||||
return workspaceIndexMetadataArgsCollection.map(
|
||||
(workspaceIndexMetadataArgs) => {
|
||||
const indexMetadata: PartialIndexMetadata = {
|
||||
workspaceId: context.workspaceId,
|
||||
objectMetadataId: customObjectMetadata.id,
|
||||
name: `IDX_${generateDeterministicIndexName([computeTableName(customObjectName, true), ...workspaceIndexMetadataArgs.columns])}`,
|
||||
columns: workspaceIndexMetadataArgs.columns,
|
||||
isCustom: false,
|
||||
indexType: workspaceIndexMetadataArgs.type,
|
||||
};
|
||||
|
||||
return indexMetadata;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ export type PartialFieldMetadata = Omit<
|
||||
workspaceId: string;
|
||||
objectMetadataId?: string;
|
||||
isActive?: boolean;
|
||||
asExpression?: string;
|
||||
generatedType?: 'STORED' | 'VIRTUAL';
|
||||
};
|
||||
|
||||
export type PartialComputedFieldMetadata = {
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
} from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
@ -143,13 +144,27 @@ export class WorkspaceSyncFieldMetadataService {
|
||||
] of standardObjectStandardFieldMetadataMap) {
|
||||
const originalObjectMetadata =
|
||||
originalObjectMetadataMap[standardObjectId];
|
||||
const computedStandardFieldMetadataCollection = computeStandardFields(
|
||||
|
||||
let computedStandardFieldMetadataCollection = computeStandardFields(
|
||||
standardFieldMetadataCollection,
|
||||
originalObjectMetadata,
|
||||
// We need to provide this for generated relations with custom objects
|
||||
customObjectMetadataCollection,
|
||||
);
|
||||
|
||||
let originalObjectMetadataFields = originalObjectMetadata.fields;
|
||||
|
||||
if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) {
|
||||
computedStandardFieldMetadataCollection =
|
||||
computedStandardFieldMetadataCollection.filter(
|
||||
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||
);
|
||||
|
||||
originalObjectMetadataFields = originalObjectMetadataFields.filter(
|
||||
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldComparatorResults = this.workspaceFieldComparator.compare(
|
||||
originalObjectMetadata.id,
|
||||
originalObjectMetadata.fields,
|
||||
@ -177,11 +192,24 @@ export class WorkspaceSyncFieldMetadataService {
|
||||
// Loop over all custom objects from the DB and compare their fields with standard fields
|
||||
for (const customObjectMetadata of customObjectMetadataCollection) {
|
||||
// Also, maybe it's better to refactor a bit and move generation part into a separate module ?
|
||||
const standardFieldMetadataCollection = computeStandardFields(
|
||||
let standardFieldMetadataCollection = computeStandardFields(
|
||||
customObjectStandardFieldMetadataCollection,
|
||||
customObjectMetadata,
|
||||
);
|
||||
|
||||
let customObjectMetadataFields = customObjectMetadata.fields;
|
||||
|
||||
if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) {
|
||||
standardFieldMetadataCollection =
|
||||
standardFieldMetadataCollection.filter(
|
||||
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||
);
|
||||
|
||||
customObjectMetadataFields = customObjectMetadataFields.filter(
|
||||
(field) => field.type !== FieldMetadataType.TS_VECTOR,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* COMPARE FIELD METADATA
|
||||
*/
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { Any, EntityManager } from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface';
|
||||
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
|
||||
import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
|
||||
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
||||
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
|
||||
import {
|
||||
IndexMetadataEntity,
|
||||
IndexType,
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
|
||||
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
||||
import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory';
|
||||
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
|
||||
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
|
||||
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
|
||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||
import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage';
|
||||
import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSyncIndexMetadataService {
|
||||
@ -47,35 +50,60 @@ export class WorkspaceSyncIndexMetadataService {
|
||||
workspaceId: context.workspaceId,
|
||||
// We're only interested in standard fields
|
||||
fields: { isCustom: false },
|
||||
isCustom: false,
|
||||
},
|
||||
relations: ['dataSource', 'fields', 'indexes'],
|
||||
});
|
||||
|
||||
// Create map of object metadata & field metadata by unique identifier
|
||||
const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
|
||||
originalObjectMetadataCollection,
|
||||
// Relation are based on the singular name
|
||||
const originalStandardObjectMetadataMap =
|
||||
mapObjectMetadataByUniqueIdentifier(
|
||||
originalObjectMetadataCollection.filter(
|
||||
(objectMetadata) => !objectMetadata.isCustom,
|
||||
),
|
||||
// Relation are based on the singular name
|
||||
(objectMetadata) => objectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
const originalCustomObjectMetadataMap = mapObjectMetadataByUniqueIdentifier(
|
||||
originalObjectMetadataCollection.filter(
|
||||
(objectMetadata) => objectMetadata.isCustom,
|
||||
),
|
||||
(objectMetadata) => objectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
const indexMetadataRepository = manager.getRepository(IndexMetadataEntity);
|
||||
|
||||
const originalIndexMetadataCollection = await indexMetadataRepository.find({
|
||||
let originalIndexMetadataCollection = await indexMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId: context.workspaceId,
|
||||
objectMetadataId: Any(
|
||||
Object.values(originalObjectMetadataCollection).map(
|
||||
(object) => object.id,
|
||||
),
|
||||
),
|
||||
isCustom: false,
|
||||
},
|
||||
relations: ['indexFieldMetadatas.fieldMetadata'],
|
||||
});
|
||||
|
||||
// Generate index metadata from models
|
||||
const standardIndexMetadataCollection = this.standardIndexFactory.create(
|
||||
let standardIndexMetadataCollection = this.standardIndexFactory.create(
|
||||
standardObjectMetadataDefinitions,
|
||||
context,
|
||||
originalObjectMetadataMap,
|
||||
originalStandardObjectMetadataMap,
|
||||
originalCustomObjectMetadataMap,
|
||||
workspaceFeatureFlagsMap,
|
||||
);
|
||||
|
||||
if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) {
|
||||
originalIndexMetadataCollection = originalIndexMetadataCollection.filter(
|
||||
(index) => index.indexType !== IndexType.GIN,
|
||||
);
|
||||
|
||||
standardIndexMetadataCollection = standardIndexMetadataCollection.filter(
|
||||
(index) => index.indexType !== IndexType.GIN,
|
||||
);
|
||||
}
|
||||
const indexComparatorResults = this.workspaceIndexComparator.compare(
|
||||
originalIndexMetadataCollection,
|
||||
standardIndexMetadataCollection,
|
||||
|
||||
@ -54,8 +54,6 @@ export const standardObjectMetadataDefinitions = [
|
||||
CompanyWorkspaceEntity,
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
FavoriteWorkspaceEntity,
|
||||
OpportunityWorkspaceEntity,
|
||||
PersonWorkspaceEntity,
|
||||
TimelineActivityWorkspaceEntity,
|
||||
ViewFieldWorkspaceEntity,
|
||||
ViewFilterWorkspaceEntity,
|
||||
@ -79,10 +77,4 @@ export const standardObjectMetadataDefinitions = [
|
||||
PersonWorkspaceEntity,
|
||||
TaskWorkspaceEntity,
|
||||
TaskTargetWorkspaceEntity,
|
||||
TimelineActivityWorkspaceEntity,
|
||||
ViewFieldWorkspaceEntity,
|
||||
ViewFilterWorkspaceEntity,
|
||||
ViewSortWorkspaceEntity,
|
||||
ViewWorkspaceEntity,
|
||||
WebhookWorkspaceEntity,
|
||||
];
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util';
|
||||
|
||||
const nameTextField = { name: 'name', type: FieldMetadataType.TEXT };
|
||||
const nameFullNameField = {
|
||||
name: 'name',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
};
|
||||
const jobTitleTextField = { name: 'jobTitle', type: FieldMetadataType.TEXT };
|
||||
const emailsEmailsField = { name: 'emails', type: FieldMetadataType.EMAILS };
|
||||
|
||||
jest.mock(
|
||||
'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util',
|
||||
() => ({
|
||||
computeColumnName: jest.fn((name) => {
|
||||
if (name === 'name') {
|
||||
return 'name';
|
||||
}
|
||||
if (name === 'jobTitle') {
|
||||
return 'jobTitle';
|
||||
}
|
||||
if (name === 'emailsPrimaryEmail') {
|
||||
return 'emailsPrimaryEmail';
|
||||
}
|
||||
if (name === 'emailsAdditionalEmails') {
|
||||
return 'emailsAdditionalEmails';
|
||||
}
|
||||
if (name === 'nameFirstName') {
|
||||
return 'nameFirstName';
|
||||
}
|
||||
if (name === 'nameLastName') {
|
||||
return 'nameLastName';
|
||||
}
|
||||
}),
|
||||
computeCompositeColumnName: jest.fn((field, property) => {
|
||||
if (
|
||||
field.name === emailsEmailsField.name &&
|
||||
property.name === 'primaryEmail'
|
||||
) {
|
||||
return 'emailsPrimaryEmail';
|
||||
}
|
||||
if (
|
||||
field.name === emailsEmailsField.name &&
|
||||
property.name === 'additionalEmails'
|
||||
) {
|
||||
return 'emailsAdditionalEmails';
|
||||
}
|
||||
if (
|
||||
field.name === nameFullNameField.name &&
|
||||
property.name === 'firstName'
|
||||
) {
|
||||
return 'nameFirstName';
|
||||
}
|
||||
if (
|
||||
field.name === nameFullNameField.name &&
|
||||
property.name === 'lastName'
|
||||
) {
|
||||
return 'nameLastName';
|
||||
}
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('getTsVectorColumnExpressionFromFields', () => {
|
||||
it('should generate correct expression for simple text field', () => {
|
||||
const fields = [nameTextField];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
expect(result).toContain("to_tsvector('simple', COALESCE(\"name\", ''))");
|
||||
});
|
||||
|
||||
it('should handle multiple fields', () => {
|
||||
const fields = [nameFullNameField, jobTitleTextField, emailsEmailsField];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
const expected = `
|
||||
CASE
|
||||
WHEN "deletedAt" IS NULL THEN
|
||||
to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' ||
|
||||
COALESCE(
|
||||
replace(
|
||||
"emailsPrimaryEmail",
|
||||
'@',
|
||||
' '
|
||||
),
|
||||
''
|
||||
)
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
`.trim();
|
||||
|
||||
expect(result.trim()).toBe(expected);
|
||||
});
|
||||
|
||||
it('should include CASE statement for handling deletedAt', () => {
|
||||
const fields = [nameTextField];
|
||||
const result = getTsVectorColumnExpressionFromFields(fields);
|
||||
|
||||
expect(result).toContain('CASE');
|
||||
expect(result).toContain('WHEN "deletedAt" IS NULL THEN');
|
||||
expect(result).toContain('ELSE NULL');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,88 @@
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
WorkspaceMigrationException,
|
||||
WorkspaceMigrationExceptionCode,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
|
||||
|
||||
type FieldTypeAndNameMetadata = {
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
};
|
||||
|
||||
export const getTsVectorColumnExpressionFromFields = (
|
||||
fieldsUsedForSearch: FieldTypeAndNameMetadata[],
|
||||
): string => {
|
||||
const columnExpressions = fieldsUsedForSearch.flatMap(
|
||||
getColumnExpressionsFromField,
|
||||
);
|
||||
const concatenatedExpression = columnExpressions.join(" || ' ' || ");
|
||||
|
||||
const tsVectorExpression = `to_tsvector('simple', ${concatenatedExpression})`;
|
||||
|
||||
return `
|
||||
CASE
|
||||
WHEN "deletedAt" IS NULL THEN
|
||||
${tsVectorExpression}
|
||||
ELSE NULL
|
||||
END
|
||||
`;
|
||||
};
|
||||
|
||||
const getColumnExpressionsFromField = (
|
||||
fieldMetadataTypeAndName: FieldTypeAndNameMetadata,
|
||||
): string[] => {
|
||||
if (isCompositeFieldMetadataType(fieldMetadataTypeAndName.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
fieldMetadataTypeAndName.type,
|
||||
);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new WorkspaceMigrationException(
|
||||
`Composite type not found for field metadata type: ${fieldMetadataTypeAndName.type}`,
|
||||
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
|
||||
);
|
||||
}
|
||||
|
||||
return compositeType.properties
|
||||
.filter((property) => property.type === FieldMetadataType.TEXT)
|
||||
.map((property) => {
|
||||
const columnName = computeCompositeColumnName(
|
||||
fieldMetadataTypeAndName,
|
||||
property,
|
||||
);
|
||||
|
||||
return getColumnExpression(columnName, fieldMetadataTypeAndName.type);
|
||||
});
|
||||
}
|
||||
const columnName = computeColumnName(fieldMetadataTypeAndName.name);
|
||||
|
||||
return [getColumnExpression(columnName, fieldMetadataTypeAndName.type)];
|
||||
};
|
||||
|
||||
const getColumnExpression = (
|
||||
columnName: string,
|
||||
fieldType: FieldMetadataType,
|
||||
): string => {
|
||||
const quotedColumnName = `"${columnName}"`;
|
||||
|
||||
if (fieldType === FieldMetadataType.EMAILS) {
|
||||
return `
|
||||
COALESCE(
|
||||
replace(
|
||||
${quotedColumnName},
|
||||
'@',
|
||||
' '
|
||||
),
|
||||
''
|
||||
)
|
||||
`;
|
||||
} else {
|
||||
return `COALESCE(${quotedColumnName}, '')`;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user