diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index a8ff395b02..71c3548197 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -171,12 +171,14 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatViewsToDelete, flatViewFieldsToDelete, flatViewsToUpdate, + flatFieldMetadatasToUpdate: flatFieldMetadatasToUpdateFromSideEffect, } = sideEffectResult.result; return { flatFieldMetadatasToUpdate: [ ...accumulator.flatFieldMetadatasToUpdate, toFlatFieldMetadata, + ...flatFieldMetadatasToUpdateFromSideEffect, ], flatIndexMetadatasToUpdate: [ ...accumulator.flatIndexMetadatasToUpdate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts index 3dd0b7eab1..0170339b30 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-flat-field-metadata-update-side-effect.util.ts @@ -1,6 +1,8 @@ import { type FromTo } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { type FieldInputTranspilationResult } from 'src/engine/metadata-modules/flat-field-metadata/types/field-input-transpilation-result.type'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { handleEnumFlatFieldMetadataUpdateSideEffects } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-enum-flat-field-metadata-update-side-effects.util'; @@ -12,6 +14,7 @@ import { type FieldMetadataUpdateIndexSideEffect, handleIndexChangesDuringFieldUpdate, } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-index-changes-during-field-update.util'; +import { handleLabelIdentifierChangesDuringFieldUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util'; import { isEnumFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-enum-flat-field-metadata.util'; import { type FlatViewFiltersToDeleteAndUpdate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-filters-on-flat-field-metadata-options-update.util'; import { type FlatViewGroupsToDeleteUpdateAndCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-view-groups-on-flat-field-metadata-options-update.util'; @@ -20,7 +23,9 @@ export type FlatFieldMetadataUpdateSideEffects = FlatViewFiltersToDeleteAndUpdate & FlatViewGroupsToDeleteUpdateAndCreate & FieldMetadataUpdateIndexSideEffect & - FieldMetadataDeactivationSideEffect; + FieldMetadataDeactivationSideEffect & { + flatFieldMetadatasToUpdate: FlatFieldMetadata[]; + }; type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo< FlatFieldMetadata, @@ -52,6 +57,7 @@ export const FLAT_FIELD_METADATA_UPDATE_EMPTY_SIDE_EFFECTS: FlatFieldMetadataUpd flatViewsToDelete: [], flatViewFieldsToDelete: [], flatViewsToUpdate: [], + flatFieldMetadatasToUpdate: [], }; export const handleFlatFieldMetadataUpdateSideEffect = ({ @@ -131,6 +137,31 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({ return indexChangesSideEffectResult; } + const flatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatObjectMetadataMaps, + flatEntityId: fromFlatFieldMetadata.objectMetadataId, + }); + + const isLabelIdentifierFieldMetadata = + flatObjectMetadata.labelIdentifierFieldMetadataId === + toFlatFieldMetadata.id; + + if (isLabelIdentifierFieldMetadata) { + const flatSearchVectorFieldToUpdate = + handleLabelIdentifierChangesDuringFieldUpdate({ + fromFlatFieldMetadata, + toFlatFieldMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, + }); + + if (isDefined(flatSearchVectorFieldToUpdate)) { + sideEffectResult.flatFieldMetadatasToUpdate.push( + flatSearchVectorFieldToUpdate, + ); + } + } + const { flatIndexMetadatasToUpdate, flatIndexMetadatasToCreate, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts new file mode 100644 index 0000000000..e3af15d3fc --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/handle-label-identifier-changes-during-field-update.util.ts @@ -0,0 +1,72 @@ +import { type FieldMetadataType, type FromTo } from 'twenty-shared/types'; +import { findOrThrow } from 'twenty-shared/utils'; + +import { + FieldMetadataException, + FieldMetadataExceptionCode, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; +import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { type SearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; + +type HandleLabelIdentifierChangesDuringFieldUpdateArgs = { + flatObjectMetadata: FlatObjectMetadata; +} & FromTo & + Pick; + +export const handleLabelIdentifierChangesDuringFieldUpdate = ({ + fromFlatFieldMetadata, + toFlatFieldMetadata, + flatObjectMetadata, + flatFieldMetadataMaps, +}: HandleLabelIdentifierChangesDuringFieldUpdateArgs): + | FlatFieldMetadata + | undefined => { + const hasNameChanged = + fromFlatFieldMetadata.name !== toFlatFieldMetadata.name; + + if (!hasNameChanged) { + return undefined; + } + + const objectFlatFieldMetadatas = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityIds: flatObjectMetadata.fieldMetadataIds, + }); + + const searchVectorField = findOrThrow( + objectFlatFieldMetadatas, + (field) => field.name === SEARCH_VECTOR_FIELD.name, + new FieldMetadataException( + `Search vector field not found for object metadata ${flatObjectMetadata.id}`, + FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND, + ), + ) as FlatFieldMetadata; + + try { + const newAsExpression = getTsVectorColumnExpressionFromFields([ + { + name: toFlatFieldMetadata.name, + type: toFlatFieldMetadata.type as SearchableFieldType, + }, + ]); + + return { + ...searchVectorField, + settings: { + ...searchVectorField.settings, + asExpression: newAsExpression, + }, + }; + } catch { + throw new FieldMetadataException( + `Failed to compute search vector column expression for field ${toFlatFieldMetadata.name}`, + FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + ); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts index 54755160b8..cd0a7dcea3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/from-update-object-input-to-flat-object-metadata-and-related-flat-entities.util.ts @@ -84,6 +84,7 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities = flatViewFieldsToCreate, flatViewFieldsToUpdate, otherObjectFlatFieldMetadatasToUpdate, + sameObjectFlatFieldMetadatasToUpdate, } = handleFlatObjectMetadataUpdateSideEffect({ fromFlatObjectMetadata: existingFlatObjectMetadata, toFlatObjectMetadata, @@ -100,5 +101,6 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities = flatViewFieldsToCreate, flatViewFieldsToUpdate, otherObjectFlatFieldMetadatasToUpdate, + sameObjectFlatFieldMetadatasToUpdate, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts index 590ebb2f2d..939531f870 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/handle-flat-object-metadata-update-side-effect.util.ts @@ -6,12 +6,14 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { recomputeIndexAfterFlatObjectMetadataSingularNameUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-index-after-flat-object-metadata-singular-name-update.util'; +import { recomputeSearchVectorFieldAfterLabelIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util'; import { recomputeViewFieldIdentifierAfterFlatObjectIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-view-field-identifier-after-flat-object-identifier-update.util'; import { renameRelatedMorphFieldOnObjectNamesUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/rename-related-morph-field-on-object-names-update.util'; import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type'; export type FlatObjectMetadataUpdateSideEffects = { otherObjectFlatFieldMetadatasToUpdate: FlatFieldMetadata[]; + sameObjectFlatFieldMetadatasToUpdate: FlatFieldMetadata[]; flatViewFieldsToUpdate: FlatViewField[]; flatViewFieldsToCreate: FlatViewField[]; flatIndexMetadatasToUpdate: FlatIndexMetadata[]; @@ -81,6 +83,27 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({ flatViewFieldsToUpdate: [], }; + const sameObjectFlatFieldMetadatasToUpdate: FlatFieldMetadata[] = []; + + if ( + toFlatObjectMetadata.isSearchable && + isDefined(toFlatObjectMetadata.labelIdentifierFieldMetadataId) && + fromFlatObjectMetadata.labelIdentifierFieldMetadataId !== + toFlatObjectMetadata.labelIdentifierFieldMetadataId + ) { + const updatedSearchVectorField = + recomputeSearchVectorFieldAfterLabelIdentifierUpdate({ + existingFlatObjectMetadata: fromFlatObjectMetadata, + flatFieldMetadataMaps, + updatedLabelIdentifierFieldMetadataId: + toFlatObjectMetadata.labelIdentifierFieldMetadataId, + }); + + if (isDefined(updatedSearchVectorField)) { + sameObjectFlatFieldMetadatasToUpdate.push(updatedSearchVectorField); + } + } + return { flatIndexMetadatasToUpdate: [ ...morphRelatedFlatIndexesToUpdate, @@ -89,5 +112,6 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({ flatViewFieldsToCreate, flatViewFieldsToUpdate, otherObjectFlatFieldMetadatasToUpdate: morphFlatFieldMetadatasToUpdate, + sameObjectFlatFieldMetadatasToUpdate, }; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts new file mode 100644 index 0000000000..9fa89f8754 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-after-label-identifier-update.util.ts @@ -0,0 +1,74 @@ +import { type FieldMetadataType } from 'twenty-shared/types'; +import { findOrThrow, isDefined } from 'twenty-shared/utils'; + +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { + ObjectMetadataException, + ObjectMetadataExceptionCode, +} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; +import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; +import { type SearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; + +type RecomputeSearchVectorFieldAfterLabelIdentifierUpdateArgs = { + existingFlatObjectMetadata: FlatObjectMetadata; + updatedLabelIdentifierFieldMetadataId: string; +} & Pick; + +export const recomputeSearchVectorFieldAfterLabelIdentifierUpdate = ({ + existingFlatObjectMetadata, + flatFieldMetadataMaps, + updatedLabelIdentifierFieldMetadataId, +}: RecomputeSearchVectorFieldAfterLabelIdentifierUpdateArgs): + | FlatFieldMetadata + | undefined => { + const objectFlatFieldMetadatas = + findManyFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityIds: existingFlatObjectMetadata.fieldMetadataIds, + }); + + const searchVectorField = findOrThrow( + objectFlatFieldMetadatas, + (field) => field.name === SEARCH_VECTOR_FIELD.name, + ) as FlatFieldMetadata; + + const newLabelIdentifierField = findFlatEntityByIdInFlatEntityMaps({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: updatedLabelIdentifierFieldMetadataId, + }); + + if (!isDefined(newLabelIdentifierField)) { + throw new ObjectMetadataException( + `New label identifier field not found for object metadata`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ); + } + + try { + const newAsExpression = getTsVectorColumnExpressionFromFields([ + { + name: newLabelIdentifierField.name, + type: newLabelIdentifierField.type as SearchableFieldType, + }, + ]); + + return { + ...searchVectorField, + settings: { + ...searchVectorField.settings, + asExpression: newAsExpression, + generatedType: 'STORED', + }, + }; + } catch { + throw new ObjectMetadataException( + `Failed to compute search vector column expression for field ${newLabelIdentifierField.name}`, + ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, + ); + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 4cf72bf313..05d764cd5c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -81,6 +81,7 @@ export class ObjectMetadataService extends TypeOrmQueryService; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util.spec.ts new file mode 100644 index 0000000000..84f1bea253 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/__tests__/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util.spec.ts @@ -0,0 +1,124 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util'; +import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; +import { createEmptyOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/constant/empty-orchestrator-actions-report.constant'; +import { aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util'; +import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/types/workspace-migration-field-action-v2'; + +describe('aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions', () => { + it('should move searchVector update actions to the end of the update list', () => { + const orchestratorActionsReport = { + ...createEmptyOrchestratorActionsReport(), + fieldMetadata: { + create: [], + update: [ + { + type: 'update', + metadataName: 'fieldMetadata', + entityId: 'search-vector-field-1', + objectMetadataId: 'object-1', + updates: [ + { + property: 'label', + from: 'Search Vector', + to: 'Updated Search Vector', + }, + ], + } satisfies UpdateFieldAction, + { + type: 'update', + metadataName: 'fieldMetadata', + entityId: 'regular-field-1', + objectMetadataId: 'object-1', + updates: [ + { + property: 'label', + from: 'First Name', + to: 'Updated First Name', + }, + ], + } satisfies UpdateFieldAction, + { + type: 'update', + metadataName: 'fieldMetadata', + entityId: 'regular-field-2', + objectMetadataId: 'object-1', + updates: [ + { + property: 'label', + from: 'Last Name', + to: 'Updated Last Name', + }, + ], + } satisfies UpdateFieldAction, + ], + delete: [], + }, + }; + + const flatFieldMetadataMaps = [ + getFlatFieldMetadataMock({ + universalIdentifier: 'search-vector-field-1', + objectMetadataId: 'object-1', + type: FieldMetadataType.TS_VECTOR, + id: 'search-vector-field-1', + name: SEARCH_VECTOR_FIELD.name, + }), + getFlatFieldMetadataMock({ + universalIdentifier: 'regular-field-1', + objectMetadataId: 'object-1', + type: FieldMetadataType.TEXT, + id: 'regular-field-1', + name: 'firstName', + }), + getFlatFieldMetadataMock({ + universalIdentifier: 'regular-field-2', + objectMetadataId: 'object-1', + type: FieldMetadataType.TEXT, + id: 'regular-field-2', + name: 'lastName', + }), + ].reduce>( + (flatEntityMaps, field) => + addFlatEntityToFlatEntityMapsOrThrow({ + flatEntity: field, + flatEntityMaps, + }), + createEmptyFlatEntityMaps() as FlatEntityMaps, + ); + + const result = + aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions( + { + orchestratorActionsReport, + flatFieldMetadataMaps, + }, + ); + + const updateFieldActions = result.fieldMetadata + .update as UpdateFieldAction[]; + + const actualEntityIds = updateFieldActions.map((action) => action.entityId); + + expect(actualEntityIds).toEqual([ + 'regular-field-1', + 'regular-field-2', + 'search-vector-field-1', + ]); + + expect(result.fieldMetadata.create).toEqual( + orchestratorActionsReport.fieldMetadata.create, + ); + expect(result.fieldMetadata.delete).toEqual( + orchestratorActionsReport.fieldMetadata.delete, + ); + expect(result.objectMetadata).toEqual( + orchestratorActionsReport.objectMetadata, + ); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util.ts index 9cfb296ad3..c78891cae5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util.ts @@ -2,14 +2,11 @@ import { isDefined } from 'twenty-shared/utils'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util'; +import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-aggregate-orchestrator-actions-report-args.type'; import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; import { type CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/types/workspace-migration-field-action-v2'; import { type CreateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/types/workspace-migration-object-action-v2'; -type AggregateOrchestratorActionsReportCreateObjectAndCreateFieldActionsArgs = { - orchestratorActionsReport: OrchestratorActionsReport; -}; - type AggregatedActions = { createdFieldActionByObjectMetadataId: Record; createdObjectActionByObjectMetadataId: Record; @@ -18,7 +15,7 @@ type AggregatedActions = { export const aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions = ({ orchestratorActionsReport, - }: AggregateOrchestratorActionsReportCreateObjectAndCreateFieldActionsArgs): OrchestratorActionsReport => { + }: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => { const initialCreatedObjectActionByObjectMetadataId = ( orchestratorActionsReport.objectMetadata.create as CreateObjectAction[] ).reduce( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util.ts index 04fc77480d..075eb69e65 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util.ts @@ -1,20 +1,17 @@ import { isDefined } from 'twenty-shared/utils'; +import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-aggregate-orchestrator-actions-report-args.type'; import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; import { type DeleteFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/types/workspace-migration-field-action-v2'; import { type DeleteObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/types/workspace-migration-object-action-v2'; -type AggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActionsArgs = { - orchestratorActionsReport: OrchestratorActionsReport; -}; - type AggregatedActions = { deleteFieldActionByFieldMetadataId: Record; }; export const aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions = ({ orchestratorActionsReport, - }: AggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActionsArgs): OrchestratorActionsReport => { + }: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => { const deleteObjectActionByObjectMetadataId = ( orchestratorActionsReport.objectMetadata.delete as DeleteObjectAction[] ).reduce>( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util.ts new file mode 100644 index 0000000000..0baeff31c1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util.ts @@ -0,0 +1,64 @@ +import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; + +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants'; +import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-aggregate-orchestrator-actions-report-args.type'; +import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { type UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/types/workspace-migration-field-action-v2'; + +export const aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions = + ({ + orchestratorActionsReport, + flatFieldMetadataMaps, + }: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => { + assertIsDefinedOrThrow(flatFieldMetadataMaps); + + const updateFieldActions = orchestratorActionsReport.fieldMetadata + .update as UpdateFieldAction[]; + + const { searchVectorUpdateFieldActions, otherUpdateFieldActions } = + updateFieldActions.reduce<{ + searchVectorUpdateFieldActions: UpdateFieldAction[]; + otherUpdateFieldActions: UpdateFieldAction[]; + }>( + (acc, updateFieldAction) => { + const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({ + flatEntityMaps: flatFieldMetadataMaps, + flatEntityId: updateFieldAction.entityId, + }); + + const isSearchVectorUpdateFieldAction = + flatFieldMetadata?.name === SEARCH_VECTOR_FIELD.name; + + if (isSearchVectorUpdateFieldAction) { + return { + ...acc, + searchVectorUpdateFieldActions: [ + ...acc.searchVectorUpdateFieldActions, + updateFieldAction, + ], + }; + } + + return { + ...acc, + otherUpdateFieldActions: [ + ...acc.otherUpdateFieldActions, + updateFieldAction, + ], + }; + }, + { + searchVectorUpdateFieldActions: [], + otherUpdateFieldActions: [], + }, + ); + + return { + ...orchestratorActionsReport, + fieldMetadata: { + ...orchestratorActionsReport.fieldMetadata, + update: [...otherUpdateFieldActions, ...searchVectorUpdateFieldActions], + }, + }; + }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util.ts index 2c67bc4de2..8959df7e6c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report.util.ts @@ -1,20 +1,21 @@ -import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type'; +import { type AggregateOrchestratorActionsReportArgs } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-aggregate-orchestrator-actions-report-args.type'; import { aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-create-object-and-create-field-actions.util'; import { aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-delete-object-and-delete-field.util'; +import { aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/utils/aggregate-orchestrator-actions-report-deprioritize-search-vector-update-field-actions.util'; -type AggregateOrchestratorActionsReportArgs = { - orchestratorActionsReport: OrchestratorActionsReport; -}; export const aggregateOrchestratorActionsReport = ({ orchestratorActionsReport, + flatFieldMetadataMaps, }: AggregateOrchestratorActionsReportArgs) => { const aggregatedOrchestratorActionsReport = [ aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions, aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions, + aggregateOrchestratorActionsReportDeprioritizeSearchVectorUpdateFieldActions, ].reduce( (currentOrchestratorActionsReport, aggregator) => aggregator({ orchestratorActionsReport: currentOrchestratorActionsReport, + flatFieldMetadataMaps, }), orchestratorActionsReport, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts index c08f399dd4..429f332103 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/field/services/update-field-action-handler.service.ts @@ -190,6 +190,37 @@ export class UpdateFieldActionHandlerService extends WorkspaceMigrationRunnerAct }, }; } + if ( + isPropertyUpdate(update, 'settings') && + isDefined(update.to?.asExpression) && + isDefined(update.from?.asExpression) && + (update.to.asExpression !== update.from.asExpression || + update.to.generatedType !== update.from.generatedType) + ) { + await this.workspaceSchemaManagerService.columnManager.dropColumns({ + queryRunner, + schemaName, + tableName, + columnNames: [optimisticFlatFieldMetadata.name], + }); + await this.workspaceSchemaManagerService.columnManager.addColumns({ + queryRunner, + schemaName, + tableName, + columnDefinitions: [ + { + name: optimisticFlatFieldMetadata.name, + type: 'tsvector', + ...update.to, + }, + ], + }); + + optimisticFlatFieldMetadata = { + ...optimisticFlatFieldMetadata, + settings: update.to, + }; + } } } diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-search-vector-side-effect.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-search-vector-side-effect.integration-spec.ts new file mode 100644 index 0000000000..cc618c78c3 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-search-vector-side-effect.integration-spec.ts @@ -0,0 +1,189 @@ +import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util'; +import { search } from 'test/integration/graphql/utils/search.util'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; + +describe('Field metadata update - search vector side effect', () => { + let testObjectMetadataId: string; + let testFieldMetadataId: string; + let createdRecordId: string; + + const OBJECT_NAME_SINGULAR = 'searchVectorTestObject'; + const OBJECT_NAME_PLURAL = 'searchVectorTestObjects'; + const INITIAL_FIELD_NAME = 'searchableTitle'; + const UPDATED_FIELD_NAME = 'renamedTitle'; + const RECORD_FIELD_VALUE = 'UniqueSearchableValue123'; + const RECORD_NAME_FIELD_VALUE = 'NameValue'; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: OBJECT_NAME_SINGULAR, + namePlural: OBJECT_NAME_PLURAL, + labelSingular: 'Search Vector Test Object', + labelPlural: 'Search Vector Test Objects', + icon: 'IconSearch', + isLabelSyncedWithName: false, + }, + }); + + testObjectMetadataId = objectMetadataId; + + const { + data: { + createOneField: { id: fieldMetadataId }, + }, + } = await createOneFieldMetadata({ + expectToFail: false, + input: { + name: INITIAL_FIELD_NAME, + label: 'Searchable Title', + type: FieldMetadataType.TEXT, + objectMetadataId: testObjectMetadataId, + isLabelSyncedWithName: false, + }, + gqlFields: ` + id + name + label + `, + }); + + testFieldMetadataId = fieldMetadataId; + + await updateOneObjectMetadata({ + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { + labelIdentifierFieldMetadataId: testFieldMetadataId, + }, + }, + expectToFail: false, + }); + + const { data } = await createManyOperation({ + objectMetadataSingularName: OBJECT_NAME_SINGULAR, + objectMetadataPluralName: OBJECT_NAME_PLURAL, + gqlFields: `id name ${INITIAL_FIELD_NAME}`, + data: [ + { + [INITIAL_FIELD_NAME]: RECORD_FIELD_VALUE, + name: RECORD_NAME_FIELD_VALUE, + }, + ], + expectToFail: false, + }); + + createdRecordId = data.createdRecords[0].id; + }); + + afterAll(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: testObjectMetadataId }, + }); + }); + + it('should update search vector asExpression when renaming a field used as label identifier and search should work with new field name', async () => { + await updateOneFieldMetadata({ + input: { + idToUpdate: testFieldMetadataId, + updatePayload: { + name: UPDATED_FIELD_NAME, + label: 'Renamed Title', + isLabelSyncedWithName: false, + }, + }, + gqlFields: ` + id + name + label + `, + expectToFail: false, + }); + + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: { + id: { eq: testObjectMetadataId }, + }, + paging: { first: 1 }, + }, + gqlFields: ` + id + nameSingular + fieldsList { + id + name + type + settings + } + `, + }); + + expect(objects).toBeDefined(); + expect(objects.length).toBe(1); + + const testObject = objects[0]; + + jestExpectToBeDefined(testObject); + jestExpectToBeDefined(testObject.fieldsList); + + const searchVectorField = testObject.fieldsList.find( + (field: FieldMetadataDTO) => field.type === FieldMetadataType.TS_VECTOR, + ); + + jestExpectToBeDefined(searchVectorField); + + const settings = searchVectorField.settings as { + asExpression?: string; + generatedType?: string; + }; + + jestExpectToBeDefined(settings); + expect(settings.asExpression).toBeDefined(); + expect(settings.asExpression).toContain(UPDATED_FIELD_NAME); + expect(settings.asExpression).not.toContain(INITIAL_FIELD_NAME); + + const searchResult = await search({ + searchInput: RECORD_FIELD_VALUE, + includedObjectNameSingulars: [OBJECT_NAME_SINGULAR], + limit: 10, + expectToFail: false, + }); + + expect(searchResult.data).toBeDefined(); + expect(searchResult.data.search).toBeDefined(); + expect(searchResult.data.search.edges).toBeDefined(); + expect(searchResult.data.search.edges.length).toBe(1); + expect(searchResult.data.search.edges[0].node.recordId).toBe( + createdRecordId, + ); + expect(searchResult.data.search.edges[0].node.objectNameSingular).toBe( + OBJECT_NAME_SINGULAR, + ); + }); +}); diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap index 7a925fd750..5deee1f766 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-update-one-object-metadata.integration-spec.ts.snap @@ -4,59 +4,12 @@ exports[`Object metadata update should fail when labelIdentifier is a UUID field [ { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "labelIdentifierFieldMetadataId validation failed: field type uuid is reserved for system object metadata", - "userFriendlyMessage": "Field cannot be used as label identifier", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "whatevers", - "nameSingular": "whatever", - "universalIdentifier": Any, - }, - "metadataName": "objectMetadata", - "status": "fail", - "type": "update", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "View field position cannot be lower than label identifier view field position", - "userFriendlyMessage": "View field position cannot be lower than label identifier view field position", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "universalIdentifier": Any, - "viewId": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - ], - }, - "message": "Validation failed for 1 objectMetadata, 1 viewField", - "summary": { - "objectMetadata": 1, - "totalErrors": 2, - "viewField": 1, - }, - "userFriendlyMessage": "Metadata validation failed", + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "Invalid object input.", }, - "message": "Multiple validation errors occurred while updating object", - "name": "GraphQLError", + "message": "Failed to compute search vector column expression for field id", + "name": "UserInputError", }, ] `; @@ -65,59 +18,12 @@ exports[`Object metadata update should fail when labelIdentifier is not a TEXT o [ { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "labelIdentifierFieldMetadataId validation failed: field type not compatible", - "userFriendlyMessage": "Field cannot be used as label identifier", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "whatevers", - "nameSingular": "whatever", - "universalIdentifier": Any, - }, - "metadataName": "objectMetadata", - "status": "fail", - "type": "update", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "View field position cannot be lower than label identifier view field position", - "userFriendlyMessage": "View field position cannot be lower than label identifier view field position", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "universalIdentifier": Any, - "viewId": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - ], - }, - "message": "Validation failed for 1 objectMetadata, 1 viewField", - "summary": { - "objectMetadata": 1, - "totalErrors": 2, - "viewField": 1, - }, - "userFriendlyMessage": "Metadata validation failed", + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "Invalid object input.", }, - "message": "Multiple validation errors occurred while updating object", - "name": "GraphQLError", + "message": "Failed to compute search vector column expression for field testName", + "name": "UserInputError", }, ] `; @@ -126,64 +32,12 @@ exports[`Object metadata update should fail when labelIdentifier is not a known [ { "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "labelIdentifierFieldMetadataId validation failed: related field metadata not found", - "userFriendlyMessage": "Field declared as label identifier not found", - }, - ], - "flatEntityMinimalInformation": { - "id": Any, - "namePlural": "whatevers", - "nameSingular": "whatever", - "universalIdentifier": Any, - }, - "metadataName": "objectMetadata", - "status": "fail", - "type": "update", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View field position cannot be lower than label identifier view field position", - "userFriendlyMessage": "View field position cannot be lower than label identifier view field position", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataId": Any, - "id": Any, - "universalIdentifier": Any, - "viewId": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - ], - }, - "message": "Validation failed for 1 objectMetadata, 1 viewField", - "summary": { - "objectMetadata": 1, - "totalErrors": 2, - "viewField": 1, - }, - "userFriendlyMessage": "Metadata validation failed", + "code": "BAD_USER_INPUT", + "subCode": "INVALID_OBJECT_INPUT", + "userFriendlyMessage": "Invalid object input.", }, - "message": "Multiple validation errors occurred while updating object", - "name": "GraphQLError", + "message": "New label identifier field not found for object metadata", + "name": "UserInputError", }, ] `; diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/update-one-object-metadata-serach-vector-side-effect.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/update-one-object-metadata-serach-vector-side-effect.integration-spec.ts new file mode 100644 index 0000000000..2541848ab1 --- /dev/null +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/update-one-object-metadata-serach-vector-side-effect.integration-spec.ts @@ -0,0 +1,170 @@ +import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util'; +import { search } from 'test/integration/graphql/utils/search.util'; +import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util'; +import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; +import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; + +describe('Object metadata update - search vector side effect', () => { + let testObjectMetadataId: string; + let testFieldMetadataId: string; + let createdRecordId: string; + + const OBJECT_NAME_SINGULAR = 'searchVectorTestObject'; + const OBJECT_NAME_PLURAL = 'searchVectorTestObjects'; + const NEW_LABEL_IDENTIFIER_FIELD_NAME = 'searchableTitle'; + const RECORD_FIELD_VALUE = 'UniqueSearchableValue123'; + const RECORD_NAME_FIELD_VALUE = 'NameValue'; + + beforeAll(async () => { + const { + data: { + createOneObject: { id: objectMetadataId }, + }, + } = await createOneObjectMetadata({ + expectToFail: false, + input: { + nameSingular: OBJECT_NAME_SINGULAR, + namePlural: OBJECT_NAME_PLURAL, + labelSingular: 'Search Vector Test Object', + labelPlural: 'Search Vector Test Objects', + icon: 'IconSearch', + isLabelSyncedWithName: false, + }, + }); + + testObjectMetadataId = objectMetadataId; + + const { + data: { + createOneField: { id: fieldMetadataId }, + }, + } = await createOneFieldMetadata({ + expectToFail: false, + input: { + name: NEW_LABEL_IDENTIFIER_FIELD_NAME, + label: 'Searchable Title', + type: FieldMetadataType.TEXT, + objectMetadataId: testObjectMetadataId, + isLabelSyncedWithName: false, + }, + gqlFields: ` + id + name + label + `, + }); + + testFieldMetadataId = fieldMetadataId; + + const { data } = await createManyOperation({ + objectMetadataSingularName: OBJECT_NAME_SINGULAR, + objectMetadataPluralName: OBJECT_NAME_PLURAL, + gqlFields: `id name ${NEW_LABEL_IDENTIFIER_FIELD_NAME}`, + data: [ + { + [NEW_LABEL_IDENTIFIER_FIELD_NAME]: RECORD_FIELD_VALUE, + name: RECORD_NAME_FIELD_VALUE, + }, + ], + expectToFail: false, + }); + + createdRecordId = data.createdRecords[0].id; + }); + + afterAll(async () => { + await updateOneObjectMetadata({ + expectToFail: false, + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { + isActive: false, + }, + }, + }); + await deleteOneObjectMetadata({ + expectToFail: false, + input: { idToDelete: testObjectMetadataId }, + }); + }); + + it('should update search vector asExpression when updating label identifier and search should work with new field', async () => { + await updateOneObjectMetadata({ + input: { + idToUpdate: testObjectMetadataId, + updatePayload: { + labelIdentifierFieldMetadataId: testFieldMetadataId, + }, + }, + expectToFail: false, + }); + + const { objects } = await findManyObjectMetadata({ + expectToFail: false, + input: { + filter: { + id: { eq: testObjectMetadataId }, + }, + paging: { first: 1 }, + }, + gqlFields: ` + id + nameSingular + fieldsList { + id + name + type + settings + } + `, + }); + + expect(objects).toBeDefined(); + expect(objects.length).toBe(1); + + const testObject = objects[0]; + + jestExpectToBeDefined(testObject); + jestExpectToBeDefined(testObject.fieldsList); + + const searchVectorField = testObject.fieldsList.find( + (field: FieldMetadataDTO) => field.type === FieldMetadataType.TS_VECTOR, + ); + + jestExpectToBeDefined(searchVectorField); + + const settings = searchVectorField.settings as { + asExpression?: string; + generatedType?: string; + }; + + jestExpectToBeDefined(settings); + expect(settings.asExpression).toBeDefined(); + expect(settings.asExpression).toContain(NEW_LABEL_IDENTIFIER_FIELD_NAME); + expect(settings.asExpression).not.toContain('name'); + + const searchResult = await search({ + searchInput: RECORD_FIELD_VALUE, + includedObjectNameSingulars: [OBJECT_NAME_SINGULAR], + limit: 10, + expectToFail: false, + }); + + expect(searchResult.data).toBeDefined(); + expect(searchResult.data.search).toBeDefined(); + expect(searchResult.data.search.edges).toBeDefined(); + expect(searchResult.data.search.edges.length).toBe(1); + expect(searchResult.data.search.edges[0].node.recordId).toBe( + createdRecordId, + ); + expect(searchResult.data.search.edges[0].node.objectNameSingular).toBe( + OBJECT_NAME_SINGULAR, + ); + }); +});