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:
@@ -2991,6 +2991,7 @@ enum AllMetadataName {
|
||||
webhook
|
||||
applicationVariable
|
||||
connectionProvider
|
||||
searchFieldMetadata
|
||||
}
|
||||
|
||||
type MinimalObjectMetadata {
|
||||
|
||||
@@ -2612,7 +2612,7 @@ export interface CollectionHash {
|
||||
__typename: 'CollectionHash'
|
||||
}
|
||||
|
||||
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'rolePermissionFlag' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider'
|
||||
export type AllMetadataName = 'fieldMetadata' | 'objectMetadata' | 'view' | 'viewField' | 'viewFieldGroup' | 'viewGroup' | 'viewSort' | 'rowLevelPermissionPredicate' | 'rowLevelPermissionPredicateGroup' | 'viewFilterGroup' | 'index' | 'logicFunction' | 'viewFilter' | 'role' | 'roleTarget' | 'agent' | 'skill' | 'pageLayout' | 'pageLayoutWidget' | 'pageLayoutTab' | 'commandMenuItem' | 'navigationMenuItem' | 'rolePermissionFlag' | 'permissionFlag' | 'objectPermission' | 'fieldPermission' | 'frontComponent' | 'webhook' | 'applicationVariable' | 'connectionProvider' | 'searchFieldMetadata'
|
||||
|
||||
export interface MinimalObjectMetadata {
|
||||
id: Scalars['UUID']
|
||||
@@ -9259,7 +9259,8 @@ export const enumAllMetadataName = {
|
||||
frontComponent: 'frontComponent' as const,
|
||||
webhook: 'webhook' as const,
|
||||
applicationVariable: 'applicationVariable' as const,
|
||||
connectionProvider: 'connectionProvider' as const
|
||||
connectionProvider: 'connectionProvider' as const,
|
||||
searchFieldMetadata: 'searchFieldMetadata' as const
|
||||
}
|
||||
|
||||
export const enumEventLogTable = {
|
||||
|
||||
@@ -208,6 +208,7 @@ export enum AllMetadataName {
|
||||
roleTarget = 'roleTarget',
|
||||
rowLevelPermissionPredicate = 'rowLevelPermissionPredicate',
|
||||
rowLevelPermissionPredicateGroup = 'rowLevelPermissionPredicateGroup',
|
||||
searchFieldMetadata = 'searchFieldMetadata',
|
||||
skill = 'skill',
|
||||
view = 'view',
|
||||
viewField = 'viewField',
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ export const useMetadataErrorHandler = () => {
|
||||
viewSort: t`view sort`,
|
||||
applicationVariable: t`application variable`,
|
||||
connectionProvider: t`connection provider`,
|
||||
searchFieldMetadata: t`search field metadata`,
|
||||
} as const satisfies Record<AllMetadataName, string>;
|
||||
|
||||
const handleMetadataError = (
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.16.0', 1782200000000)
|
||||
export class AddUniversalIdentifierAndApplicationIdToSearchFieldMetadataFastInstanceCommand implements FastInstanceCommand {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" ADD "universalIdentifier" uuid NOT NULL');
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" ADD "applicationId" uuid NOT NULL');
|
||||
// The column is NOT NULL but added without a default: searchFieldMetadata is dormant/empty
|
||||
// at instance-command time and the 2-16 backfill workspace command populates positions.
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" ADD "position" double precision NOT NULL');
|
||||
await queryRunner.query('CREATE UNIQUE INDEX "IDX_c2e441c901b45221a70d325349" ON "core"."searchFieldMetadata" ("workspaceId", "universalIdentifier") ');
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" ADD CONSTRAINT "FK_927b6101a5d9562a558a18ed412" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE ON UPDATE NO ACTION');
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" DROP CONSTRAINT "FK_927b6101a5d9562a558a18ed412"');
|
||||
await queryRunner.query('DROP INDEX "core"."IDX_c2e441c901b45221a70d325349"');
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" DROP COLUMN "position"');
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" DROP COLUMN "applicationId"');
|
||||
await queryRunner.query('ALTER TABLE "core"."searchFieldMetadata" DROP COLUMN "universalIdentifier"');
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillSearchFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-workspace-command-1799100000000-backfill-search-field-metadata.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMigrationModule,
|
||||
],
|
||||
providers: [BackfillSearchFieldMetadataCommand],
|
||||
})
|
||||
export class V2_16_UpgradeVersionCommandModule {}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner';
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||
import { buildSearchFieldMetadataBackfillOperations } from 'src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@RegisteredWorkspaceCommand('2.16.0', 1799100000000)
|
||||
@Command({
|
||||
name: 'upgrade:2-16:backfill-search-field-metadata',
|
||||
description:
|
||||
'Backfill searchFieldMetadata rows for each searchable object. Standard objects mirror their SEARCH_FIELDS_FOR_* set; custom objects get their label-identifier field. Idempotent: existing rows are skipped.',
|
||||
})
|
||||
export class BackfillSearchFieldMetadataCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatSearchFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
// The standard-application sync does not run during upgrades, so standard objects'
|
||||
// rows are backfilled from the same definition provisioning uses
|
||||
// (SEARCH_FIELDS_FOR_*), not by parsing the searchVector asExpression.
|
||||
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||
now: new Date().toISOString(),
|
||||
workspaceId,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
});
|
||||
|
||||
const {
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
} = buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
standardAllFlatEntityMaps.flatSearchFieldMetadataMaps,
|
||||
customApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
const applicationUniversalIdentifiers = Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
);
|
||||
|
||||
const totalRowsToCreate = applicationUniversalIdentifiers.reduce(
|
||||
(total, applicationUniversalIdentifier) =>
|
||||
total +
|
||||
(flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
applicationUniversalIdentifier
|
||||
]?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
if (totalRowsToCreate === 0) {
|
||||
this.logger.log(
|
||||
`No missing searchFieldMetadata rows for workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Found ${totalRowsToCreate} missing searchFieldMetadata row(s) for workspace ${workspaceId} across ${applicationUniversalIdentifiers.length} application(s)`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
// One migration per application: the runner assigns applicationId from the single
|
||||
// application passed here, keeping custom-object rows tied to the custom application.
|
||||
for (const applicationUniversalIdentifier of applicationUniversalIdentifiers) {
|
||||
const flatSearchFieldMetadataToCreate =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
applicationUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(flatSearchFieldMetadataToCreate) ||
|
||||
flatSearchFieldMetadataToCreate.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: flatSearchFieldMetadataToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to persist searchFieldMetadata rows for application ${applicationUniversalIdentifier}:\n${JSON.stringify(
|
||||
validateAndBuildResult,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to persist searchFieldMetadata rows for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully backfilled ${totalRowsToCreate} searchFieldMetadata row(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+646
@@ -0,0 +1,646 @@
|
||||
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { buildSearchFieldMetadataBackfillOperations } from 'src/database/commands/upgrade-version-command/2-16/utils/build-search-field-metadata-backfill-operations.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
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 {
|
||||
getFlatObjectMetadataMock,
|
||||
getStandardFlatObjectMetadataMock,
|
||||
} from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
|
||||
const CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER = 'custom-application-uid';
|
||||
const CUSTOM_APPLICATION_ID = 'custom-application-id';
|
||||
|
||||
const buildUniversalIdentifiersByApplicationId = (
|
||||
flatObjectMetadatas: FlatObjectMetadata[],
|
||||
): Record<string, string[]> =>
|
||||
flatObjectMetadatas.reduce<Record<string, string[]>>(
|
||||
(universalIdentifiersByApplicationId, flatObjectMetadata) => {
|
||||
const applicationUniversalIdentifiers =
|
||||
universalIdentifiersByApplicationId[flatObjectMetadata.applicationId] ??
|
||||
[];
|
||||
|
||||
applicationUniversalIdentifiers.push(
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
);
|
||||
|
||||
universalIdentifiersByApplicationId[flatObjectMetadata.applicationId] =
|
||||
applicationUniversalIdentifiers;
|
||||
|
||||
return universalIdentifiersByApplicationId;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const buildFlatObjectMetadataMaps = (
|
||||
flatObjectMetadatas: FlatObjectMetadata[],
|
||||
): FlatEntityMaps<FlatObjectMetadata> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatObjectMetadatas.map((flatObjectMetadata) => [
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatObjectMetadatas.map((flatObjectMetadata) => [
|
||||
flatObjectMetadata.id,
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId:
|
||||
buildUniversalIdentifiersByApplicationId(flatObjectMetadatas),
|
||||
});
|
||||
|
||||
const buildFlatFieldMetadataMaps = (
|
||||
flatFieldMetadatas: FlatFieldMetadata[],
|
||||
): FlatEntityMaps<FlatFieldMetadata> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatFieldMetadatas.map((flatFieldMetadata) => [
|
||||
flatFieldMetadata.universalIdentifier,
|
||||
flatFieldMetadata,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatFieldMetadatas.map((flatFieldMetadata) => [
|
||||
flatFieldMetadata.id,
|
||||
flatFieldMetadata.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const buildFlatSearchFieldMetadataMaps = (
|
||||
flatSearchFieldMetadatas: FlatSearchFieldMetadata[],
|
||||
): FlatEntityMaps<FlatSearchFieldMetadata> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatSearchFieldMetadatas.map((flatSearchFieldMetadata) => [
|
||||
flatSearchFieldMetadata.universalIdentifier,
|
||||
flatSearchFieldMetadata,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatSearchFieldMetadatas.map((flatSearchFieldMetadata) => [
|
||||
flatSearchFieldMetadata.id,
|
||||
flatSearchFieldMetadata.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const buildSearchFieldMetadata = ({
|
||||
id,
|
||||
universalIdentifier,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
position = 0,
|
||||
}: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
position?: number;
|
||||
}): FlatSearchFieldMetadata => {
|
||||
const createdAt = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
return {
|
||||
id,
|
||||
universalIdentifier,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
applicationId: 'unused-application-id',
|
||||
applicationUniversalIdentifier,
|
||||
position,
|
||||
workspaceId: 'workspace-id',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
// Custom object with a searchable `name` field plus a TEXT field whose name
|
||||
// overlaps it by prefix (name / nameDescription). Only the `name` field should
|
||||
// produce a row; the exact-name match makes the prefix overlap irrelevant. This
|
||||
// mirrors pre-2.15 provisioning, which indexes the custom object's `name` field only.
|
||||
const buildCustomObjectFixture = () => {
|
||||
const nameFieldId = 'name-field-id';
|
||||
const nameDescriptionFieldId = 'name-description-field-id';
|
||||
const customObjectUniversalIdentifier = 'custom-object-uid';
|
||||
const customObjectId = 'custom-object-id';
|
||||
|
||||
const nameField = getFlatFieldMetadataMock({
|
||||
id: nameFieldId,
|
||||
universalIdentifier: 'name-field-uid',
|
||||
objectMetadataId: customObjectId,
|
||||
objectMetadataUniversalIdentifier: customObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const nameDescriptionField = getFlatFieldMetadataMock({
|
||||
id: nameDescriptionFieldId,
|
||||
universalIdentifier: 'name-description-field-uid',
|
||||
objectMetadataId: customObjectId,
|
||||
objectMetadataUniversalIdentifier: customObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'nameDescription',
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const customObject = getFlatObjectMetadataMock({
|
||||
id: customObjectId,
|
||||
universalIdentifier: customObjectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
applicationId: CUSTOM_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataId: nameFieldId,
|
||||
fieldUniversalIdentifiers: [
|
||||
nameField.universalIdentifier,
|
||||
nameDescriptionField.universalIdentifier,
|
||||
],
|
||||
});
|
||||
|
||||
return { customObject, nameField, nameDescriptionField };
|
||||
};
|
||||
|
||||
describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
it('selects only the name field for a custom object, ignoring a prefix-overlapping field name', () => {
|
||||
const { customObject, nameField, nameDescriptionField } =
|
||||
buildCustomObjectFixture();
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([customObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameField,
|
||||
nameDescriptionField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
const customApplicationRows =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
expect(customApplicationRows).toHaveLength(1);
|
||||
expect(customApplicationRows?.[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
nameField.universalIdentifier,
|
||||
);
|
||||
expect(customApplicationRows?.[0].objectMetadataUniversalIdentifier).toBe(
|
||||
customObject.universalIdentifier,
|
||||
);
|
||||
// Custom object name field is seeded at position 0.
|
||||
expect(customApplicationRows?.[0].position).toBe(0);
|
||||
// No spurious row for the prefix-overlapping `nameDescription` field.
|
||||
expect(
|
||||
customApplicationRows?.some(
|
||||
(row) =>
|
||||
row.fieldMetadataUniversalIdentifier ===
|
||||
nameDescriptionField.universalIdentifier,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('creates no row for a searchable custom object that has no name field (junction object whose label identifier is a UUID id field)', () => {
|
||||
const junctionObjectId = 'junction-object-id';
|
||||
const junctionObjectUniversalIdentifier = 'junction-object-uid';
|
||||
const idFieldId = 'junction-id-field-id';
|
||||
|
||||
// No name field: the label identifier falls back to the UUID `id` field.
|
||||
// UUID is a searchable type, so a label-identifier-based derivation would
|
||||
// wrongly emit a row — but pre-2.15 indexes nothing here, so backfill must
|
||||
// skip it. Regression guard for the junction-object over-creation bug.
|
||||
const idField = getFlatFieldMetadataMock({
|
||||
id: idFieldId,
|
||||
universalIdentifier: 'junction-id-field-uid',
|
||||
objectMetadataId: junctionObjectId,
|
||||
objectMetadataUniversalIdentifier: junctionObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const junctionObject = getFlatObjectMetadataMock({
|
||||
id: junctionObjectId,
|
||||
universalIdentifier: junctionObjectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
applicationId: CUSTOM_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataId: idFieldId,
|
||||
fieldUniversalIdentifiers: [idField.universalIdentifier],
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([junctionObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([idField]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('groups rows per application: standard-object rows under the standard application, custom-object rows under the custom application', () => {
|
||||
const { customObject, nameField, nameDescriptionField } =
|
||||
buildCustomObjectFixture();
|
||||
|
||||
const standardObjectId = 'standard-object-id';
|
||||
const standardObjectUniversalIdentifier = 'standard-object-uid';
|
||||
const standardFieldId = 'standard-field-id';
|
||||
const standardFieldUniversalIdentifier = 'standard-field-uid';
|
||||
|
||||
const standardField = getFlatFieldMetadataMock({
|
||||
id: standardFieldId,
|
||||
universalIdentifier: standardFieldUniversalIdentifier,
|
||||
objectMetadataId: standardObjectId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const standardObject = getStandardFlatObjectMetadataMock({
|
||||
id: standardObjectId,
|
||||
universalIdentifier: standardObjectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataId: standardFieldId,
|
||||
fieldIds: [standardFieldId],
|
||||
});
|
||||
|
||||
const standardSearchFieldMetadata = buildSearchFieldMetadata({
|
||||
id: 'standard-search-field-id',
|
||||
universalIdentifier: 'standard-search-field-uid',
|
||||
objectMetadataId: standardObjectId,
|
||||
fieldMetadataId: standardFieldId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: standardFieldUniversalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
customObject,
|
||||
standardObject,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameField,
|
||||
nameDescriptionField,
|
||||
standardField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
standardSearchFieldMetadata,
|
||||
]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
).sort(),
|
||||
).toEqual(
|
||||
[
|
||||
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
].sort(),
|
||||
);
|
||||
|
||||
const standardApplicationRows =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
expect(standardApplicationRows).toHaveLength(1);
|
||||
expect(standardApplicationRows?.[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
standardFieldUniversalIdentifier,
|
||||
);
|
||||
|
||||
const customApplicationRows =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
expect(customApplicationRows).toHaveLength(1);
|
||||
expect(customApplicationRows?.[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
nameField.universalIdentifier,
|
||||
);
|
||||
});
|
||||
|
||||
it('selects only the standard-set field on a standard object, never a prefix-overlapping sibling (phone vs phoneNumber)', () => {
|
||||
const standardObjectId = 'standard-object-id';
|
||||
const standardObjectUniversalIdentifier = 'standard-object-uid';
|
||||
|
||||
// Two fields whose names overlap by prefix. Only `phone` is part of the
|
||||
// standard search set; `phoneNumber` must never be pulled in. The old
|
||||
// asExpression-token heuristic could mis-bind these by name; the id-based
|
||||
// derivation cannot.
|
||||
const phoneFieldId = 'phone-field-id';
|
||||
const phoneFieldUniversalIdentifier = 'phone-field-uid';
|
||||
const phoneNumberFieldId = 'phone-number-field-id';
|
||||
const phoneNumberFieldUniversalIdentifier = 'phone-number-field-uid';
|
||||
|
||||
const phoneField = getFlatFieldMetadataMock({
|
||||
id: phoneFieldId,
|
||||
universalIdentifier: phoneFieldUniversalIdentifier,
|
||||
objectMetadataId: standardObjectId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'phone',
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const phoneNumberField = getFlatFieldMetadataMock({
|
||||
id: phoneNumberFieldId,
|
||||
universalIdentifier: phoneNumberFieldUniversalIdentifier,
|
||||
objectMetadataId: standardObjectId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'phoneNumber',
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const standardObject = getStandardFlatObjectMetadataMock({
|
||||
id: standardObjectId,
|
||||
universalIdentifier: standardObjectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataId: phoneFieldId,
|
||||
fieldIds: [phoneFieldId, phoneNumberFieldId],
|
||||
});
|
||||
|
||||
const phoneSearchFieldMetadata = buildSearchFieldMetadata({
|
||||
id: 'phone-search-field-id',
|
||||
universalIdentifier: 'phone-search-field-uid',
|
||||
objectMetadataId: standardObjectId,
|
||||
fieldMetadataId: phoneFieldId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: phoneFieldUniversalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([standardObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
phoneField,
|
||||
phoneNumberField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
phoneSearchFieldMetadata,
|
||||
]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
const standardApplicationRows =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
expect(standardApplicationRows).toHaveLength(1);
|
||||
expect(standardApplicationRows?.[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
phoneFieldUniversalIdentifier,
|
||||
);
|
||||
// The prefix-overlapping `phoneNumber` field is never selected.
|
||||
expect(
|
||||
standardApplicationRows?.some(
|
||||
(row) =>
|
||||
row.fieldMetadataUniversalIdentifier ===
|
||||
phoneNumberFieldUniversalIdentifier,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('carries the position from the standard maps row onto the backfilled standard row', () => {
|
||||
const standardObjectId = 'standard-object-id';
|
||||
const standardObjectUniversalIdentifier = 'standard-object-uid';
|
||||
const emailsFieldId = 'emails-field-id';
|
||||
const emailsFieldUniversalIdentifier = 'emails-field-uid';
|
||||
|
||||
const emailsField = getFlatFieldMetadataMock({
|
||||
id: emailsFieldId,
|
||||
universalIdentifier: emailsFieldUniversalIdentifier,
|
||||
objectMetadataId: standardObjectId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'emails',
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const standardObject = getStandardFlatObjectMetadataMock({
|
||||
id: standardObjectId,
|
||||
universalIdentifier: standardObjectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataId: emailsFieldId,
|
||||
fieldIds: [emailsFieldId],
|
||||
});
|
||||
|
||||
// The standard maps row sits at position 3 (e.g. SEARCH_FIELDS_FOR_PERSON order);
|
||||
// backfill must replicate that ordinal, not reset it to 0.
|
||||
const standardSearchFieldMetadata = buildSearchFieldMetadata({
|
||||
id: 'emails-search-field-id',
|
||||
universalIdentifier: 'emails-search-field-uid',
|
||||
objectMetadataId: standardObjectId,
|
||||
fieldMetadataId: emailsFieldId,
|
||||
objectMetadataUniversalIdentifier: standardObjectUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: emailsFieldUniversalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([standardObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([emailsField]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
standardSearchFieldMetadata,
|
||||
]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
const standardApplicationRows =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER
|
||||
];
|
||||
|
||||
expect(standardApplicationRows).toHaveLength(1);
|
||||
expect(standardApplicationRows?.[0].position).toBe(3);
|
||||
});
|
||||
|
||||
it('is a no-op when the searchFieldMetadata rows already exist (idempotent)', () => {
|
||||
const { customObject, nameField, nameDescriptionField } =
|
||||
buildCustomObjectFixture();
|
||||
|
||||
const existingSearchFieldMetadata = buildSearchFieldMetadata({
|
||||
id: 'existing-search-field-id',
|
||||
universalIdentifier: 'existing-search-field-uid',
|
||||
objectMetadataId: customObject.id,
|
||||
fieldMetadataId: nameField.id,
|
||||
objectMetadataUniversalIdentifier: customObject.universalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: nameField.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([customObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameField,
|
||||
nameDescriptionField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
existingSearchFieldMetadata,
|
||||
]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('skips a custom object whose name field is not a searchable type', () => {
|
||||
const customObjectId = 'relation-name-object-id';
|
||||
const customObjectUniversalIdentifier = 'relation-name-object-uid';
|
||||
const relationNameFieldId = 'relation-name-field-id';
|
||||
|
||||
// A `name` field that is a RELATION (non-searchable) type: pre-2.15 would not
|
||||
// include it in the tsvector and the recompute filters it out, so no row.
|
||||
const relationNameField = getFlatFieldMetadataMock({
|
||||
id: relationNameFieldId,
|
||||
universalIdentifier: 'relation-name-field-uid',
|
||||
objectMetadataId: customObjectId,
|
||||
objectMetadataUniversalIdentifier: customObjectUniversalIdentifier,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'name',
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const customObject = getFlatObjectMetadataMock({
|
||||
id: customObjectId,
|
||||
universalIdentifier: customObjectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
applicationId: CUSTOM_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataId: relationNameFieldId,
|
||||
fieldUniversalIdentifiers: [relationNameField.universalIdentifier],
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([customObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([relationNameField]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('skips a searchable object owned by a third-party application (only the workspace custom application is backfilled)', () => {
|
||||
const thirdPartyApplicationId = 'third-party-application-id';
|
||||
const thirdPartyApplicationUniversalIdentifier =
|
||||
'third-party-application-uid';
|
||||
const { customObject, nameField, nameDescriptionField } =
|
||||
buildCustomObjectFixture();
|
||||
|
||||
// Same fixture but owned by a different (third-party) application: each application
|
||||
// owns its own searchFieldMetadata, so this backfill must not fabricate a row for it.
|
||||
const thirdPartyObject: FlatObjectMetadata = {
|
||||
...customObject,
|
||||
applicationId: thirdPartyApplicationId,
|
||||
applicationUniversalIdentifier: thirdPartyApplicationUniversalIdentifier,
|
||||
};
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([thirdPartyObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameField,
|
||||
nameDescriptionField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('skips non-searchable objects', () => {
|
||||
const { customObject, nameField, nameDescriptionField } =
|
||||
buildCustomObjectFixture();
|
||||
|
||||
const nonSearchableObject: FlatObjectMetadata = {
|
||||
...customObject,
|
||||
isSearchable: false,
|
||||
};
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
nonSearchableObject,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameField,
|
||||
nameDescriptionField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
fromArrayToValuesByKeyRecord,
|
||||
isDefined,
|
||||
isSearchableFieldType,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.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 { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { buildFlatSearchFieldMetadataForField } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/build-flat-search-field-metadata-for-field.util';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/constants/object-metadata.constants';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
type SearchFieldMetadataBackfillOperationsArgs = {
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatSearchFieldMetadataMaps: FlatEntityMaps<FlatSearchFieldMetadata>;
|
||||
standardFlatSearchFieldMetadataMaps: FlatEntityMaps<FlatSearchFieldMetadata>;
|
||||
customApplicationId: string;
|
||||
};
|
||||
|
||||
type BuildSearchFieldMetadataBackfillOperationsReturnType = {
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier: Record<
|
||||
string,
|
||||
UniversalFlatSearchFieldMetadata[]
|
||||
>;
|
||||
};
|
||||
|
||||
// Groups rows by application so each group runs through the migration runner under
|
||||
// the matching application (the runner assigns applicationId from that single app).
|
||||
export const buildSearchFieldMetadataBackfillOperations = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
standardFlatSearchFieldMetadataMaps,
|
||||
customApplicationId,
|
||||
}: SearchFieldMetadataBackfillOperationsArgs): BuildSearchFieldMetadataBackfillOperationsReturnType => {
|
||||
const existingSearchFieldMetadataKeys = new Set(
|
||||
Object.values(flatSearchFieldMetadataMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.map(
|
||||
(searchFieldMetadata) =>
|
||||
`${searchFieldMetadata.objectMetadataId}:${searchFieldMetadata.fieldMetadataId}`,
|
||||
),
|
||||
);
|
||||
|
||||
const flatSearchFieldMetadatasToCreate: UniversalFlatSearchFieldMetadata[] =
|
||||
[];
|
||||
// Dedupe (object, field) within a run: standard and custom derivations can overlap.
|
||||
const candidateSearchFieldMetadataKeys = new Set<string>();
|
||||
|
||||
const pushCandidateIfMissing = ({
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
position,
|
||||
}: {
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
position: number;
|
||||
}): void => {
|
||||
const flatObjectMetadata =
|
||||
flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
objectMetadataUniversalIdentifier
|
||||
];
|
||||
const flatFieldMetadata =
|
||||
flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
fieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
// Skip rows whose object/field isn't provisioned in this workspace yet (keeps
|
||||
// the backfill idempotent and lets each sync own its own rows).
|
||||
if (!isDefined(flatObjectMetadata) || !isDefined(flatFieldMetadata)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchFieldMetadataKey = `${flatObjectMetadata.id}:${flatFieldMetadata.id}`;
|
||||
|
||||
if (
|
||||
existingSearchFieldMetadataKeys.has(searchFieldMetadataKey) ||
|
||||
candidateSearchFieldMetadataKeys.has(searchFieldMetadataKey)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
candidateSearchFieldMetadataKeys.add(searchFieldMetadataKey);
|
||||
|
||||
flatSearchFieldMetadatasToCreate.push(
|
||||
buildFlatSearchFieldMetadataForField({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadata,
|
||||
position,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// Standard objects: mirror exactly what provisioning/standard-sync creates.
|
||||
for (const standardSearchFieldMetadata of Object.values(
|
||||
standardFlatSearchFieldMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
pushCandidateIfMissing({
|
||||
objectMetadataUniversalIdentifier:
|
||||
standardSearchFieldMetadata.objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
standardSearchFieldMetadata.fieldMetadataUniversalIdentifier,
|
||||
position: standardSearchFieldMetadata.position,
|
||||
});
|
||||
}
|
||||
|
||||
const customApplicationFlatObjectMetadatas = findFlatEntitiesByApplicationId({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
applicationId: customApplicationId,
|
||||
});
|
||||
|
||||
// Custom objects index only the field named 'name' (SEARCH_FIELDS_FOR_CUSTOM_OBJECT).
|
||||
// Resolve it by exact name, not the label identifier: junction objects (skipNameField)
|
||||
// have no name field and must stay unsearchable — their label identifier is the UUID id.
|
||||
for (const flatObjectMetadata of customApplicationFlatObjectMetadatas) {
|
||||
if (!flatObjectMetadata.isSearchable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nameFieldMetadata = flatObjectMetadata.fieldUniversalIdentifiers
|
||||
.map(
|
||||
(fieldUniversalIdentifier) =>
|
||||
flatFieldMetadataMaps.byUniversalIdentifier[fieldUniversalIdentifier],
|
||||
)
|
||||
.find(
|
||||
(flatFieldMetadata) =>
|
||||
isDefined(flatFieldMetadata) &&
|
||||
flatFieldMetadata.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME,
|
||||
);
|
||||
|
||||
// Aligns with the recompute, which drops non-searchable-type fields.
|
||||
if (
|
||||
!isDefined(nameFieldMetadata) ||
|
||||
!isSearchableFieldType(nameFieldMetadata.type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pushCandidateIfMissing({
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: nameFieldMetadata.universalIdentifier,
|
||||
position: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
fromArrayToValuesByKeyRecord({
|
||||
array: flatSearchFieldMetadatasToCreate,
|
||||
key: 'applicationUniversalIdentifier',
|
||||
});
|
||||
|
||||
return {
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
+4
-2
@@ -29,8 +29,11 @@ import { CreateUnsubscribeTopicCoreTableFastInstanceCommand } from 'src/database
|
||||
import { RenameIsUiReadOnlyToIsUiEditableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-13/2-13-instance-command-fast-1781277453604-rename-is-ui-read-only-to-is-ui-editable';
|
||||
import { BackfillNonUiCreatableStandardSystemObjectsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-13/2-13-instance-command-slow-1781277480000-backfill-non-ui-creatable-standard-system-objects';
|
||||
import { SetTableWidgetViewsVisibilityToWorkspaceSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-14/2-14-instance-command-slow-1781515653781-set-table-widget-views-visibility-to-workspace';
|
||||
import { AddHasPaymentMethodToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781280240009-add-has-payment-method-to-billing-customer';
|
||||
import { AddIsSystemSideEffectFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781600000000-add-is-system-side-effect';
|
||||
import { BackfillConnectionSecuritySlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-slow-1781461753981-backfill-connection-security';
|
||||
import { AddChannelWebhookSubscriptionFieldsFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782152096938-add-channel-webhook-subscription-fields';
|
||||
import { AddUniversalIdentifierAndApplicationIdToSearchFieldMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782200000000-add-universal-identifier-and-application-id-to-search-field-metadata';
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
import { AddSubFieldNameToViewSortEarlyFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort';
|
||||
@@ -72,10 +75,8 @@ import { EmailingDomainTenantStatusAndGlobalUniquenessFastInstanceCommand } from
|
||||
import { AddLogicFunctionExecutionModeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-fast-1799000030000-add-logic-function-execution-mode';
|
||||
import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1798400000000-encrypt-non-secret-application-variable';
|
||||
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
|
||||
import { AddHasPaymentMethodToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781280240009-add-has-payment-method-to-billing-customer';
|
||||
import { AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action';
|
||||
import { AddViewKanbanColumnWidthFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781900000000-add-view-kanban-column-width';
|
||||
import { AddChannelWebhookSubscriptionFieldsFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-16/2-16-instance-command-fast-1782152096938-add-channel-webhook-subscription-fields';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -154,4 +155,5 @@ export const INSTANCE_COMMANDS = [
|
||||
AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand,
|
||||
AddViewKanbanColumnWidthFastInstanceCommand,
|
||||
AddChannelWebhookSubscriptionFieldsFastInstanceCommand,
|
||||
AddUniversalIdentifierAndApplicationIdToSearchFieldMetadataFastInstanceCommand,
|
||||
];
|
||||
|
||||
+2
@@ -16,6 +16,7 @@ import { V2_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
|
||||
import { V2_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-13/2-13-upgrade-version-command.module';
|
||||
import { V2_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-14/2-14-upgrade-version-command.module';
|
||||
import { V2_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-15/2-15-upgrade-version-command.module';
|
||||
import { V2_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/2-16/2-16-upgrade-version-command.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -35,6 +36,7 @@ import { V2_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade
|
||||
V2_13_UpgradeVersionCommandModule,
|
||||
V2_14_UpgradeVersionCommandModule,
|
||||
V2_15_UpgradeVersionCommandModule,
|
||||
V2_16_UpgradeVersionCommandModule,
|
||||
],
|
||||
})
|
||||
export class WorkspaceCommandProviderModule {}
|
||||
|
||||
+2
@@ -152,6 +152,7 @@ export const mockPersonFlatObjectMetadata = (
|
||||
workspaceId,
|
||||
universalIdentifier: objectMetadataId,
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds: mockFieldMetadatas.map((field) => field.id),
|
||||
@@ -173,6 +174,7 @@ export const mockPersonFlatObjectMetadata = (
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
|
||||
isLabelSyncedWithName: false,
|
||||
fieldUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
objectPermissionUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
|
||||
+2
@@ -25,6 +25,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
|
||||
isAuditLogged: true,
|
||||
isSearchable: true,
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds: [],
|
||||
@@ -47,6 +48,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
};
|
||||
|
||||
+14
-1
@@ -95,6 +95,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -103,6 +104,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
'flatIndexMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatSearchFieldMetadataMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -111,11 +113,14 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatFieldMetadatasToDelete,
|
||||
flatIndexesToDelete,
|
||||
flatIndexesToUpdate,
|
||||
searchFieldMetadatasToDelete,
|
||||
flatSearchVectorFieldsToUpdate,
|
||||
} = fromDeleteFieldInputToFlatFieldMetadatasToDelete({
|
||||
deleteOneFieldInput,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const deletedFlatFieldMetadata = findFlatEntityByUniversalIdentifierOrThrow(
|
||||
@@ -157,13 +162,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: flatFieldMetadatasToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
flatEntityToUpdate: flatSearchVectorFieldsToUpdate,
|
||||
},
|
||||
index: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: flatIndexesToDelete,
|
||||
flatEntityToUpdate: flatIndexesToUpdate,
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: searchFieldMetadatasToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
...(flatPageLayoutWidgetsToDelete.length > 0
|
||||
? {
|
||||
pageLayoutWidget: {
|
||||
@@ -218,6 +228,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -229,6 +240,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
'flatViewGroupMaps',
|
||||
'flatViewMaps',
|
||||
'flatViewFieldMaps',
|
||||
'flatSearchFieldMetadataMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -242,6 +254,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatViewGroupMaps: existingFlatViewGroupMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
isSystemBuild,
|
||||
});
|
||||
|
||||
+6
@@ -303,6 +303,12 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
"searchFieldMetadata": {
|
||||
"propertiesToCompare": [
|
||||
"position",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
"skill": {
|
||||
"propertiesToCompare": [
|
||||
"name",
|
||||
|
||||
+27
@@ -1769,6 +1769,33 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
objectMetadataId: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
fieldMetadataId: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: 'fieldMetadataUniversalIdentifier',
|
||||
},
|
||||
position: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
updatedAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
},
|
||||
} as const satisfies {
|
||||
[P in AllMetadataName]: MetadataEntityPropertyConfiguration<P>;
|
||||
};
|
||||
|
||||
+10
@@ -303,6 +303,16 @@ export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
workspace: null,
|
||||
application: null,
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
objectMetadata: {
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
fieldMetadata: {
|
||||
foreignKey: 'fieldMetadataId',
|
||||
},
|
||||
},
|
||||
} as const satisfies ManyToOneMetadataRelationsProperties;
|
||||
|
||||
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
|
||||
|
||||
+18
@@ -531,6 +531,24 @@ export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
workspace: null,
|
||||
application: null,
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
objectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
foreignKey: 'objectMetadataId',
|
||||
inverseOneToManyProperty: 'searchFieldMetadatas',
|
||||
isNullable: false,
|
||||
universalForeignKey: 'objectMetadataUniversalIdentifier',
|
||||
},
|
||||
fieldMetadata: {
|
||||
metadataName: 'fieldMetadata',
|
||||
foreignKey: 'fieldMetadataId',
|
||||
inverseOneToManyProperty: null,
|
||||
isNullable: false,
|
||||
universalForeignKey: 'fieldMetadataUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
} as const satisfies ManyToOneMetadataRelationsProperties;
|
||||
|
||||
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entiti
|
||||
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
|
||||
export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
viewField: ViewFieldEntity,
|
||||
@@ -63,4 +64,5 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
applicationVariable: ApplicationVariableEntity,
|
||||
viewSort: ViewSortEntity,
|
||||
connectionProvider: ConnectionProviderEntity,
|
||||
searchFieldMetadata: SearchFieldMetadataEntity,
|
||||
} as const satisfies Record<AllMetadataName, EntityTarget<ObjectLiteral>>;
|
||||
|
||||
+4
@@ -118,4 +118,8 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
webhook: {},
|
||||
applicationVariable: {},
|
||||
connectionProvider: {},
|
||||
searchFieldMetadata: {
|
||||
objectMetadata: true,
|
||||
fieldMetadata: true,
|
||||
},
|
||||
} as const satisfies MetadataRequiredForValidation;
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ export const ALL_METADATA_SERIALIZED_RELATION = {
|
||||
webhook: {},
|
||||
applicationVariable: {},
|
||||
connectionProvider: {},
|
||||
searchFieldMetadata: {},
|
||||
} as const satisfies MetadataSerializedRelationProperties;
|
||||
|
||||
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
|
||||
|
||||
+7
@@ -107,6 +107,12 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
flatEntityForeignKeyAggregator: 'viewIds',
|
||||
universalFlatEntityForeignKeyAggregator: 'viewUniversalIdentifiers',
|
||||
},
|
||||
searchFieldMetadatas: {
|
||||
metadataName: 'searchFieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'searchFieldMetadataIds',
|
||||
universalFlatEntityForeignKeyAggregator:
|
||||
'searchFieldMetadataUniversalIdentifiers',
|
||||
},
|
||||
},
|
||||
view: {
|
||||
viewFields: {
|
||||
@@ -254,6 +260,7 @@ export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
applicationVariable: {},
|
||||
viewSort: {},
|
||||
connectionProvider: {},
|
||||
searchFieldMetadata: {},
|
||||
} as const satisfies OneToManyMetadataRelationsProperties;
|
||||
|
||||
// satisfies with complex mapped types involving nested generics doesn't always catch missing required keys
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ import { WorkspaceFlatViewFieldMapCacheService } from 'src/engine/metadata-modul
|
||||
import { WorkspaceFlatViewFilterGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-filter-group/services/workspace-flat-view-filter-group-map-cache.service';
|
||||
import { WorkspaceFlatViewFilterMapCacheService } from 'src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service';
|
||||
import { WorkspaceFlatViewGroupMapCacheService } from 'src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service';
|
||||
import { WorkspaceFlatSearchFieldMetadataMapCacheService } from 'src/engine/metadata-modules/flat-search-field-metadata/services/workspace-flat-search-field-metadata-map-cache.service';
|
||||
import { WorkspaceFlatViewSortMapCacheService } from 'src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service';
|
||||
import { WorkspaceFlatViewMapCacheService } from 'src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
@@ -37,6 +38,7 @@ import { RolePermissionFlagEntity } from 'src/engine/metadata-modules/role-permi
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
@@ -74,6 +76,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
ApplicationEntity,
|
||||
RoleEntity,
|
||||
FrontComponentEntity,
|
||||
SearchFieldMetadataEntity,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -92,12 +95,14 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceFlatRolePermissionFlagMapCacheService,
|
||||
WorkspaceFlatPermissionFlagMapCacheService,
|
||||
WorkspaceFlatViewSortMapCacheService,
|
||||
WorkspaceFlatSearchFieldMetadataMapCacheService,
|
||||
WorkspaceFlatPageLayoutMapCacheService,
|
||||
WorkspaceFlatPageLayoutTabMapCacheService,
|
||||
WorkspaceFlatPageLayoutWidgetMapCacheService,
|
||||
WorkspaceFlatRowLevelPermissionPredicateMapCacheService,
|
||||
WorkspaceFlatRowLevelPermissionPredicateGroupMapCacheService,
|
||||
provideWorkspaceScopedRepository(PermissionFlagEntity),
|
||||
provideWorkspaceScopedRepository(SearchFieldMetadataEntity),
|
||||
provideWorkspaceScopedRepository(IndexMetadataEntity),
|
||||
provideWorkspaceScopedRepository(ViewEntity),
|
||||
provideWorkspaceScopedRepository(ViewFieldEntity),
|
||||
@@ -131,6 +136,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceFlatRolePermissionFlagMapCacheService,
|
||||
WorkspaceFlatPermissionFlagMapCacheService,
|
||||
WorkspaceFlatViewSortMapCacheService,
|
||||
WorkspaceFlatSearchFieldMetadataMapCacheService,
|
||||
WorkspaceFlatPageLayoutMapCacheService,
|
||||
WorkspaceFlatPageLayoutTabMapCacheService,
|
||||
WorkspaceFlatPageLayoutWidgetMapCacheService,
|
||||
|
||||
+27
@@ -45,6 +45,8 @@ import { type FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-gr
|
||||
import { type FlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group.type';
|
||||
import { type FlatViewSortMaps } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort-maps.type';
|
||||
import { type FlatViewSort } from 'src/engine/metadata-modules/flat-view-sort/types/flat-view-sort.type';
|
||||
import { type FlatSearchFieldMetadataMaps } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata-maps.type';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type';
|
||||
import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { type FlatApplicationVariableMaps } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable-maps.type';
|
||||
@@ -84,6 +86,7 @@ import { type UniversalFlatViewFilterGroup } from 'src/engine/workspace-manager/
|
||||
import { type UniversalFlatViewFilter } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-filter.type';
|
||||
import { type UniversalFlatViewGroup } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-group.type';
|
||||
import { type UniversalFlatViewSort } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-sort.type';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
import { type UniversalFlatView } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view.type';
|
||||
import { type UniversalFlatApplicationVariable } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-application-variable.type';
|
||||
import { type UniversalFlatWebhook } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-webhook.type';
|
||||
@@ -327,6 +330,14 @@ import {
|
||||
type UniversalDeleteWebhookAction,
|
||||
type UniversalUpdateWebhookAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/types/workspace-migration-webhook-action.type';
|
||||
import {
|
||||
type FlatCreateSearchFieldMetadataAction,
|
||||
type FlatDeleteSearchFieldMetadataAction,
|
||||
type FlatUpdateSearchFieldMetadataAction,
|
||||
type UniversalCreateSearchFieldMetadataAction,
|
||||
type UniversalDeleteSearchFieldMetadataAction,
|
||||
type UniversalUpdateSearchFieldMetadataAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/search-field-metadata/types/workspace-migration-search-field-metadata-action.type';
|
||||
|
||||
export type AllFlatEntityTypesByMetadataName = {
|
||||
fieldMetadata: {
|
||||
@@ -812,4 +823,20 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
universalFlatEntity: UniversalFlatConnectionProvider;
|
||||
entity: MetadataEntity<'connectionProvider'>;
|
||||
};
|
||||
searchFieldMetadata: {
|
||||
flatEntityMaps: FlatSearchFieldMetadataMaps;
|
||||
universalActions: {
|
||||
create: UniversalCreateSearchFieldMetadataAction;
|
||||
update: UniversalUpdateSearchFieldMetadataAction;
|
||||
delete: UniversalDeleteSearchFieldMetadataAction;
|
||||
};
|
||||
flatActions: {
|
||||
create: FlatCreateSearchFieldMetadataAction;
|
||||
update: FlatUpdateSearchFieldMetadataAction;
|
||||
delete: FlatDeleteSearchFieldMetadataAction;
|
||||
};
|
||||
flatEntity: FlatSearchFieldMetadata;
|
||||
universalFlatEntity: UniversalFlatSearchFieldMetadata;
|
||||
entity: MetadataEntity<'searchFieldMetadata'>;
|
||||
};
|
||||
};
|
||||
|
||||
+8
@@ -60,6 +60,7 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
"objectPermission",
|
||||
"fieldPermission",
|
||||
"view",
|
||||
"searchFieldMetadata",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -139,6 +140,13 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for searchFieldMetadata 1`] = `
|
||||
[
|
||||
"objectMetadata",
|
||||
"fieldMetadata",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for skill 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for view 1`] = `
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
|
||||
"objectPermission",
|
||||
"pageLayoutWidget",
|
||||
"rolePermissionFlag",
|
||||
"searchFieldMetadata",
|
||||
"viewSort",
|
||||
"index",
|
||||
"pageLayout",
|
||||
|
||||
+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],
|
||||
]),
|
||||
}),
|
||||
};
|
||||
};
|
||||
+2
@@ -20,6 +20,7 @@ export const getFlatObjectMetadataMock = (
|
||||
return {
|
||||
viewIds: [],
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds: [],
|
||||
@@ -55,6 +56,7 @@ export const getFlatObjectMetadataMock = (
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelIdentifierFieldMetadataId,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier:
|
||||
|
||||
+14
@@ -14,6 +14,7 @@ import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadat
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
|
||||
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
|
||||
@@ -40,6 +41,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
private readonly viewRepository: WorkspaceScopedRepository<ViewEntity>,
|
||||
@InjectWorkspaceScopedRepository(ObjectPermissionEntity)
|
||||
private readonly objectPermissionRepository: WorkspaceScopedRepository<ObjectPermissionEntity>,
|
||||
@InjectWorkspaceScopedRepository(SearchFieldMetadataEntity)
|
||||
private readonly searchFieldMetadataRepository: WorkspaceScopedRepository<SearchFieldMetadataEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -54,6 +57,7 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
indexMetadatas,
|
||||
views,
|
||||
objectPermissions,
|
||||
searchFieldMetadatas,
|
||||
] = await Promise.all([
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
@@ -81,6 +85,9 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.searchFieldMetadataRepository.find(workspaceId, {
|
||||
select: ['id', 'universalIdentifier', 'objectMetadataId'],
|
||||
}),
|
||||
]);
|
||||
|
||||
const [
|
||||
@@ -88,6 +95,7 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
indexesByObjectId,
|
||||
viewsByObjectId,
|
||||
objectPermissionsByObjectId,
|
||||
searchFieldMetadatasByObjectId,
|
||||
] = (
|
||||
[
|
||||
{
|
||||
@@ -106,6 +114,10 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
entities: objectPermissions,
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
{
|
||||
entities: searchFieldMetadatas,
|
||||
foreignKey: 'objectMetadataId',
|
||||
},
|
||||
] as const
|
||||
).map(regroupEntitiesByRelatedEntityId);
|
||||
|
||||
@@ -125,6 +137,8 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCachePr
|
||||
views: viewsByObjectId.get(objectMetadataEntity.id) || [],
|
||||
objectPermissions:
|
||||
objectPermissionsByObjectId.get(objectMetadataEntity.id) || [],
|
||||
searchFieldMetadatas:
|
||||
searchFieldMetadatasByObjectId.get(objectMetadataEntity.id) || [],
|
||||
},
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
|
||||
+12
@@ -10,9 +10,11 @@ import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metad
|
||||
import { buildDefaultFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util';
|
||||
import { buildDefaultIndexesForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-index-for-custom-object.util';
|
||||
import { buildDefaultRelationFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-relation-flat-field-metadatas-for-custom-object.util';
|
||||
import { buildDefaultSearchFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-search-field-metadatas-for-custom-object.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 UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
type FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs =
|
||||
{
|
||||
@@ -29,6 +31,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
relationTargetFlatFieldMetadataToCreate: UniversalFlatFieldMetadata[];
|
||||
flatFieldMetadataToCreateOnObject: UniversalFlatFieldMetadata[];
|
||||
flatIndexMetadataToCreate: UniversalFlatIndexMetadata[];
|
||||
flatSearchFieldMetadataToCreate: UniversalFlatSearchFieldMetadata[];
|
||||
} => {
|
||||
const createObjectInput =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
@@ -94,6 +97,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
};
|
||||
@@ -119,12 +123,20 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
flatObjectMetadata: universalFlatObjectMetadataToCreate,
|
||||
});
|
||||
|
||||
const defaultSearchFieldMetadatasForCustomObject =
|
||||
buildDefaultSearchFieldMetadatasForCustomObject({
|
||||
defaultFlatFieldForCustomObjectMaps,
|
||||
flatObjectMetadata: universalFlatObjectMetadataToCreate,
|
||||
});
|
||||
|
||||
return {
|
||||
flatObjectMetadataToCreate: universalFlatObjectMetadataToCreate,
|
||||
flatIndexMetadataToCreate: [
|
||||
...Object.values(defaultIndexesForCustomObject.indexes),
|
||||
...standardTargetFlatIndexMetadatas,
|
||||
],
|
||||
flatSearchFieldMetadataToCreate:
|
||||
defaultSearchFieldMetadatasForCustomObject.searchFieldMetadatas,
|
||||
relationTargetFlatFieldMetadataToCreate: standardTargetFlatFieldMetadatas,
|
||||
flatFieldMetadataToCreateOnObject: objectFlatFieldMetadatas,
|
||||
};
|
||||
|
||||
+7
@@ -77,6 +77,9 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
|
||||
updatedAt: objectMetadataEntity.updatedAt.toISOString(),
|
||||
viewIds: objectMetadataEntity.views.map(({ id }) => id),
|
||||
indexMetadataIds: objectMetadataEntity.indexMetadatas.map(({ id }) => id),
|
||||
searchFieldMetadataIds: objectMetadataEntity.searchFieldMetadatas.map(
|
||||
({ id }) => id,
|
||||
),
|
||||
fieldIds: objectMetadataEntity.fields.map(({ id }) => id),
|
||||
objectPermissionIds: objectMetadataEntity.objectPermissions.map(
|
||||
({ id }) => id,
|
||||
@@ -92,6 +95,10 @@ export const fromObjectMetadataEntityToFlatObjectMetadata = ({
|
||||
indexMetadataUniversalIdentifiers: objectMetadataEntity.indexMetadatas.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
searchFieldMetadataUniversalIdentifiers:
|
||||
objectMetadataEntity.searchFieldMetadatas?.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
) ?? [],
|
||||
viewUniversalIdentifiers: objectMetadataEntity.views.map(
|
||||
({ universalIdentifier }) => universalIdentifier,
|
||||
),
|
||||
|
||||
+5
@@ -30,6 +30,7 @@ type FromUpdateObjectInputToFlatObjectMetadataArgs = {
|
||||
| 'flatFieldMetadataMaps'
|
||||
| 'flatViewFieldMaps'
|
||||
| 'flatViewMaps'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
>;
|
||||
|
||||
export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
|
||||
@@ -40,6 +41,7 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
|
||||
flatFieldMetadataMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
}: FromUpdateObjectInputToFlatObjectMetadataArgs): FlatObjectMetadataUpdateSideEffects & {
|
||||
flatObjectMetadataToUpdate: UniversalFlatObjectMetadata;
|
||||
} => {
|
||||
@@ -101,6 +103,7 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
|
||||
flatViewFieldsToUpdate,
|
||||
otherObjectFlatFieldMetadatasToUpdate,
|
||||
sameObjectFlatFieldMetadatasToUpdate,
|
||||
searchFieldMetadatasToCreate,
|
||||
} = handleFlatObjectMetadataUpdateSideEffect({
|
||||
fromFlatObjectMetadata: existingFlatObjectMetadata,
|
||||
toFlatObjectMetadata,
|
||||
@@ -109,6 +112,7 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
|
||||
flatIndexMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -118,5 +122,6 @@ export const fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities =
|
||||
flatViewFieldsToUpdate,
|
||||
otherObjectFlatFieldMetadatasToUpdate,
|
||||
sameObjectFlatFieldMetadatasToUpdate,
|
||||
searchFieldMetadatasToCreate,
|
||||
};
|
||||
};
|
||||
|
||||
+15
-19
@@ -5,11 +5,12 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/
|
||||
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 { 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 { recomputeSearchVectorOnLabelIdentifierUpdate } from 'src/engine/metadata-modules/flat-object-metadata/utils/recompute-search-vector-on-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 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';
|
||||
import { type UniversalFlatViewField } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-view-field.type';
|
||||
|
||||
export type FlatObjectMetadataUpdateSideEffects = {
|
||||
@@ -18,6 +19,7 @@ export type FlatObjectMetadataUpdateSideEffects = {
|
||||
flatViewFieldsToUpdate: UniversalFlatViewField[];
|
||||
flatViewFieldsToCreate: UniversalFlatViewField[];
|
||||
flatIndexMetadatasToUpdate: UniversalFlatIndexMetadata[];
|
||||
searchFieldMetadatasToCreate: UniversalFlatSearchFieldMetadata[];
|
||||
};
|
||||
|
||||
type HandleFlatObjectMetadataUpdateSideEffectArgs = FromTo<
|
||||
@@ -31,6 +33,7 @@ type HandleFlatObjectMetadataUpdateSideEffectArgs = FromTo<
|
||||
| 'flatViewFieldMaps'
|
||||
| 'flatIndexMaps'
|
||||
| 'flatViewMaps'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
>;
|
||||
|
||||
export const handleFlatObjectMetadataUpdateSideEffect = ({
|
||||
@@ -39,6 +42,7 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatViewFieldMaps,
|
||||
flatViewMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
fromFlatObjectMetadata,
|
||||
toFlatObjectMetadata,
|
||||
}: HandleFlatObjectMetadataUpdateSideEffectArgs): FlatObjectMetadataUpdateSideEffects => {
|
||||
@@ -87,25 +91,16 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({
|
||||
|
||||
const sameObjectFlatFieldMetadatasToUpdate: FlatFieldMetadata[] = [];
|
||||
|
||||
if (
|
||||
toFlatObjectMetadata.isSearchable &&
|
||||
isDefined(
|
||||
toFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
) &&
|
||||
fromFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier !==
|
||||
toFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier
|
||||
) {
|
||||
const updatedSearchVectorField =
|
||||
recomputeSearchVectorFieldAfterLabelIdentifierUpdate({
|
||||
existingFlatObjectMetadata: fromFlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
toFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
});
|
||||
const { flatSearchVectorFieldToUpdate, searchFieldMetadatasToCreate } =
|
||||
recomputeSearchVectorOnLabelIdentifierUpdate({
|
||||
fromFlatObjectMetadata,
|
||||
toFlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (isDefined(updatedSearchVectorField)) {
|
||||
sameObjectFlatFieldMetadatasToUpdate.push(updatedSearchVectorField);
|
||||
}
|
||||
if (isDefined(flatSearchVectorFieldToUpdate)) {
|
||||
sameObjectFlatFieldMetadatasToUpdate.push(flatSearchVectorFieldToUpdate);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -117,5 +112,6 @@ export const handleFlatObjectMetadataUpdateSideEffect = ({
|
||||
flatViewFieldsToUpdate,
|
||||
otherObjectFlatFieldMetadatasToUpdate: morphFlatFieldMetadatasToUpdate,
|
||||
sameObjectFlatFieldMetadatasToUpdate,
|
||||
searchFieldMetadatasToCreate,
|
||||
};
|
||||
};
|
||||
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
findOrThrow,
|
||||
isDefined,
|
||||
type SearchableFieldType,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.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/utils/get-ts-vector-column-expression.util';
|
||||
|
||||
type RecomputeSearchVectorFieldAfterLabelIdentifierUpdateArgs = {
|
||||
existingFlatObjectMetadata: FlatObjectMetadata;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string;
|
||||
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
|
||||
|
||||
export const recomputeSearchVectorFieldAfterLabelIdentifierUpdate = ({
|
||||
existingFlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
}: RecomputeSearchVectorFieldAfterLabelIdentifierUpdateArgs):
|
||||
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
|
||||
| undefined => {
|
||||
const objectFlatFieldMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityIds: existingFlatObjectMetadata.fieldIds,
|
||||
});
|
||||
|
||||
const searchVectorField = findOrThrow(
|
||||
objectFlatFieldMetadatas,
|
||||
(field) => field.name === SEARCH_VECTOR_FIELD.name,
|
||||
) as FlatFieldMetadata<FieldMetadataType.TS_VECTOR>;
|
||||
|
||||
const newLabelIdentifierField = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
universalIdentifier: labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
});
|
||||
|
||||
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,
|
||||
universalSettings: {
|
||||
...searchVectorField.universalSettings,
|
||||
asExpression: newAsExpression,
|
||||
generatedType: 'STORED',
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
throw new ObjectMetadataException(
|
||||
`Failed to compute search vector column expression for field ${newLabelIdentifierField.name}`,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
);
|
||||
}
|
||||
};
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { 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 { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import {
|
||||
buildSearchVectorTargetField,
|
||||
computeSearchVectorAsExpressionFromSearchFieldMetadatas,
|
||||
} from 'src/engine/metadata-modules/flat-search-field-metadata/utils/compute-search-vector-as-expression-from-search-field-metadatas.util';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
type RecomputeSearchVectorFieldFromSearchFieldMetadatasArgs = {
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
// Post-change searchFieldMetadata rows for the object, carrying the field id and the
|
||||
// per-object position that drives the deterministic asExpression order.
|
||||
searchFieldMetadatas: Pick<
|
||||
FlatSearchFieldMetadata,
|
||||
'fieldMetadataId' | 'position' | 'universalIdentifier'
|
||||
>[];
|
||||
// Takes precedence over the flat maps when resolving an id — for a field created or
|
||||
// renamed in the same migration, whose maps entry is absent or stale.
|
||||
overrideFlatFieldMetadataById?: Map<string, FlatFieldMetadata>;
|
||||
} & Pick<AllFlatEntityMaps, 'flatFieldMetadataMaps'>;
|
||||
|
||||
export const recomputeSearchVectorFieldFromSearchFieldMetadatas = ({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
searchFieldMetadatas,
|
||||
overrideFlatFieldMetadataById,
|
||||
}: RecomputeSearchVectorFieldFromSearchFieldMetadatasArgs):
|
||||
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
|
||||
| undefined => {
|
||||
const objectFlatFieldMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityIds: flatObjectMetadata.fieldIds,
|
||||
});
|
||||
|
||||
const searchVectorField = objectFlatFieldMetadatas.find(
|
||||
(field) => field.name === SEARCH_VECTOR_FIELD.name,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(searchVectorField) ||
|
||||
!isFlatFieldMetadataOfType(searchVectorField, FieldMetadataType.TS_VECTOR)
|
||||
) {
|
||||
throw new ObjectMetadataException(
|
||||
`Search vector field not found for object metadata ${flatObjectMetadata.id}`,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const targetSearchableFields = searchFieldMetadatas.map(
|
||||
(searchFieldMetadata) => {
|
||||
const { fieldMetadataId, position, universalIdentifier } =
|
||||
searchFieldMetadata;
|
||||
|
||||
const overriddenFlatFieldMetadata =
|
||||
overrideFlatFieldMetadataById?.get(fieldMetadataId);
|
||||
|
||||
const flatFieldMetadata =
|
||||
overriddenFlatFieldMetadata ??
|
||||
findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: fieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
throw new ObjectMetadataException(
|
||||
`Field metadata ${fieldMetadataId} referenced by searchFieldMetadata not found for object metadata ${flatObjectMetadata.id}`,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return buildSearchVectorTargetField(
|
||||
flatFieldMetadata,
|
||||
position,
|
||||
universalIdentifier,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const newAsExpression =
|
||||
computeSearchVectorAsExpressionFromSearchFieldMetadatas(
|
||||
targetSearchableFields,
|
||||
);
|
||||
|
||||
return {
|
||||
...searchVectorField,
|
||||
universalSettings: {
|
||||
...searchVectorField.universalSettings,
|
||||
asExpression: newAsExpression,
|
||||
generatedType: 'STORED',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
throw new ObjectMetadataException(
|
||||
`Failed to compute search vector column expression for object metadata ${flatObjectMetadata.id}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
);
|
||||
}
|
||||
};
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { type FieldMetadataType, type FromTo } from 'twenty-shared/types';
|
||||
import { isDefined, isSearchableFieldType } 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 { 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 { buildFlatSearchFieldMetadataForField } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/build-flat-search-field-metadata-for-field.util';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
export type LabelIdentifierUpdateSearchVectorSideEffect = {
|
||||
flatSearchVectorFieldToUpdate:
|
||||
| FlatFieldMetadata<FieldMetadataType.TS_VECTOR>
|
||||
| undefined;
|
||||
searchFieldMetadatasToCreate: UniversalFlatSearchFieldMetadata[];
|
||||
};
|
||||
|
||||
type RecomputeSearchVectorOnLabelIdentifierUpdateArgs = FromTo<
|
||||
FlatObjectMetadata,
|
||||
'flatObjectMetadata'
|
||||
> &
|
||||
Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatFieldMetadataMaps' | 'flatSearchFieldMetadataMaps'
|
||||
>;
|
||||
|
||||
const EMPTY_SIDE_EFFECT: LabelIdentifierUpdateSearchVectorSideEffect = {
|
||||
flatSearchVectorFieldToUpdate: undefined,
|
||||
searchFieldMetadatasToCreate: [],
|
||||
};
|
||||
|
||||
// Relabeling is additive: it indexes the new label identifier field while preserving the
|
||||
// existing search surface (e.g. the provisioned name row). We never drop a row here so the
|
||||
// previous label identifier stays searchable. Removal of fields from the search surface is
|
||||
// deferred to a follow-up PR.
|
||||
export const recomputeSearchVectorOnLabelIdentifierUpdate = ({
|
||||
fromFlatObjectMetadata,
|
||||
toFlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
}: RecomputeSearchVectorOnLabelIdentifierUpdateArgs): LabelIdentifierUpdateSearchVectorSideEffect => {
|
||||
const newLabelIdentifierFieldMetadataId =
|
||||
toFlatObjectMetadata.labelIdentifierFieldMetadataId;
|
||||
|
||||
if (
|
||||
!toFlatObjectMetadata.isSearchable ||
|
||||
!isDefined(newLabelIdentifierFieldMetadataId) ||
|
||||
fromFlatObjectMetadata.labelIdentifierFieldMetadataId ===
|
||||
newLabelIdentifierFieldMetadataId
|
||||
) {
|
||||
return EMPTY_SIDE_EFFECT;
|
||||
}
|
||||
|
||||
const existingSearchFieldMetadatas =
|
||||
findManyFlatEntityByIdInFlatEntityMapsOrThrow<FlatSearchFieldMetadata>({
|
||||
flatEntityMaps: flatSearchFieldMetadataMaps,
|
||||
flatEntityIds: fromFlatObjectMetadata.searchFieldMetadataIds,
|
||||
});
|
||||
|
||||
const newLabelIdentifierAlreadyIndexed = existingSearchFieldMetadatas.some(
|
||||
(searchFieldMetadata) =>
|
||||
searchFieldMetadata.fieldMetadataId === newLabelIdentifierFieldMetadataId,
|
||||
);
|
||||
|
||||
const newLabelIdentifierField = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: newLabelIdentifierFieldMetadataId,
|
||||
});
|
||||
|
||||
const shouldIndexNewLabelIdentifier =
|
||||
isDefined(newLabelIdentifierField) &&
|
||||
isSearchableFieldType(newLabelIdentifierField.type) &&
|
||||
!newLabelIdentifierAlreadyIndexed;
|
||||
|
||||
if (!shouldIndexNewLabelIdentifier) {
|
||||
return EMPTY_SIDE_EFFECT;
|
||||
}
|
||||
|
||||
const newLabelIdentifierPosition =
|
||||
existingSearchFieldMetadatas.reduce(
|
||||
(maxPosition, searchFieldMetadata) =>
|
||||
Math.max(maxPosition, searchFieldMetadata.position),
|
||||
-1,
|
||||
) + 1;
|
||||
|
||||
const newSearchFieldMetadata = buildFlatSearchFieldMetadataForField({
|
||||
flatObjectMetadata: fromFlatObjectMetadata,
|
||||
flatFieldMetadata: newLabelIdentifierField,
|
||||
position: newLabelIdentifierPosition,
|
||||
});
|
||||
|
||||
const nextSearchFieldMetadatas = [
|
||||
...existingSearchFieldMetadatas,
|
||||
{
|
||||
fieldMetadataId: newLabelIdentifierFieldMetadataId,
|
||||
position: newLabelIdentifierPosition,
|
||||
universalIdentifier: newSearchFieldMetadata.universalIdentifier,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
flatSearchVectorFieldToUpdate:
|
||||
recomputeSearchVectorFieldFromSearchFieldMetadatas({
|
||||
flatObjectMetadata: fromFlatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
searchFieldMetadatas: nextSearchFieldMetadatas,
|
||||
}),
|
||||
searchFieldMetadatasToCreate: [newSearchFieldMetadata],
|
||||
};
|
||||
};
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { FlatSearchFieldMetadataMaps } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata-maps.type';
|
||||
import { fromSearchFieldMetadataEntityToFlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/from-search-field-metadata-entity-to-flat-search-field-metadata.util';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
|
||||
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/utils/create-id-to-universal-identifier-map.util';
|
||||
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('flatSearchFieldMetadataMaps')
|
||||
export class WorkspaceFlatSearchFieldMetadataMapCacheService extends WorkspaceCacheProvider<FlatSearchFieldMetadataMaps> {
|
||||
constructor(
|
||||
@InjectWorkspaceScopedRepository(SearchFieldMetadataEntity)
|
||||
private readonly searchFieldMetadataRepository: WorkspaceScopedRepository<SearchFieldMetadataEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity)
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<FlatSearchFieldMetadataMaps> {
|
||||
const [
|
||||
existingSearchFieldMetadatas,
|
||||
applications,
|
||||
objectMetadatas,
|
||||
fieldMetadatas,
|
||||
] = await Promise.all([
|
||||
this.searchFieldMetadataRepository.find(workspaceId),
|
||||
this.applicationRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.objectMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
this.fieldMetadataRepository.find({
|
||||
where: { workspaceId },
|
||||
select: ['id', 'universalIdentifier'],
|
||||
withDeleted: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const applicationIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(applications);
|
||||
const objectMetadataIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(objectMetadatas);
|
||||
const fieldMetadataIdToUniversalIdentifierMap =
|
||||
createIdToUniversalIdentifierMap(fieldMetadatas);
|
||||
|
||||
const flatSearchFieldMetadataMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const searchFieldMetadata of existingSearchFieldMetadatas) {
|
||||
const flatSearchFieldMetadata =
|
||||
fromSearchFieldMetadataEntityToFlatSearchFieldMetadata({
|
||||
entity: searchFieldMetadata,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
});
|
||||
|
||||
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
|
||||
flatEntity: flatSearchFieldMetadata,
|
||||
flatEntityMapsToMutate: flatSearchFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatSearchFieldMetadataMaps;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
|
||||
export type FlatSearchFieldMetadataMaps =
|
||||
FlatEntityMaps<FlatSearchFieldMetadata>;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
|
||||
export type FlatSearchFieldMetadata = FlatEntityFrom<SearchFieldMetadataEntity>;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
export const buildFlatSearchFieldMetadataForField = ({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadata,
|
||||
position,
|
||||
}: {
|
||||
flatObjectMetadata: Pick<
|
||||
FlatObjectMetadata,
|
||||
'applicationUniversalIdentifier' | 'universalIdentifier'
|
||||
>;
|
||||
flatFieldMetadata: { universalIdentifier: string };
|
||||
position: number;
|
||||
}): UniversalFlatSearchFieldMetadata => {
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
universalIdentifier: v4(),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
position,
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: flatFieldMetadata.universalIdentifier,
|
||||
};
|
||||
};
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isSearchableFieldType } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type FieldTypeAndNameMetadata,
|
||||
getTsVectorColumnExpressionFromFields,
|
||||
} from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
|
||||
export type SearchVectorTargetField = {
|
||||
name: string;
|
||||
type: FieldMetadataType;
|
||||
// Per-object ordinal from the searchFieldMetadata row, driving deterministic order.
|
||||
position: number;
|
||||
// Tie-break for rows sharing a position (searchFieldMetadata universalIdentifier).
|
||||
sortKey: string;
|
||||
};
|
||||
|
||||
export const buildSearchVectorTargetField = (
|
||||
field: { name: string; type: FieldMetadataType },
|
||||
position: number,
|
||||
sortKey: string,
|
||||
): SearchVectorTargetField => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
position,
|
||||
sortKey,
|
||||
});
|
||||
|
||||
// Builds the searchVector to_tsvector asExpression from an object's targeted fields.
|
||||
// Callers MUST pass the POST-change field set (account for rows added/removed in the
|
||||
// same operation), not re-read stale maps. Ordering is deterministic only to minimize
|
||||
// asExpression churn — tsvector matching is order-insensitive.
|
||||
export const computeSearchVectorAsExpressionFromSearchFieldMetadatas = (
|
||||
targetSearchableFields: SearchVectorTargetField[],
|
||||
): string => {
|
||||
const orderedSearchableFields: FieldTypeAndNameMetadata[] = [
|
||||
...targetSearchableFields,
|
||||
]
|
||||
.sort((a, b) => {
|
||||
if (a.position !== b.position) {
|
||||
return a.position - b.position;
|
||||
}
|
||||
|
||||
return a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0;
|
||||
})
|
||||
.flatMap((targetField) =>
|
||||
isSearchableFieldType(targetField.type)
|
||||
? [{ name: targetField.name, type: targetField.type }]
|
||||
: [],
|
||||
);
|
||||
|
||||
return getTsVectorColumnExpressionFromFields(orderedSearchableFields);
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { getMetadataEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-entity-relation-properties.util';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import type { FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
|
||||
|
||||
export const fromSearchFieldMetadataEntityToFlatSearchFieldMetadata = ({
|
||||
entity: searchFieldMetadataEntity,
|
||||
applicationIdToUniversalIdentifierMap,
|
||||
objectMetadataIdToUniversalIdentifierMap,
|
||||
fieldMetadataIdToUniversalIdentifierMap,
|
||||
}: FromEntityToFlatEntityArgs<'searchFieldMetadata'>): FlatSearchFieldMetadata => {
|
||||
const searchFieldMetadataEntityWithoutRelations = removePropertiesFromRecord(
|
||||
searchFieldMetadataEntity,
|
||||
getMetadataEntityRelationProperties('searchFieldMetadata'),
|
||||
);
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
applicationIdToUniversalIdentifierMap.get(
|
||||
searchFieldMetadataEntity.applicationId,
|
||||
);
|
||||
|
||||
if (!isDefined(applicationUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Application with id ${searchFieldMetadataEntity.applicationId} not found for searchFieldMetadata ${searchFieldMetadataEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataUniversalIdentifier =
|
||||
objectMetadataIdToUniversalIdentifierMap.get(
|
||||
searchFieldMetadataEntity.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Object metadata with id ${searchFieldMetadataEntity.objectMetadataId} not found for searchFieldMetadata ${searchFieldMetadataEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const fieldMetadataUniversalIdentifier =
|
||||
fieldMetadataIdToUniversalIdentifierMap.get(
|
||||
searchFieldMetadataEntity.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldMetadataUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
`Field metadata with id ${searchFieldMetadataEntity.fieldMetadataId} not found for searchFieldMetadata ${searchFieldMetadataEntity.id}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...searchFieldMetadataEntityWithoutRelations,
|
||||
createdAt: searchFieldMetadataEntity.createdAt.toISOString(),
|
||||
updatedAt: searchFieldMetadataEntity.updatedAt.toISOString(),
|
||||
universalIdentifier:
|
||||
searchFieldMetadataEntityWithoutRelations.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier,
|
||||
};
|
||||
};
|
||||
+10
@@ -12,6 +12,7 @@ import {
|
||||
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { type ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
|
||||
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
@@ -142,6 +143,15 @@ export class ObjectMetadataEntity
|
||||
})
|
||||
indexMetadatas: Relation<IndexMetadataEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => SearchFieldMetadataEntity,
|
||||
(searchFieldMetadata) => searchFieldMetadata.objectMetadata,
|
||||
{
|
||||
cascade: true,
|
||||
},
|
||||
)
|
||||
searchFieldMetadatas: Relation<SearchFieldMetadataEntity[]>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+15
@@ -89,6 +89,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatCommandMenuItemMaps: existingFlatCommandMenuItemMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -99,6 +100,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
'flatViewFieldMaps',
|
||||
'flatViewMaps',
|
||||
'flatCommandMenuItemMaps',
|
||||
'flatSearchFieldMetadataMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -110,6 +112,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatIndexMetadatasToUpdate,
|
||||
flatViewFieldsToCreate,
|
||||
flatViewFieldsToUpdate,
|
||||
searchFieldMetadatasToCreate,
|
||||
} = fromUpdateObjectInputToFlatObjectMetadataAndRelatedFlatEntities({
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
@@ -117,6 +120,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatViewFieldMaps: existingFlatViewFieldMaps,
|
||||
flatViewMaps: existingFlatViewMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const existingFlatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
@@ -177,6 +181,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: flatViewFieldsToUpdate,
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: searchFieldMetadatasToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: [],
|
||||
@@ -485,6 +494,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
const {
|
||||
flatObjectMetadataToCreate,
|
||||
flatIndexMetadataToCreate,
|
||||
flatSearchFieldMetadataToCreate,
|
||||
flatFieldMetadataToCreateOnObject,
|
||||
relationTargetFlatFieldMetadataToCreate,
|
||||
} = fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate({
|
||||
@@ -620,6 +630,11 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: flatSearchFieldMetadataToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
...(isDefined(flatNavigationMenuItemToCreate)
|
||||
? {
|
||||
navigationMenuItem: {
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { buildFlatSearchFieldMetadataForField } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/build-flat-search-field-metadata-for-field.util';
|
||||
import { type DefaultFlatFieldForCustomObjectMaps } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-field-metadatas-for-custom-object.util';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
// Mirrors the custom-object searchVector, which indexes the name field only. Junction
|
||||
// objects (skipNameField) have no name field and therefore get no row.
|
||||
export const buildDefaultSearchFieldMetadatasForCustomObject = ({
|
||||
flatObjectMetadata,
|
||||
defaultFlatFieldForCustomObjectMaps,
|
||||
}: {
|
||||
flatObjectMetadata: UniversalFlatObjectMetadata & { id: string };
|
||||
defaultFlatFieldForCustomObjectMaps: DefaultFlatFieldForCustomObjectMaps;
|
||||
}): {
|
||||
searchFieldMetadatas: UniversalFlatSearchFieldMetadata[];
|
||||
} => {
|
||||
const nameField = defaultFlatFieldForCustomObjectMaps.fields.nameField;
|
||||
|
||||
if (!isDefined(nameField)) {
|
||||
return {
|
||||
searchFieldMetadatas: [],
|
||||
};
|
||||
}
|
||||
|
||||
const nameSearchFieldMetadata = buildFlatSearchFieldMetadataForField({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadata: nameField,
|
||||
position: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
searchFieldMetadatas: [nameSearchFieldMetadata],
|
||||
};
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export enum SearchFieldMetadataExceptionCode {
|
||||
SEARCH_FIELD_METADATA_NOT_FOUND = 'SEARCH_FIELD_METADATA_NOT_FOUND',
|
||||
INVALID_SEARCH_FIELD_METADATA_DATA = 'INVALID_SEARCH_FIELD_METADATA_DATA',
|
||||
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
||||
FIELD_METADATA_NOT_FOUND = 'FIELD_METADATA_NOT_FOUND',
|
||||
}
|
||||
+11
-4
@@ -13,23 +13,27 @@ import {
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity('searchFieldMetadata')
|
||||
@Entity({ name: 'searchFieldMetadata', schema: 'core' })
|
||||
@Unique('IDX_SEARCH_FIELD_METADATA_OBJECT_FIELD_UNIQUE', [
|
||||
'objectMetadataId',
|
||||
'fieldMetadataId',
|
||||
])
|
||||
@Index('IDX_SEARCH_FIELD_METADATA_WORKSPACE_ID', ['workspaceId'])
|
||||
@Index('IDX_SEARCH_FIELD_METADATA_OBJECT_METADATA_ID', ['objectMetadataId'])
|
||||
export class SearchFieldMetadataEntity extends WorkspaceRelatedEntity {
|
||||
export class SearchFieldMetadataEntity extends SyncableEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
objectMetadataId: string;
|
||||
|
||||
@ManyToOne(() => ObjectMetadataEntity, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(
|
||||
() => ObjectMetadataEntity,
|
||||
(objectMetadata) => objectMetadata.searchFieldMetadatas,
|
||||
{ onDelete: 'CASCADE' },
|
||||
)
|
||||
@JoinColumn({ name: 'objectMetadataId' })
|
||||
objectMetadata: Relation<ObjectMetadataEntity>;
|
||||
|
||||
@@ -40,6 +44,9 @@ export class SearchFieldMetadataEntity extends WorkspaceRelatedEntity {
|
||||
@JoinColumn({ name: 'fieldMetadataId' })
|
||||
fieldMetadata: Relation<FieldMetadataEntity>;
|
||||
|
||||
@Column({ nullable: false, type: 'float' })
|
||||
position: number;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+2
@@ -109,6 +109,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
targetTableName: 'test_entity',
|
||||
fieldIds: ['field-id'],
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
viewIds: [],
|
||||
@@ -131,6 +132,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
};
|
||||
|
||||
+2
@@ -25,6 +25,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
universalIdentifier: 'test-object-id',
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds,
|
||||
@@ -47,6 +48,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
+2
@@ -25,6 +25,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
universalIdentifier: 'test-object-id',
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds,
|
||||
@@ -47,6 +48,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
+2
@@ -25,6 +25,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
universalIdentifier: 'test-object-id',
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
fieldIds,
|
||||
@@ -47,6 +48,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ export const WORKSPACE_CACHE_KEYS_V2 = {
|
||||
flatViewFilterMaps: 'flat-maps:view-filter',
|
||||
flatViewFilterGroupMaps: 'flat-maps:view-filter-group',
|
||||
flatViewSortMaps: 'flat-maps:view-sort',
|
||||
flatSearchFieldMetadataMaps: 'flat-maps:search-field-metadata',
|
||||
flatLogicFunctionMaps: 'flat-maps:logic-function',
|
||||
featureFlagsMap: 'feature-flag:feature-flags-map',
|
||||
rolesPermissions: 'metadata:permissions:roles-permissions',
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
export const TWENTY_STANDARD_ALL_METADATA_NAME = [
|
||||
'index',
|
||||
'searchFieldMetadata',
|
||||
'objectMetadata',
|
||||
'fieldMetadata',
|
||||
'viewField',
|
||||
|
||||
+2
@@ -106,6 +106,7 @@ export const createStandardObjectFlatMetadata = <
|
||||
targetTableName: 'DEPRECATED',
|
||||
fieldIds: [],
|
||||
indexMetadataIds: [],
|
||||
searchFieldMetadataIds: [],
|
||||
objectPermissionIds: [],
|
||||
fieldPermissionIds: [],
|
||||
viewIds: [],
|
||||
@@ -119,6 +120,7 @@ export const createStandardObjectFlatMetadata = <
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
viewUniversalIdentifiers: [],
|
||||
indexMetadataUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
imageIdentifierFieldMetadataUniversalIdentifier,
|
||||
};
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
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 { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
|
||||
import { buildCompanyStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-company-standard-flat-search-field-metadata.util';
|
||||
import { buildDashboardStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-dashboard-standard-flat-search-field-metadata.util';
|
||||
import { buildMessageListStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-message-list-standard-flat-search-field-metadata.util';
|
||||
import { buildNoteStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-note-standard-flat-search-field-metadata.util';
|
||||
import { buildOpportunityStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-opportunity-standard-flat-search-field-metadata.util';
|
||||
import { buildPersonStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-person-standard-flat-search-field-metadata.util';
|
||||
import { buildTaskStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-task-standard-flat-search-field-metadata.util';
|
||||
import { buildWorkflowStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-workflow-standard-flat-search-field-metadata.util';
|
||||
import { buildWorkspaceMemberStandardFlatSearchFieldMetadatas } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/compute-workspace-member-standard-flat-search-field-metadata.util';
|
||||
import { type CreateStandardSearchFieldArgs } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
type StandardSearchFieldBuilder<P extends AllStandardObjectName> = (
|
||||
args: Omit<CreateStandardSearchFieldArgs<P>, 'context'>,
|
||||
) => FlatSearchFieldMetadata[];
|
||||
|
||||
// Only isSearchable objects get rows; each object's field set mirrors its searchVector
|
||||
// asExpression, built from the same SEARCH_FIELDS_FOR_* constant.
|
||||
const STANDARD_FLAT_SEARCH_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
||||
company: buildCompanyStandardFlatSearchFieldMetadatas,
|
||||
dashboard: buildDashboardStandardFlatSearchFieldMetadatas,
|
||||
messageList: buildMessageListStandardFlatSearchFieldMetadatas,
|
||||
note: buildNoteStandardFlatSearchFieldMetadatas,
|
||||
opportunity: buildOpportunityStandardFlatSearchFieldMetadatas,
|
||||
person: buildPersonStandardFlatSearchFieldMetadatas,
|
||||
task: buildTaskStandardFlatSearchFieldMetadatas,
|
||||
workflow: buildWorkflowStandardFlatSearchFieldMetadatas,
|
||||
workspaceMember: buildWorkspaceMemberStandardFlatSearchFieldMetadatas,
|
||||
} satisfies {
|
||||
[P in AllStandardObjectName]?: StandardSearchFieldBuilder<P>;
|
||||
};
|
||||
|
||||
export const buildStandardFlatSearchFieldMetadataMaps = (
|
||||
args: Omit<CreateStandardSearchFieldArgs, 'context' | 'objectName'>,
|
||||
): FlatEntityMaps<FlatSearchFieldMetadata> => {
|
||||
const allSearchFieldMetadatas: FlatSearchFieldMetadata[] = (
|
||||
Object.keys(
|
||||
STANDARD_FLAT_SEARCH_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME,
|
||||
) as (keyof typeof STANDARD_FLAT_SEARCH_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME)[]
|
||||
).flatMap((objectName) => {
|
||||
const builder: StandardSearchFieldBuilder<typeof objectName> =
|
||||
STANDARD_FLAT_SEARCH_FIELD_METADATA_BUILDERS_BY_OBJECT_NAME[objectName];
|
||||
|
||||
return builder({
|
||||
...args,
|
||||
objectName,
|
||||
});
|
||||
});
|
||||
|
||||
let flatSearchFieldMetadataMaps = createEmptyFlatEntityMaps();
|
||||
|
||||
for (const searchFieldMetadata of allSearchFieldMetadatas) {
|
||||
flatSearchFieldMetadataMaps = addFlatEntityToFlatEntityMapsOrThrow({
|
||||
flatEntity: searchFieldMetadata,
|
||||
flatEntityMaps: flatSearchFieldMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return flatSearchFieldMetadataMaps;
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { SEARCH_FIELDS_FOR_COMPANY } from 'src/modules/company/standard-objects/company.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildCompanyStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'company'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_COMPANY.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: searchField.name as AllStandardObjectFieldName<'company'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { SEARCH_FIELDS_FOR_DASHBOARD } from 'src/modules/dashboard/standard-objects/dashboard.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildDashboardStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'dashboard'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_DASHBOARD.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: searchField.name as AllStandardObjectFieldName<'dashboard'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { SEARCH_FIELDS_FOR_MESSAGE_LIST } from 'src/modules/emailing/standard-objects/message-list.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildMessageListStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'messageList'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_MESSAGE_LIST.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName:
|
||||
searchField.name as AllStandardObjectFieldName<'messageList'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildNoteStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'note'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_NOTES.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: searchField.name as AllStandardObjectFieldName<'note'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { SEARCH_FIELDS_FOR_OPPORTUNITY } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildOpportunityStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'opportunity'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_OPPORTUNITY.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName:
|
||||
searchField.name as AllStandardObjectFieldName<'opportunity'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { SEARCH_FIELDS_FOR_PERSON } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildPersonStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'person'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_PERSON.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: searchField.name as AllStandardObjectFieldName<'person'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildTaskStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'task'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_TASKS.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: searchField.name as AllStandardObjectFieldName<'task'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { SEARCH_FIELDS_FOR_WORKFLOWS } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildWorkflowStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'workflow'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_WORKFLOWS.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName: searchField.name as AllStandardObjectFieldName<'workflow'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { SEARCH_FIELDS_FOR_WORKSPACE_MEMBER } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import {
|
||||
type CreateStandardSearchFieldArgs,
|
||||
createStandardSearchFieldFlatMetadata,
|
||||
} from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/create-standard-search-field-flat-metadata.util';
|
||||
|
||||
export const buildWorkspaceMemberStandardFlatSearchFieldMetadatas = ({
|
||||
now,
|
||||
objectName,
|
||||
workspaceId,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
}: Omit<
|
||||
CreateStandardSearchFieldArgs<'workspaceMember'>,
|
||||
'context'
|
||||
>): FlatSearchFieldMetadata[] =>
|
||||
SEARCH_FIELDS_FOR_WORKSPACE_MEMBER.map((searchField, position) =>
|
||||
createStandardSearchFieldFlatMetadata({
|
||||
objectName,
|
||||
workspaceId,
|
||||
context: {
|
||||
fieldName:
|
||||
searchField.name as AllStandardObjectFieldName<'workspaceMember'>,
|
||||
position,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}),
|
||||
);
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type';
|
||||
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
|
||||
import { type StandardBuilderArgs } from 'src/engine/workspace-manager/twenty-standard-application/types/metadata-standard-buillder-args.type';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
|
||||
export type CreateStandardSearchFieldOptions<O extends AllStandardObjectName> =
|
||||
{
|
||||
fieldName: AllStandardObjectFieldName<O>;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type CreateStandardSearchFieldArgs<
|
||||
O extends AllStandardObjectName = AllStandardObjectName,
|
||||
> = StandardBuilderArgs<'searchFieldMetadata'> & {
|
||||
objectName: O;
|
||||
context: CreateStandardSearchFieldOptions<O>;
|
||||
};
|
||||
|
||||
export const createStandardSearchFieldFlatMetadata = <
|
||||
O extends AllStandardObjectName,
|
||||
>({
|
||||
workspaceId,
|
||||
objectName,
|
||||
context: { fieldName, position },
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps: { flatFieldMetadataMaps, flatObjectMetadataMaps },
|
||||
twentyStandardApplicationId,
|
||||
now,
|
||||
}: CreateStandardSearchFieldArgs<O>): FlatSearchFieldMetadata => {
|
||||
const objectFields = STANDARD_OBJECTS[objectName].fields;
|
||||
|
||||
const objectMetadataId =
|
||||
standardObjectMetadataRelatedEntityIds[objectName].id;
|
||||
const fieldMetadataId =
|
||||
standardObjectMetadataRelatedEntityIds[objectName].fields[fieldName].id;
|
||||
|
||||
const objectMetadataUniversalIdentifier =
|
||||
STANDARD_OBJECTS[objectName].universalIdentifier;
|
||||
const flatObjectMetadata =
|
||||
findFlatEntityByUniversalIdentifierOrThrow<UniversalFlatObjectMetadata>({
|
||||
universalIdentifier: objectMetadataUniversalIdentifier,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const fieldMetadataUniversalIdentifier =
|
||||
objectFields[fieldName as keyof typeof objectFields].universalIdentifier;
|
||||
const flatFieldMetadata =
|
||||
findFlatEntityByUniversalIdentifierOrThrow<UniversalFlatFieldMetadata>({
|
||||
universalIdentifier: fieldMetadataUniversalIdentifier,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
universalIdentifier: v4(),
|
||||
applicationId: twentyStandardApplicationId,
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectMetadataId,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
fieldMetadataId,
|
||||
fieldMetadataUniversalIdentifier: flatFieldMetadata.universalIdentifier,
|
||||
position,
|
||||
workspaceId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
};
|
||||
+13
@@ -17,6 +17,7 @@ import { buildStandardFlatPageLayoutWidgetMetadataMaps } from 'src/engine/worksp
|
||||
import { buildStandardFlatPageLayoutMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/page-layout/build-standard-flat-page-layout-metadata-maps.util';
|
||||
import { buildStandardFlatPermissionFlagMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/permission-flag/build-standard-flat-permission-flag-metadata-maps.util';
|
||||
import { buildStandardFlatRoleMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/role-metadata/build-standard-flat-role-metadata-maps.util';
|
||||
import { buildStandardFlatSearchFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/search-field-metadata/build-standard-flat-search-field-metadata-maps.util';
|
||||
import { buildStandardFlatSkillMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/build-standard-flat-skill-metadata-maps.util';
|
||||
import { buildStandardFlatViewFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field/build-standard-flat-view-field-metadata-maps.util';
|
||||
import { buildStandardFlatViewFieldGroupMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/view-field-group/build-standard-flat-view-field-group-metadata-maps.util';
|
||||
@@ -74,6 +75,17 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
twentyStandardApplicationId,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadataMaps = buildStandardFlatSearchFieldMetadataMaps({
|
||||
dependencyFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
now,
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
workspaceId,
|
||||
twentyStandardApplicationId,
|
||||
});
|
||||
|
||||
const flatViewMaps = buildStandardFlatViewMetadataMaps({
|
||||
dependencyFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
@@ -217,6 +229,7 @@ export const computeTwentyStandardApplicationAllFlatEntityMaps = ({
|
||||
flatViewGroupMaps,
|
||||
flatViewMaps,
|
||||
flatIndexMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
flatNavigationMenuItemMaps,
|
||||
|
||||
+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,
|
||||
);
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util';
|
||||
import { search } from 'test/integration/graphql/utils/search.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { jestExpectToBeDefined } from 'test/utils/jest-expect-to-be-defined.util.test';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
|
||||
// ISO search surface: a custom object's searchVector indexes the name field only. Creating or
|
||||
// deleting an additional searchable field must NOT change the search surface through the API.
|
||||
describe('Field metadata create/delete - search vector ISO surface', () => {
|
||||
let testObjectMetadataId: string;
|
||||
let extraFieldMetadataId: string;
|
||||
|
||||
const OBJECT_NAME_SINGULAR = 'searchSurfaceObject';
|
||||
const OBJECT_NAME_PLURAL = 'searchSurfaceObjects';
|
||||
const EXTRA_FIELD_NAME = 'extraSearchableField';
|
||||
const RECORD_NAME_VALUE = 'SearchSurfaceNameToken11';
|
||||
const RECORD_EXTRA_VALUE = 'SearchSurfaceExtraToken22';
|
||||
|
||||
const getSearchVectorAsExpression = async (): Promise<string> => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: { id: { eq: testObjectMetadataId } },
|
||||
paging: { first: 1 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
nameSingular
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
type
|
||||
settings
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const testObject = objects[0];
|
||||
|
||||
jestExpectToBeDefined(testObject);
|
||||
jestExpectToBeDefined(testObject.fieldsList);
|
||||
|
||||
const searchVectorField = testObject.fieldsList.find(
|
||||
(field: FieldMetadataDTO) => field.type === FieldMetadataType.TS_VECTOR,
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(searchVectorField);
|
||||
|
||||
const settings = searchVectorField.settings as {
|
||||
asExpression?: string;
|
||||
};
|
||||
|
||||
jestExpectToBeDefined(settings);
|
||||
jestExpectToBeDefined(settings.asExpression);
|
||||
|
||||
return settings.asExpression;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: OBJECT_NAME_PLURAL,
|
||||
labelSingular: 'Search Surface Object',
|
||||
labelPlural: 'Search Surface Objects',
|
||||
icon: 'IconSearch',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
testObjectMetadataId = objectMetadataId;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add a newly created searchable field to the search vector or to global search', async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneField: { id: extraFieldId },
|
||||
},
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: EXTRA_FIELD_NAME,
|
||||
label: 'Extra Searchable Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
gqlFields: `id name type`,
|
||||
});
|
||||
|
||||
extraFieldMetadataId = extraFieldId;
|
||||
|
||||
const asExpressionAfterCreate = await getSearchVectorAsExpression();
|
||||
|
||||
expect(asExpressionAfterCreate).not.toContain(EXTRA_FIELD_NAME);
|
||||
|
||||
await createManyOperation({
|
||||
objectMetadataSingularName: OBJECT_NAME_SINGULAR,
|
||||
objectMetadataPluralName: OBJECT_NAME_PLURAL,
|
||||
gqlFields: `id name ${EXTRA_FIELD_NAME}`,
|
||||
data: [
|
||||
{
|
||||
name: RECORD_NAME_VALUE,
|
||||
[EXTRA_FIELD_NAME]: RECORD_EXTRA_VALUE,
|
||||
},
|
||||
],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
// The name field is indexed, so the record is reachable through its name value.
|
||||
const searchByName = await search({
|
||||
searchInput: RECORD_NAME_VALUE,
|
||||
includedObjectNameSingulars: [OBJECT_NAME_SINGULAR],
|
||||
limit: 10,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(searchByName.data.search.edges.length).toBe(1);
|
||||
|
||||
// The extra field is outside the search surface, so its value is not searchable.
|
||||
const searchByExtraField = await search({
|
||||
searchInput: RECORD_EXTRA_VALUE,
|
||||
includedObjectNameSingulars: [OBJECT_NAME_SINGULAR],
|
||||
limit: 10,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(searchByExtraField.data.search.edges.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should keep the search vector unchanged after the extra field is deleted', async () => {
|
||||
// Custom fields must be deactivated before deletion.
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: extraFieldMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
gqlFields: `id`,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
await deleteOneFieldMetadata({
|
||||
input: { idToDelete: extraFieldMetadataId },
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const asExpressionAfterDelete = await getSearchVectorAsExpression();
|
||||
|
||||
expect(asExpressionAfterDelete).not.toContain(EXTRA_FIELD_NAME);
|
||||
});
|
||||
});
|
||||
+100
-124
@@ -1,6 +1,5 @@
|
||||
import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util';
|
||||
import { search } from 'test/integration/graphql/utils/search.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
@@ -11,125 +10,24 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
|
||||
describe('Field metadata update - search vector side effect', () => {
|
||||
// ISO search surface: renaming an indexed field (the name field, the default search field of a
|
||||
// custom object) must recompute the searchVector asExpression so global search keeps working,
|
||||
// since the expression embeds the field's column name.
|
||||
describe('Field metadata update - search vector rename recompute', () => {
|
||||
let testObjectMetadataId: string;
|
||||
let testFieldMetadataId: string;
|
||||
let nameFieldMetadataId: string;
|
||||
let createdRecordId: string;
|
||||
|
||||
const OBJECT_NAME_SINGULAR = 'searchVectorTestObject';
|
||||
const OBJECT_NAME_PLURAL = 'searchVectorTestObjects';
|
||||
const INITIAL_FIELD_NAME = 'searchableTitle';
|
||||
const UPDATED_FIELD_NAME = 'renamedTitle';
|
||||
const RECORD_FIELD_VALUE = 'UniqueSearchableValue123';
|
||||
const RECORD_NAME_FIELD_VALUE = 'NameValue';
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: OBJECT_NAME_PLURAL,
|
||||
labelSingular: 'Search Vector Test Object',
|
||||
labelPlural: 'Search Vector Test Objects',
|
||||
icon: 'IconSearch',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
testObjectMetadataId = objectMetadataId;
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneField: { id: fieldMetadataId },
|
||||
},
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: INITIAL_FIELD_NAME,
|
||||
label: 'Searchable Title',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
`,
|
||||
});
|
||||
|
||||
testFieldMetadataId = fieldMetadataId;
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: {
|
||||
labelIdentifierFieldMetadataId: testFieldMetadataId,
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const { data } = await createManyOperation({
|
||||
objectMetadataSingularName: OBJECT_NAME_SINGULAR,
|
||||
objectMetadataPluralName: OBJECT_NAME_PLURAL,
|
||||
gqlFields: `id name ${INITIAL_FIELD_NAME}`,
|
||||
data: [
|
||||
{
|
||||
[INITIAL_FIELD_NAME]: RECORD_FIELD_VALUE,
|
||||
name: RECORD_NAME_FIELD_VALUE,
|
||||
},
|
||||
],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdRecordId = data.createdRecords[0].id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: {
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should update search vector asExpression when renaming a field used as label identifier and search should work with new field name', async () => {
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: testFieldMetadataId,
|
||||
updatePayload: {
|
||||
name: UPDATED_FIELD_NAME,
|
||||
label: 'Renamed Title',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
label
|
||||
`,
|
||||
expectToFail: false,
|
||||
});
|
||||
const OBJECT_NAME_SINGULAR = 'searchVectorRenameObject';
|
||||
const OBJECT_NAME_PLURAL = 'searchVectorRenameObjects';
|
||||
const RENAMED_NAME_FIELD = 'searchableLabelColumn';
|
||||
const RECORD_NAME_VALUE = 'RenameRecomputeSearchToken33';
|
||||
|
||||
const getSearchVectorAsExpression = async (): Promise<string> => {
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: {
|
||||
id: { eq: testObjectMetadataId },
|
||||
},
|
||||
filter: { id: { eq: testObjectMetadataId } },
|
||||
paging: { first: 1 },
|
||||
},
|
||||
gqlFields: `
|
||||
@@ -144,9 +42,6 @@ describe('Field metadata update - search vector side effect', () => {
|
||||
`,
|
||||
});
|
||||
|
||||
expect(objects).toBeDefined();
|
||||
expect(objects.length).toBe(1);
|
||||
|
||||
const testObject = objects[0];
|
||||
|
||||
jestExpectToBeDefined(testObject);
|
||||
@@ -160,24 +55,105 @@ describe('Field metadata update - search vector side effect', () => {
|
||||
|
||||
const settings = searchVectorField.settings as {
|
||||
asExpression?: string;
|
||||
generatedType?: string;
|
||||
};
|
||||
|
||||
jestExpectToBeDefined(settings);
|
||||
expect(settings.asExpression).toBeDefined();
|
||||
expect(settings.asExpression).toContain(UPDATED_FIELD_NAME);
|
||||
expect(settings.asExpression).not.toContain(INITIAL_FIELD_NAME);
|
||||
jestExpectToBeDefined(settings.asExpression);
|
||||
|
||||
return settings.asExpression;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: OBJECT_NAME_PLURAL,
|
||||
labelSingular: 'Search Vector Rename Object',
|
||||
labelPlural: 'Search Vector Rename Objects',
|
||||
icon: 'IconSearch',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
testObjectMetadataId = objectMetadataId;
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: { id: { eq: testObjectMetadataId } },
|
||||
paging: { first: 1 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
fieldsList {
|
||||
id
|
||||
name
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const nameField = objects[0]?.fieldsList?.find(
|
||||
(field: FieldMetadataDTO) => field.name === 'name',
|
||||
);
|
||||
|
||||
jestExpectToBeDefined(nameField);
|
||||
nameFieldMetadataId = nameField.id;
|
||||
|
||||
const { data } = await createManyOperation({
|
||||
objectMetadataSingularName: OBJECT_NAME_SINGULAR,
|
||||
objectMetadataPluralName: OBJECT_NAME_PLURAL,
|
||||
gqlFields: `id name`,
|
||||
data: [{ name: RECORD_NAME_VALUE }],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
createdRecordId = data.createdRecords[0].id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('should recompute the search vector and keep search working when the indexed name field is renamed', async () => {
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: nameFieldMetadataId,
|
||||
updatePayload: {
|
||||
name: RENAMED_NAME_FIELD,
|
||||
label: 'Searchable Label Column',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
},
|
||||
gqlFields: `id name`,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const asExpression = await getSearchVectorAsExpression();
|
||||
|
||||
expect(asExpression).toContain(RENAMED_NAME_FIELD);
|
||||
|
||||
const searchResult = await search({
|
||||
searchInput: RECORD_FIELD_VALUE,
|
||||
searchInput: RECORD_NAME_VALUE,
|
||||
includedObjectNameSingulars: [OBJECT_NAME_SINGULAR],
|
||||
limit: 10,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(searchResult.data).toBeDefined();
|
||||
expect(searchResult.data.search).toBeDefined();
|
||||
expect(searchResult.data.search.edges).toBeDefined();
|
||||
expect(searchResult.data.search.edges.length).toBe(1);
|
||||
expect(searchResult.data.search.edges[0].node.recordId).toBe(
|
||||
createdRecordId,
|
||||
|
||||
+390
-30
@@ -403,6 +403,29 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -636,12 +659,13 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -1055,6 +1079,29 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -1288,12 +1335,13 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -1718,6 +1766,29 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -1951,12 +2022,13 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -2370,6 +2442,29 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -2603,12 +2698,13 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -3057,6 +3153,29 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -3290,12 +3409,13 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -3709,6 +3829,29 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -3942,12 +4085,13 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -4361,6 +4505,29 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -4594,12 +4761,13 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -5024,6 +5192,29 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -5257,12 +5448,13 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -5700,6 +5892,29 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -5933,12 +6148,13 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -6358,6 +6574,29 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -6591,12 +6830,13 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -7034,6 +7274,29 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -7267,12 +7530,13 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -7686,6 +7950,29 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -7919,12 +8206,13 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -8373,6 +8661,29 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -8606,12 +8917,13 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -9025,6 +9337,29 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -9258,12 +9593,13 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
@@ -9677,6 +10013,29 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"searchFieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_METADATA_NOT_FOUND",
|
||||
"message": "Field metadata not found",
|
||||
"userFriendlyMessage": "Field metadata not found",
|
||||
},
|
||||
{
|
||||
"code": "OBJECT_METADATA_NOT_FOUND",
|
||||
"message": "Object metadata not found",
|
||||
"userFriendlyMessage": "Object metadata not found",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "searchFieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"view": [
|
||||
{
|
||||
"errors": [
|
||||
@@ -9910,12 +10269,13 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices",
|
||||
"message": "Validation failed for 17 fieldMetadata, 1 objectMetadata, 2 views, 9 viewFields, 5 indices, 1 searchFieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 17,
|
||||
"index": 5,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 34,
|
||||
"searchFieldMetadata": 1,
|
||||
"totalErrors": 35,
|
||||
"view": 2,
|
||||
"viewField": 9,
|
||||
},
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
// Asserts the DB-level invariant the backfill relies on: a custom object's
|
||||
// searchFieldMetadata rows are persisted with the object's own applicationId, never the
|
||||
// standard application's. The backfill's selection/grouping/idempotency is unit-tested
|
||||
// in build-search-field-metadata-backfill-operations.util.spec.ts.
|
||||
const LABEL_FIELD_NAME = 'tag';
|
||||
const PREFIX_OVERLAP_FIELD_NAME = 'tagline';
|
||||
|
||||
const STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-64aa-4b6f-b003-9c74b97cee20';
|
||||
|
||||
type SearchFieldMetadataRow = {
|
||||
objectApplicationId: string;
|
||||
searchFieldApplicationId: string;
|
||||
fieldObjectMetadataId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
const querySearchFieldMetadataRows = async (
|
||||
objectMetadataId: string,
|
||||
): Promise<SearchFieldMetadataRow[]> => {
|
||||
return global.testDataSource.query(
|
||||
`SELECT
|
||||
om."applicationId" AS "objectApplicationId",
|
||||
sfm."applicationId" AS "searchFieldApplicationId",
|
||||
fm."objectMetadataId" AS "fieldObjectMetadataId",
|
||||
fm.name AS "fieldName"
|
||||
FROM core."searchFieldMetadata" sfm
|
||||
INNER JOIN core."objectMetadata" om ON om.id = sfm."objectMetadataId"
|
||||
INNER JOIN core."fieldMetadata" fm ON fm.id = sfm."fieldMetadataId"
|
||||
WHERE sfm."objectMetadataId" = $1
|
||||
ORDER BY fm.name ASC`,
|
||||
[objectMetadataId],
|
||||
);
|
||||
};
|
||||
|
||||
// Resolves the standard application id of the workspace that owns the given object,
|
||||
// without needing the object's (GraphQL-hidden) workspaceId.
|
||||
const queryStandardApplicationIdForObjectWorkspace = async (
|
||||
objectMetadataId: string,
|
||||
): Promise<string> => {
|
||||
const rows: { id: string }[] = await global.testDataSource.query(
|
||||
`SELECT app.id AS id
|
||||
FROM core."application" app
|
||||
INNER JOIN core."objectMetadata" om ON om."workspaceId" = app."workspaceId"
|
||||
WHERE om.id = $1 AND app."universalIdentifier" = $2`,
|
||||
[objectMetadataId, STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER],
|
||||
);
|
||||
|
||||
return rows[0].id;
|
||||
};
|
||||
|
||||
describe('searchFieldMetadata rows - app-correct and deterministic per object', () => {
|
||||
let testObjectMetadataId: string;
|
||||
let labelFieldMetadataId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const {
|
||||
data: {
|
||||
createOneObject: { id: objectMetadataId },
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: 'backfillSearchFieldObject',
|
||||
namePlural: 'backfillSearchFieldObjects',
|
||||
labelSingular: 'Backfill Search Field Object',
|
||||
labelPlural: 'Backfill Search Field Objects',
|
||||
icon: 'IconSearch',
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
});
|
||||
|
||||
testObjectMetadataId = objectMetadataId;
|
||||
|
||||
const {
|
||||
data: {
|
||||
createOneField: { id: labelFieldId },
|
||||
},
|
||||
} = await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: LABEL_FIELD_NAME,
|
||||
label: 'Tag',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
gqlFields: `id name`,
|
||||
});
|
||||
|
||||
labelFieldMetadataId = labelFieldId;
|
||||
|
||||
await createOneFieldMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: PREFIX_OVERLAP_FIELD_NAME,
|
||||
label: 'Tagline',
|
||||
type: FieldMetadataType.TEXT,
|
||||
objectMetadataId: testObjectMetadataId,
|
||||
isLabelSyncedWithName: false,
|
||||
},
|
||||
gqlFields: `id name`,
|
||||
});
|
||||
|
||||
await updateOneObjectMetadata({
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: {
|
||||
labelIdentifierFieldMetadataId: labelFieldMetadataId,
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
idToUpdate: testObjectMetadataId,
|
||||
updatePayload: { isActive: false },
|
||||
},
|
||||
});
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: testObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it('persists every searchFieldMetadata row of a custom object with the custom object application id, not the standard application id', async () => {
|
||||
const rows = await querySearchFieldMetadataRows(testObjectMetadataId);
|
||||
const standardApplicationId =
|
||||
await queryStandardApplicationIdForObjectWorkspace(testObjectMetadataId);
|
||||
|
||||
expect(rows.length).toBeGreaterThan(0);
|
||||
|
||||
for (const row of rows) {
|
||||
// A custom object's rows carry its own applicationId, never the standard one.
|
||||
expect(row.searchFieldApplicationId).toBe(row.objectApplicationId);
|
||||
expect(row.searchFieldApplicationId).not.toBe(standardApplicationId);
|
||||
// No cross-object leakage: every targeted field belongs to this object.
|
||||
expect(row.fieldObjectMetadataId).toBe(testObjectMetadataId);
|
||||
}
|
||||
|
||||
expect(rows.some((row) => row.fieldName === LABEL_FIELD_NAME)).toBe(true);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user