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
@@ -171,12 +171,14 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
flatViewsToDelete,
flatViewFieldsToDelete,
flatViewsToUpdate,
flatFieldMetadatasToUpdate: flatFieldMetadatasToUpdateFromSideEffect,
} = sideEffectResult.result;
return {
flatFieldMetadatasToUpdate: [
...accumulator.flatFieldMetadatasToUpdate,
toFlatFieldMetadata,
...flatFieldMetadatasToUpdateFromSideEffect,
],
flatIndexMetadatasToUpdate: [
...accumulator.flatIndexMetadatasToUpdate,
@@ -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,
@@ -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<FlatFieldMetadata, 'flatFieldMetadata'> &
Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
export const handleLabelIdentifierChangesDuringFieldUpdate = ({
fromFlatFieldMetadata,
toFlatFieldMetadata,
flatObjectMetadata,
flatFieldMetadataMaps,
}: HandleLabelIdentifierChangesDuringFieldUpdateArgs):
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
| 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<FieldMetadataType.TS_VECTOR>;
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,
);
}
};
@@ -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,
};
};
@@ -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,
};
};
@@ -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<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
export const recomputeSearchVectorFieldAfterLabelIdentifierUpdate = ({
existingFlatObjectMetadata,
flatFieldMetadataMaps,
updatedLabelIdentifierFieldMetadataId,
}: RecomputeSearchVectorFieldAfterLabelIdentifierUpdateArgs):
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
| undefined => {
const objectFlatFieldMetadatas =
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityMaps: flatFieldMetadataMaps,
flatEntityIds: existingFlatObjectMetadata.fieldMetadataIds,
});
const searchVectorField = findOrThrow(
objectFlatFieldMetadatas,
(field) => field.name === SEARCH_VECTOR_FIELD.name,
) as FlatFieldMetadata<FieldMetadataType.TS_VECTOR>;
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,
);
}
};
@@ -81,6 +81,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const {
otherObjectFlatFieldMetadatasToUpdate,
sameObjectFlatFieldMetadatasToUpdate,
flatObjectMetadataToUpdate,
flatIndexMetadatasToUpdate,
flatViewFieldsToCreate,
@@ -111,7 +112,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
fieldMetadata: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: otherObjectFlatFieldMetadatasToUpdate,
flatEntityToUpdate: [
...otherObjectFlatFieldMetadatasToUpdate,
...sameObjectFlatFieldMetadatasToUpdate,
],
},
viewField: {
flatEntityToCreate: flatViewFieldsToCreate,