Wire up search field metadata (#21964)
## Part 1 - Exact scope of the current PR (#21964) close https://github.com/twentyhq/core-team-issues/issues/2586 This PR introduces `searchFieldMetadata` as a first-class flat metadata entity and migrates the existing search surface onto it, with **no change to which records are searchable** (ISO with `main`). In scope (what the PR does): - New flat entity `searchFieldMetadata` (universalIdentifier, applicationId, **`position`**, maps, conversions), registered in the central flat-entity constants and the migration build orchestrator. - `searchVector.asExpression` is **derived server-side** from `searchFieldMetadata` rows (validated by `isSafeTsVectorExpression`); never trusted from client input. - **Derivation order is deterministic, driven by each row's `position`** ([compute-search-vector-as-expression-from-search-field-metadatas.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util.ts)), replacing the previous non-deterministic `(createdAt, id)` sort. That sort collapsed to random UUIDs for standard fields (same `createdAt`), so any rename/relabel rewrote the `STORED` generated column to a logically-identical-but-textually-different expression and produced a permanent per-workspace diff vs the standard definition. Ordering now equals provisioning order; ties break on `universalIdentifier`. - Provisioning at object creation mirrors the existing surface exactly **and seeds `position`**: - custom objects -> the `name` field only, at `position: 0` ([build-default-search-field-metadatas-for-custom-object.util.ts](packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-default-search-field-metadatas-for-custom-object.util.ts)) - standard objects -> their curated `SEARCH_FIELDS_FOR_*` sets, `position` = the curated index - Backfill (instance + workspace commands in `2-16`) provisions rows for existing workspaces with the same surface **and the same positions** (standard from the curated standard maps, custom `name` = `0`), scoped to the workspace's own custom application ([build-search-field-metadata-backfill-operations.util.ts](packages/twenty-server/src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util.ts)). The `position` column is added in the same `2-16` fast instance command as `universalIdentifier`/`applicationId`. - Field rename of an already-indexed field recomputes `asExpression` (positions preserved, so order is stable) ([recompute-search-vector-on-field-rename.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/recompute-search-vector-on-field-rename.util.ts)). - Field delete drops the matching row(s) and recomputes; remaining rows keep their relative order (no renumber) ([from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util.ts)). - Object relabel is **additive** and ISO/regression-fix only: it indexes the new label identifier **appended last (`position = max(existing) + 1`)** without dropping `name` ([recompute-search-vector-on-label-identifier-update.util.ts](packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-on-label-identifier-update.util.ts)). This is a deliberate, temporary bridge. Explicitly OUT of scope (deferred): - No API to edit `searchFieldMetadata` (no user-facing search-field configuration, including `position` — it is internal and only written by provisioning/backfill/recompute). - No auto-indexing of arbitrary searchable fields. Creating a custom TEXT/EMAILS/etc. field does NOT add it to search (the `computeSearchFieldMetadataCreationForFields` behavior was removed in `e6820ad`). - No field-type-transition handling (field type is immutable - not in `FLAT_FIELD_METADATA_EDITABLE_PROPERTIES`, so that path was dead code). - No `position` validation (uniqueness/range) and no multi-vector / per-field `weight` config — deferred to the configurable-search follow-up (#1428). Net: `searchFieldMetadata` becomes the source of truth for the *same* surface as `main`. The only intentional divergences from `main` are "relabel preserves `name`" (additive) and the deterministic `position`-ordered `asExpression` (a correctness/perf fix that is byte-identical to provisioning order, so it does not change the searchable surface). --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+6
@@ -46,6 +46,7 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps('obj-1'),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
@@ -69,6 +70,7 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
@@ -92,6 +94,7 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(new RegExp('Cannot delete standard field "city"'));
|
||||
});
|
||||
@@ -110,6 +113,7 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([customField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
});
|
||||
|
||||
expect(result.flatFieldMetadatasToDelete).toContainEqual(
|
||||
@@ -132,6 +136,7 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(FieldMetadataException);
|
||||
});
|
||||
@@ -155,6 +160,7 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardAppField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { type FieldMetadataType } 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 { 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 { recomputeSearchVectorFieldFromSearchFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-from-search-field-metadatas.util';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
export const computeSearchFieldMetadataDeletionForDeletedFields = ({
|
||||
flatFieldMetadatasToDelete,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
}: {
|
||||
flatFieldMetadatasToDelete: FlatFieldMetadata[];
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
| 'flatObjectMetadataMaps'
|
||||
| 'flatFieldMetadataMaps'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
>): {
|
||||
searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[];
|
||||
flatSearchVectorFieldsToUpdate: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>[];
|
||||
} => {
|
||||
const deletedFieldIds = new Set(
|
||||
flatFieldMetadatasToDelete.map((flatFieldMetadata) => flatFieldMetadata.id),
|
||||
);
|
||||
const affectedObjectMetadataIds = new Set(
|
||||
flatFieldMetadatasToDelete.map(
|
||||
(flatFieldMetadata) => flatFieldMetadata.objectMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[] = [];
|
||||
const flatSearchVectorFieldsToUpdate: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>[] =
|
||||
[];
|
||||
|
||||
for (const objectMetadataId of affectedObjectMetadataIds) {
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata) || !flatObjectMetadata.isSearchable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectSearchFieldMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow<FlatSearchFieldMetadata>({
|
||||
flatEntityMaps: flatSearchFieldMetadataMaps,
|
||||
flatEntityIds: flatObjectMetadata.searchFieldMetadataIds,
|
||||
});
|
||||
|
||||
const rowsToDelete = objectSearchFieldMetadatas.filter(
|
||||
(searchFieldMetadata) =>
|
||||
deletedFieldIds.has(searchFieldMetadata.fieldMetadataId),
|
||||
);
|
||||
|
||||
if (rowsToDelete.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchFieldMetadatasToDelete.push(...rowsToDelete);
|
||||
|
||||
const remainingSearchFieldMetadatas = objectSearchFieldMetadatas.filter(
|
||||
(searchFieldMetadata) =>
|
||||
!deletedFieldIds.has(searchFieldMetadata.fieldMetadataId),
|
||||
);
|
||||
|
||||
const flatSearchVectorFieldToUpdate =
|
||||
recomputeSearchVectorFieldFromSearchFieldMetadatas({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
searchFieldMetadatas: remainingSearchFieldMetadatas,
|
||||
});
|
||||
|
||||
if (isDefined(flatSearchVectorFieldToUpdate)) {
|
||||
flatSearchVectorFieldsToUpdate.push(flatSearchVectorFieldToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
searchFieldMetadatasToDelete,
|
||||
flatSearchVectorFieldsToUpdate,
|
||||
};
|
||||
};
|
||||
+21
-1
@@ -1,3 +1,4 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
@@ -12,18 +13,24 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { computeFlatFieldMetadataRelatedFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-flat-field-metadata-related-flat-field-metadata.util';
|
||||
import { computeSearchFieldMetadataDeletionForDeletedFields } from 'src/engine/metadata-modules/flat-field-metadata/utils/compute-search-field-metadata-deletion-for-deleted-fields.util';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
|
||||
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
type FromDeleteFieldInputToFlatFieldMetadatasToDeleteArgs = {
|
||||
deleteOneFieldInput: DeleteOneFieldInput;
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatFieldMetadataMaps' | 'flatIndexMaps' | 'flatObjectMetadataMaps'
|
||||
| 'flatFieldMetadataMaps'
|
||||
| 'flatIndexMaps'
|
||||
| 'flatObjectMetadataMaps'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
>;
|
||||
// TODO refactor as a side effect service
|
||||
export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
|
||||
@@ -31,10 +38,13 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
|
||||
deleteOneFieldInput: rawDeleteOneInput,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
}: FromDeleteFieldInputToFlatFieldMetadatasToDeleteArgs): {
|
||||
flatFieldMetadatasToDelete: UniversalFlatFieldMetadata[];
|
||||
flatIndexesToUpdate: UniversalFlatIndexMetadata[];
|
||||
flatIndexesToDelete: UniversalFlatIndexMetadata[];
|
||||
searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[];
|
||||
flatSearchVectorFieldsToUpdate: FlatFieldMetadata<FieldMetadataType.TS_VECTOR>[];
|
||||
} => {
|
||||
const { id: fieldMetadataToDeleteId } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
@@ -177,9 +187,19 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { searchFieldMetadatasToDelete, flatSearchVectorFieldsToUpdate } =
|
||||
computeSearchFieldMetadataDeletionForDeletedFields({
|
||||
flatFieldMetadatasToDelete,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatFieldMetadatasToDelete,
|
||||
flatIndexesToDelete,
|
||||
flatIndexesToUpdate,
|
||||
searchFieldMetadatasToDelete,
|
||||
flatSearchVectorFieldsToUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
+3
@@ -35,6 +35,7 @@ type FromUpdateFieldInputToFlatFieldMetadataArgs = {
|
||||
| 'flatViewGroupMaps'
|
||||
| 'flatViewMaps'
|
||||
| 'flatViewFieldMaps'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
>;
|
||||
|
||||
type FlatFieldMetadataAndIndexToUpdate = {
|
||||
@@ -51,6 +52,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
flatViewGroupMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
isSystemBuild,
|
||||
}: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult<FlatFieldMetadataAndIndexToUpdate> => {
|
||||
const updateFieldInputInformalProperties =
|
||||
@@ -150,6 +152,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({
|
||||
toFlatFieldMetadata,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
flatApplication,
|
||||
});
|
||||
|
||||
|
||||
+14
-18
@@ -15,8 +15,8 @@ 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 { recomputeSearchVectorOnFieldRename } from 'src/engine/metadata-modules/flat-field-metadata/utils/recompute-search-vector-on-field-rename.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';
|
||||
|
||||
@@ -41,6 +41,7 @@ type HandleFlatFieldMetadataUpdateSideEffectArgs = FromTo<
|
||||
| 'flatViewGroupMaps'
|
||||
| 'flatViewMaps'
|
||||
| 'flatViewFieldMaps'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
> & {
|
||||
flatApplication: FlatApplication;
|
||||
};
|
||||
@@ -71,6 +72,7 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
flatViewGroupMaps,
|
||||
flatViewMaps,
|
||||
flatViewFieldMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
flatApplication,
|
||||
}: HandleFlatFieldMetadataUpdateSideEffectArgs): FieldInputTranspilationResult<FlatFieldMetadataUpdateSideEffects> => {
|
||||
const sideEffectResult = structuredClone(
|
||||
@@ -143,24 +145,18 @@ export const handleFlatFieldMetadataUpdateSideEffect = ({
|
||||
flatEntityId: fromFlatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
const isLabelIdentifierFieldMetadata =
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataId ===
|
||||
toFlatFieldMetadata.id;
|
||||
const { flatSearchVectorFieldToUpdate } = recomputeSearchVectorOnFieldRename({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (isLabelIdentifierFieldMetadata) {
|
||||
const flatSearchVectorFieldToUpdate =
|
||||
handleLabelIdentifierChangesDuringFieldUpdate({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (isDefined(flatSearchVectorFieldToUpdate)) {
|
||||
sideEffectResult.flatFieldMetadatasToUpdate.push(
|
||||
flatSearchVectorFieldToUpdate,
|
||||
);
|
||||
}
|
||||
if (isDefined(flatSearchVectorFieldToUpdate)) {
|
||||
sideEffectResult.flatFieldMetadatasToUpdate.push(
|
||||
flatSearchVectorFieldToUpdate,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { type FieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
import { findOrThrow, type SearchableFieldType } 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/utils/get-ts-vector-column-expression.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.fieldIds,
|
||||
});
|
||||
|
||||
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,
|
||||
universalSettings: {
|
||||
...searchVectorField.universalSettings,
|
||||
asExpression: newAsExpression,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
throw new FieldMetadataException(
|
||||
`Failed to compute search vector column expression for field ${toFlatFieldMetadata.name}`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { type FieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
|
||||
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 { recomputeSearchVectorFieldFromSearchFieldMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-field-from-search-field-metadatas.util';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
|
||||
export type FieldMetadataUpdateSearchVectorSideEffect = {
|
||||
flatSearchVectorFieldToUpdate:
|
||||
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
|
||||
| undefined;
|
||||
};
|
||||
|
||||
type RecomputeSearchVectorOnFieldRenameArgs = {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
} & FromTo<FlatFieldMetadata, 'flatFieldMetadata'> &
|
||||
Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatFieldMetadataMaps' | 'flatSearchFieldMetadataMaps'
|
||||
>;
|
||||
|
||||
const EMPTY_SIDE_EFFECT: FieldMetadataUpdateSearchVectorSideEffect = {
|
||||
flatSearchVectorFieldToUpdate: undefined,
|
||||
};
|
||||
|
||||
export const recomputeSearchVectorOnFieldRename = ({
|
||||
fromFlatFieldMetadata,
|
||||
toFlatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
}: RecomputeSearchVectorOnFieldRenameArgs): FieldMetadataUpdateSearchVectorSideEffect => {
|
||||
if (!flatObjectMetadata.isSearchable) {
|
||||
return EMPTY_SIDE_EFFECT;
|
||||
}
|
||||
|
||||
const objectSearchFieldMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow<FlatSearchFieldMetadata>({
|
||||
flatEntityMaps: flatSearchFieldMetadataMaps,
|
||||
flatEntityIds: flatObjectMetadata.searchFieldMetadataIds,
|
||||
});
|
||||
|
||||
const isIndexedField = objectSearchFieldMetadatas.some(
|
||||
(searchFieldMetadata) =>
|
||||
searchFieldMetadata.fieldMetadataId === toFlatFieldMetadata.id,
|
||||
);
|
||||
|
||||
const hasNameChanged =
|
||||
fromFlatFieldMetadata.name !== toFlatFieldMetadata.name;
|
||||
|
||||
if (!isIndexedField || !hasNameChanged) {
|
||||
return EMPTY_SIDE_EFFECT;
|
||||
}
|
||||
|
||||
return {
|
||||
flatSearchVectorFieldToUpdate:
|
||||
recomputeSearchVectorFieldFromSearchFieldMetadatas({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
searchFieldMetadatas: objectSearchFieldMetadatas,
|
||||
overrideFlatFieldMetadataById: new Map([
|
||||
[toFlatFieldMetadata.id, toFlatFieldMetadata],
|
||||
]),
|
||||
}),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user