Fix - Update searchVector when labelIdentifier is updated (#16940)

Fixes https://github.com/twentyhq/twenty/issues/16891

In next PR, validation rules will be added in migration logic
This commit is contained in:
Etienne
2026-01-06 13:55:25 +01:00
committed by GitHub
parent bd9e5986d2
commit 0c6f4021bf
18 changed files with 823 additions and 177 deletions
@@ -945,6 +945,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
const { aggregatedOrchestratorActionsReport } =
aggregateOrchestratorActionsReport({
orchestratorActionsReport,
flatFieldMetadataMaps:
optimisticAllFlatEntityMaps.flatFieldMetadataMaps,
});
return {
@@ -0,0 +1,8 @@
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type OrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-orchestrator.type';
export type AggregateOrchestratorActionsReportArgs = {
orchestratorActionsReport: OrchestratorActionsReport;
flatFieldMetadataMaps?: FlatEntityMaps<FlatFieldMetadata>;
};
@@ -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<FlatFieldMetadata>>(
(flatEntityMaps, field) =>
addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: field,
flatEntityMaps,
}),
createEmptyFlatEntityMaps() as FlatEntityMaps<FlatFieldMetadata>,
);
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,
);
});
});
@@ -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<string, CreateFieldAction>;
createdObjectActionByObjectMetadataId: Record<string, CreateObjectAction>;
@@ -18,7 +15,7 @@ type AggregatedActions = {
export const aggregateOrchestratorActionsReportCreateObjectAndCreateFieldActions =
({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportCreateObjectAndCreateFieldActionsArgs): OrchestratorActionsReport => {
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
const initialCreatedObjectActionByObjectMetadataId = (
orchestratorActionsReport.objectMetadata.create as CreateObjectAction[]
).reduce(
@@ -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<string, DeleteFieldAction>;
};
export const aggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActions =
({
orchestratorActionsReport,
}: AggregateOrchestratorActionsReportDeleteObjectAndDeleteFieldActionsArgs): OrchestratorActionsReport => {
}: AggregateOrchestratorActionsReportArgs): OrchestratorActionsReport => {
const deleteObjectActionByObjectMetadataId = (
orchestratorActionsReport.objectMetadata.delete as DeleteObjectAction[]
).reduce<Record<string, DeleteObjectAction>>(
@@ -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],
},
};
};
@@ -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,
);
@@ -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,
};
}
}
}