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:
+2
-1
@@ -304,7 +304,8 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
|
||||
case 'fieldPermission':
|
||||
case 'webhook':
|
||||
case 'applicationVariable':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
if (!isDefined(idByUniversalIdentifier)) {
|
||||
return action;
|
||||
}
|
||||
|
||||
+6
@@ -44,6 +44,7 @@ import { WorkspaceMigrationRoleTargetActionsBuilderService } from 'src/engine/wo
|
||||
import { WorkspaceMigrationRoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/role/workspace-migration-role-actions-builder.service';
|
||||
import { WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate-group/workspace-migration-row-level-permission-predicate-group-actions-builder.service';
|
||||
import { WorkspaceMigrationRowLevelPermissionPredicateActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/workspace-migration-row-level-permission-predicate-actions-builder.service';
|
||||
import { WorkspaceMigrationSearchFieldMetadataActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/workspace-migration-search-field-metadata-actions.builder.service';
|
||||
import { WorkspaceMigrationSkillActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/skill/workspace-migration-skill-actions-builder.service';
|
||||
import { WorkspaceMigrationViewFieldGroupActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field-group/workspace-migration-view-field-group-actions-builder.service';
|
||||
import { WorkspaceMigrationViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-field/workspace-migration-view-field-actions-builder.service';
|
||||
@@ -156,6 +157,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
workspaceMigrationWebhookActionsBuilderService: WorkspaceMigrationWebhookActionsBuilderService,
|
||||
workspaceMigrationApplicationVariableActionsBuilderService: WorkspaceMigrationApplicationVariableActionsBuilderService,
|
||||
workspaceMigrationConnectionProviderActionsBuilderService: WorkspaceMigrationConnectionProviderActionsBuilderService,
|
||||
workspaceMigrationSearchFieldMetadataActionsBuilderService: WorkspaceMigrationSearchFieldMetadataActionsBuilderService,
|
||||
) {
|
||||
// The order of this array defines the execution order of the per-entity
|
||||
// builders. Each builder may mutate `optimisticAllFlatEntityMaps`, so
|
||||
@@ -204,6 +206,10 @@ export class WorkspaceMigrationBuildOrchestratorService {
|
||||
ALL_METADATA_NAME.viewSort,
|
||||
workspaceMigrationViewSortActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.searchFieldMetadata,
|
||||
workspaceMigrationSearchFieldMetadataActionsBuilderService,
|
||||
),
|
||||
createEntityActionsBuilderTask(
|
||||
ALL_METADATA_NAME.logicFunction,
|
||||
workspaceMigrationLogicFunctionActionsBuilderService,
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
|
||||
"objectPermissionUniversalIdentifiers",
|
||||
"fieldPermissionUniversalIdentifiers",
|
||||
"viewUniversalIdentifiers",
|
||||
"searchFieldMetadataUniversalIdentifiers",
|
||||
],
|
||||
"objectPermission": [],
|
||||
"pageLayout": [
|
||||
@@ -53,6 +54,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should matc
|
||||
"childRowLevelPermissionPredicateGroupUniversalIdentifiers",
|
||||
"rowLevelPermissionPredicateUniversalIdentifiers",
|
||||
],
|
||||
"searchFieldMetadata": [],
|
||||
"skill": [],
|
||||
"view": [
|
||||
"viewFieldUniversalIdentifiers",
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
|
||||
frontComponent: {},
|
||||
applicationVariable: {},
|
||||
connectionProvider: {},
|
||||
searchFieldMetadata: {},
|
||||
} as const satisfies {
|
||||
[P in AllMetadataName]: Partial<{
|
||||
// TODO prastoin: improve strict typing to recursively serach for nested SerializedRelation
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
|
||||
export type UniversalFlatSearchFieldMetadata = UniversalFlatEntityFrom<
|
||||
SearchFieldMetadataEntity,
|
||||
'searchFieldMetadata'
|
||||
>;
|
||||
+3
@@ -6,6 +6,7 @@ export const computeOrderedMigrationActions = (
|
||||
): AllUniversalWorkspaceMigrationAction[] => {
|
||||
return [
|
||||
// Object and fields and indexes
|
||||
...aggregatedOrchestratorActionsReport.searchFieldMetadata.delete,
|
||||
...aggregatedOrchestratorActionsReport.index.delete,
|
||||
...aggregatedOrchestratorActionsReport.fieldMetadata.delete,
|
||||
...aggregatedOrchestratorActionsReport.objectMetadata.delete,
|
||||
@@ -15,6 +16,8 @@ export const computeOrderedMigrationActions = (
|
||||
...aggregatedOrchestratorActionsReport.fieldMetadata.update,
|
||||
...aggregatedOrchestratorActionsReport.index.create,
|
||||
...aggregatedOrchestratorActionsReport.index.update.flat(),
|
||||
...aggregatedOrchestratorActionsReport.searchFieldMetadata.create,
|
||||
...aggregatedOrchestratorActionsReport.searchFieldMetadata.update,
|
||||
///
|
||||
|
||||
// Views
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
|
||||
import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type';
|
||||
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
|
||||
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
|
||||
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
|
||||
import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type';
|
||||
|
||||
export type FlatCreateSearchFieldMetadataAction =
|
||||
BaseFlatCreateWorkspaceMigrationAction<'searchFieldMetadata'>;
|
||||
|
||||
export type FlatUpdateSearchFieldMetadataAction =
|
||||
BaseFlatUpdateWorkspaceMigrationAction<'searchFieldMetadata'>;
|
||||
|
||||
export type FlatDeleteSearchFieldMetadataAction =
|
||||
BaseFlatDeleteWorkspaceMigrationAction<'searchFieldMetadata'>;
|
||||
|
||||
export type UniversalCreateSearchFieldMetadataAction =
|
||||
BaseUniversalCreateWorkspaceMigrationAction<'searchFieldMetadata'>;
|
||||
|
||||
export type UniversalUpdateSearchFieldMetadataAction =
|
||||
BaseUniversalUpdateWorkspaceMigrationAction<'searchFieldMetadata'>;
|
||||
|
||||
export type UniversalDeleteSearchFieldMetadataAction =
|
||||
BaseUniversalDeleteWorkspaceMigrationAction<'searchFieldMetadata'>;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
|
||||
import { UniversalUpdateSearchFieldMetadataAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/types/workspace-migration-search-field-metadata-action.type';
|
||||
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
|
||||
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
|
||||
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
|
||||
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
|
||||
import { FlatSearchFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-search-field-metadata-validator.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationSearchFieldMetadataActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatSearchFieldMetadataValidatorService: FlatSearchFieldMetadataValidatorService,
|
||||
) {
|
||||
super(ALL_METADATA_NAME.searchFieldMetadata);
|
||||
}
|
||||
|
||||
protected validateFlatEntityCreation(
|
||||
args: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
>,
|
||||
): UniversalFlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata,
|
||||
'create'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatSearchFieldMetadataValidatorService.validateFlatSearchFieldMetadataCreation(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatSearchFieldMetadataToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create',
|
||||
metadataName: 'searchFieldMetadata',
|
||||
flatEntity: flatSearchFieldMetadataToValidate,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityDeletion(
|
||||
args: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
>,
|
||||
): UniversalFlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata,
|
||||
'delete'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatSearchFieldMetadataValidatorService.validateFlatSearchFieldMetadataDeletion(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { flatEntityToValidate: flatSearchFieldMetadataToValidate } = args;
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete',
|
||||
metadataName: 'searchFieldMetadata',
|
||||
universalIdentifier:
|
||||
flatSearchFieldMetadataToValidate.universalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected validateFlatEntityUpdate(
|
||||
args: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
>,
|
||||
): UniversalFlatEntityValidationReturnType<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata,
|
||||
'update'
|
||||
> {
|
||||
const validationResult =
|
||||
this.flatSearchFieldMetadataValidatorService.validateFlatSearchFieldMetadataUpdate(
|
||||
args,
|
||||
);
|
||||
|
||||
if (validationResult.errors.length > 0) {
|
||||
return {
|
||||
status: 'fail',
|
||||
...validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
const { universalIdentifier, flatEntityUpdate } = args;
|
||||
|
||||
const updateSearchFieldMetadataAction: UniversalUpdateSearchFieldMetadataAction =
|
||||
{
|
||||
type: 'update',
|
||||
metadataName: 'searchFieldMetadata',
|
||||
universalIdentifier,
|
||||
update: flatEntityUpdate,
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: updateSearchFieldMetadataAction,
|
||||
};
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.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 { SearchFieldMetadataExceptionCode } from 'src/engine/metadata-modules/search-field-metadata/exceptions/search-field-metadata.exception';
|
||||
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
|
||||
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class FlatSearchFieldMetadataValidatorService {
|
||||
constructor() {}
|
||||
|
||||
validateFlatSearchFieldMetadataCreation({
|
||||
flatEntityToValidate: flatSearchFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatSearchFieldMetadataMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
},
|
||||
}: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
>): FailedFlatEntityValidation<'searchFieldMetadata', 'create'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier:
|
||||
flatSearchFieldMetadataToValidate.universalIdentifier,
|
||||
objectMetadataUniversalIdentifier:
|
||||
flatSearchFieldMetadataToValidate.objectMetadataUniversalIdentifier,
|
||||
},
|
||||
metadataName: 'searchFieldMetadata',
|
||||
type: 'create',
|
||||
});
|
||||
|
||||
const existingFlatSearchFieldMetadata = findFlatEntityByUniversalIdentifier(
|
||||
{
|
||||
universalIdentifier:
|
||||
flatSearchFieldMetadataToValidate.universalIdentifier,
|
||||
flatEntityMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(existingFlatSearchFieldMetadata)) {
|
||||
const searchFieldMetadataId =
|
||||
flatSearchFieldMetadataToValidate.universalIdentifier;
|
||||
|
||||
validationResult.errors.push({
|
||||
code: SearchFieldMetadataExceptionCode.INVALID_SEARCH_FIELD_METADATA_DATA,
|
||||
message: t`Search field metadata with id ${searchFieldMetadataId} already exists`,
|
||||
userFriendlyMessage: msg`Search field metadata already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
const flatFieldMetadata = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier:
|
||||
flatSearchFieldMetadataToValidate.fieldMetadataUniversalIdentifier,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
validationResult.errors.push({
|
||||
code: SearchFieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
message: t`Field metadata not found`,
|
||||
userFriendlyMessage: msg`Field metadata not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
|
||||
universalIdentifier:
|
||||
flatSearchFieldMetadataToValidate.objectMetadataUniversalIdentifier,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
validationResult.errors.push({
|
||||
code: SearchFieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
message: t`Object metadata not found`,
|
||||
userFriendlyMessage: msg`Object metadata not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
const otherFlatSearchFieldMetadatas =
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({
|
||||
universalIdentifiers:
|
||||
flatObjectMetadata.searchFieldMetadataUniversalIdentifiers,
|
||||
flatEntityMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const equivalentExistingFlatSearchFieldMetadataExists =
|
||||
otherFlatSearchFieldMetadatas.some(
|
||||
(flatSearchFieldMetadata) =>
|
||||
flatSearchFieldMetadata.universalIdentifier !==
|
||||
flatSearchFieldMetadataToValidate.universalIdentifier &&
|
||||
flatSearchFieldMetadata.fieldMetadataUniversalIdentifier ===
|
||||
flatSearchFieldMetadataToValidate.fieldMetadataUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (equivalentExistingFlatSearchFieldMetadataExists) {
|
||||
validationResult.errors.push({
|
||||
code: SearchFieldMetadataExceptionCode.INVALID_SEARCH_FIELD_METADATA_DATA,
|
||||
message: t`Search field metadata with same fieldMetadataId and objectMetadataId already exists`,
|
||||
userFriendlyMessage: msg`Search field metadata already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatSearchFieldMetadataUpdate({
|
||||
universalIdentifier,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatSearchFieldMetadataMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
},
|
||||
}: FlatEntityUpdateValidationArgs<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
>): FailedFlatEntityValidation<'searchFieldMetadata', 'update'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier,
|
||||
},
|
||||
metadataName: 'searchFieldMetadata',
|
||||
type: 'update',
|
||||
});
|
||||
|
||||
const existingFlatSearchFieldMetadata = findFlatEntityByUniversalIdentifier(
|
||||
{
|
||||
universalIdentifier,
|
||||
flatEntityMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(existingFlatSearchFieldMetadata)) {
|
||||
validationResult.errors.push({
|
||||
code: SearchFieldMetadataExceptionCode.SEARCH_FIELD_METADATA_NOT_FOUND,
|
||||
message: t`Search field metadata to update not found`,
|
||||
userFriendlyMessage: msg`Search field metadata to update not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
public validateFlatSearchFieldMetadataDeletion({
|
||||
flatEntityToValidate: { universalIdentifier },
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatSearchFieldMetadataMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
},
|
||||
}: UniversalFlatEntityValidationArgs<
|
||||
typeof ALL_METADATA_NAME.searchFieldMetadata
|
||||
>): FailedFlatEntityValidation<'searchFieldMetadata', 'delete'> {
|
||||
const validationResult = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier,
|
||||
},
|
||||
metadataName: 'searchFieldMetadata',
|
||||
type: 'delete',
|
||||
});
|
||||
|
||||
const existingFlatSearchFieldMetadata = findFlatEntityByUniversalIdentifier(
|
||||
{
|
||||
universalIdentifier,
|
||||
flatEntityMaps: optimisticFlatSearchFieldMetadataMaps,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(existingFlatSearchFieldMetadata)) {
|
||||
validationResult.errors.push({
|
||||
code: SearchFieldMetadataExceptionCode.SEARCH_FIELD_METADATA_NOT_FOUND,
|
||||
message: t`Search field metadata to delete not found`,
|
||||
userFriendlyMessage: msg`Search field metadata to delete not found`,
|
||||
});
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
}
|
||||
}
|
||||
+3
@@ -33,6 +33,7 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
|
||||
import { FlatApplicationVariableValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-application-variable-validator.service';
|
||||
import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service';
|
||||
import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-connection-provider-validator.service';
|
||||
import { FlatSearchFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-search-field-metadata-validator.service';
|
||||
|
||||
@Module({
|
||||
imports: [FeatureFlagModule],
|
||||
@@ -69,6 +70,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man
|
||||
FlatWebhookValidatorService,
|
||||
FlatApplicationVariableValidatorService,
|
||||
FlatConnectionProviderValidatorService,
|
||||
FlatSearchFieldMetadataValidatorService,
|
||||
],
|
||||
exports: [
|
||||
FlatViewValidatorService,
|
||||
@@ -102,6 +104,7 @@ import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-man
|
||||
FlatWebhookValidatorService,
|
||||
FlatApplicationVariableValidatorService,
|
||||
FlatConnectionProviderValidatorService,
|
||||
FlatSearchFieldMetadataValidatorService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderValidatorsModule {}
|
||||
|
||||
+3
@@ -32,6 +32,7 @@ import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspac
|
||||
import { WorkspaceMigrationApplicationVariableActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/application-variable/workspace-migration-application-variable-actions-builder.service';
|
||||
import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service';
|
||||
import { WorkspaceMigrationConnectionProviderActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/workspace-migration-connection-provider-actions-builder.service';
|
||||
import { WorkspaceMigrationSearchFieldMetadataActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/workspace-migration-search-field-metadata-actions.builder.service';
|
||||
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module';
|
||||
|
||||
@Module({
|
||||
@@ -68,6 +69,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationWebhookActionsBuilderService,
|
||||
WorkspaceMigrationApplicationVariableActionsBuilderService,
|
||||
WorkspaceMigrationConnectionProviderActionsBuilderService,
|
||||
WorkspaceMigrationSearchFieldMetadataActionsBuilderService,
|
||||
],
|
||||
exports: [
|
||||
WorkspaceMigrationViewActionsBuilderService,
|
||||
@@ -101,6 +103,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
|
||||
WorkspaceMigrationWebhookActionsBuilderService,
|
||||
WorkspaceMigrationApplicationVariableActionsBuilderService,
|
||||
WorkspaceMigrationConnectionProviderActionsBuilderService,
|
||||
WorkspaceMigrationSearchFieldMetadataActionsBuilderService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderModule {}
|
||||
|
||||
+1
@@ -117,6 +117,7 @@ export const fromUniversalFlatObjectMetadataToFlatObjectMetadata = ({
|
||||
fieldIds: [],
|
||||
viewIds: [],
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
...emptyUniversalForeignKeyAggregators,
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import {
|
||||
FlatCreateSearchFieldMetadataAction,
|
||||
UniversalCreateSearchFieldMetadataAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/types/workspace-migration-search-field-metadata-action.type';
|
||||
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
|
||||
import {
|
||||
WorkspaceMigrationActionRunnerArgs,
|
||||
WorkspaceMigrationActionRunnerContext,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateSearchFieldMetadataActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'create',
|
||||
'searchFieldMetadata',
|
||||
) {
|
||||
override async transpileUniversalActionToFlatAction({
|
||||
action,
|
||||
allFlatEntityMaps,
|
||||
flatApplication,
|
||||
workspaceId,
|
||||
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateSearchFieldMetadataAction>): Promise<FlatCreateSearchFieldMetadataAction> {
|
||||
const { objectMetadataId, fieldMetadataId } =
|
||||
resolveUniversalRelationIdentifiersToIds({
|
||||
flatEntityMaps: allFlatEntityMaps,
|
||||
metadataName: action.metadataName,
|
||||
universalForeignKeyValues: action.flatEntity,
|
||||
});
|
||||
|
||||
return {
|
||||
...action,
|
||||
flatEntity: {
|
||||
...action.flatEntity,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
id: action.id ?? v4(),
|
||||
applicationId: flatApplication.id,
|
||||
workspaceId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatCreateSearchFieldMetadataAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner } = context;
|
||||
const { flatEntity } = flatAction;
|
||||
|
||||
await this.insertFlatEntitiesInRepository({
|
||||
queryRunner,
|
||||
flatEntities: [flatEntity],
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatCreateSearchFieldMetadataAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import {
|
||||
FlatDeleteSearchFieldMetadataAction,
|
||||
UniversalDeleteSearchFieldMetadataAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/types/workspace-migration-search-field-metadata-action.type';
|
||||
import {
|
||||
WorkspaceMigrationActionRunnerArgs,
|
||||
WorkspaceMigrationActionRunnerContext,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteSearchFieldMetadataActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'delete',
|
||||
'searchFieldMetadata',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override async transpileUniversalActionToFlatAction(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UniversalDeleteSearchFieldMetadataAction>,
|
||||
): Promise<FlatDeleteSearchFieldMetadataAction> {
|
||||
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatDeleteSearchFieldMetadataAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner, workspaceId } = context;
|
||||
|
||||
const searchFieldMetadataRepository =
|
||||
queryRunner.manager.getRepository<SearchFieldMetadataEntity>(
|
||||
SearchFieldMetadataEntity,
|
||||
);
|
||||
|
||||
await searchFieldMetadataRepository.delete({
|
||||
id: flatAction.entityId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteSearchFieldMetadataAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
|
||||
import {
|
||||
FlatUpdateSearchFieldMetadataAction,
|
||||
UniversalUpdateSearchFieldMetadataAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/types/workspace-migration-search-field-metadata-action.type';
|
||||
import {
|
||||
WorkspaceMigrationActionRunnerArgs,
|
||||
WorkspaceMigrationActionRunnerContext,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateSearchFieldMetadataActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
|
||||
'update',
|
||||
'searchFieldMetadata',
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override async transpileUniversalActionToFlatAction(
|
||||
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateSearchFieldMetadataAction>,
|
||||
): Promise<FlatUpdateSearchFieldMetadataAction> {
|
||||
const { action, allFlatEntityMaps } = context;
|
||||
|
||||
const flatSearchFieldMetadata = findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: allFlatEntityMaps.flatSearchFieldMetadataMaps,
|
||||
universalIdentifier: action.universalIdentifier,
|
||||
});
|
||||
|
||||
const update = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'searchFieldMetadata',
|
||||
universalUpdate: action.update,
|
||||
allFlatEntityMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'update',
|
||||
metadataName: 'searchFieldMetadata',
|
||||
entityId: flatSearchFieldMetadata.id,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatUpdateSearchFieldMetadataAction>,
|
||||
): Promise<void> {
|
||||
const { flatAction, queryRunner, workspaceId } = context;
|
||||
const { entityId, update } = flatAction;
|
||||
|
||||
const searchFieldMetadataRepository =
|
||||
queryRunner.manager.getRepository<SearchFieldMetadataEntity>(
|
||||
SearchFieldMetadataEntity,
|
||||
);
|
||||
|
||||
await searchFieldMetadataRepository.update(
|
||||
{ id: entityId, workspaceId },
|
||||
update,
|
||||
);
|
||||
}
|
||||
|
||||
async executeForWorkspaceSchema(
|
||||
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateSearchFieldMetadataAction>,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+7
@@ -94,6 +94,9 @@ import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/
|
||||
import { CreateConnectionProviderActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/connection-provider/services/create-connection-provider-action-handler.service';
|
||||
import { DeleteConnectionProviderActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/connection-provider/services/delete-connection-provider-action-handler.service';
|
||||
import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/connection-provider/services/update-connection-provider-action-handler.service';
|
||||
import { CreateSearchFieldMetadataActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/search-field-metadata/services/create-search-field-metadata-action-handler.service';
|
||||
import { DeleteSearchFieldMetadataActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/search-field-metadata/services/delete-search-field-metadata-action-handler.service';
|
||||
import { UpdateSearchFieldMetadataActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/search-field-metadata/services/update-search-field-metadata-action-handler.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -221,6 +224,10 @@ import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspa
|
||||
CreateConnectionProviderActionHandlerService,
|
||||
UpdateConnectionProviderActionHandlerService,
|
||||
DeleteConnectionProviderActionHandlerService,
|
||||
|
||||
CreateSearchFieldMetadataActionHandlerService,
|
||||
UpdateSearchFieldMetadataActionHandlerService,
|
||||
DeleteSearchFieldMetadataActionHandlerService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
|
||||
|
||||
+1
@@ -31,4 +31,5 @@ export const METADATA_EVENTS_TO_EMIT = {
|
||||
webhook: false,
|
||||
applicationVariable: false,
|
||||
connectionProvider: true,
|
||||
searchFieldMetadata: true,
|
||||
} as const satisfies { [P in AllMetadataName]: boolean };
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ const deriveAllMetadataEventsFromCreateAction = (
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
return [
|
||||
{
|
||||
type: 'created',
|
||||
|
||||
+2
-1
@@ -62,7 +62,8 @@ const deriveAllMetadataEventsFromDeleteAction = ({
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
|
||||
MetadataFlatEntity<typeof flatAction.metadataName>
|
||||
>({
|
||||
|
||||
+2
-1
@@ -140,7 +140,8 @@ const deriveAllMetadataEventsFromUpdateAction = ({
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
flatAction.metadataName,
|
||||
);
|
||||
|
||||
+2
-1
@@ -74,7 +74,8 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
|
||||
case 'fieldPermission':
|
||||
case 'webhook':
|
||||
case 'applicationVariable':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatAction.flatEntity,
|
||||
flatEntityAndRelatedMapsToMutate: allFlatEntityMaps,
|
||||
|
||||
+2
-1
@@ -46,7 +46,8 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
|
||||
case 'fieldPermission':
|
||||
case 'webhook':
|
||||
case 'applicationVariable':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
|
||||
MetadataFlatEntity<typeof flatAction.metadataName>
|
||||
>({
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
|
||||
case 'fieldPermission':
|
||||
case 'webhook':
|
||||
case 'applicationVariable':
|
||||
case 'connectionProvider': {
|
||||
case 'connectionProvider':
|
||||
case 'searchFieldMetadata': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
flatAction.metadataName,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user