Centralize system field side effects + search field metadata (#22594)
## Introduction Closes twentyhq/core-team-issues#2635 and twentyhq/core-team-issues#2642 and twentyhq/core-team-issues#2589 Object system fields (`searchVector` + its GIN index + `searchFieldMetadata`, the reserved system fields, default relations) were provisioned through several scattered, path-specific code paths. As a result the **app-manifest sync path** authored objects with an empty/`NULL` `searchVector` and **zero `searchFieldMetadata`**, so app-owned objects shipped a broken generated search column (see #22657). The generation logic also lived partly in imperative services rather than in the metadata side-effect engine, and relied on non-deterministic (`v4`) universal identifiers that `twenty apply` could not converge, destroying manually backfilled rows. This PR centralizes every object-creation system side effect into the **metadata side-effect engine**, extends the engine to keep search metadata consistent on field delete and object relabel, makes the standard app's search identifiers deterministic, and ships upgrade commands to reconcile existing workspaces. ## What changed ### Side effects moved into the metadata side-effect engine New dedicated, self-contained handlers — so every write path (API and app manifest) gets identical results, and side effects never trigger other side effects. **Object create / delete** (`handlers/object-metadata`) * **`objectSystemFieldsOnCreate`** — generates the 7 reserved system fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`). * **`objectSearchVectorOnCreate`** — provisions the full-text search surface as one unit: the `searchVector` `TS_VECTOR` field, its backing GIN index, and the `searchFieldMetadata` row (for searchable objects whose label identifier is a searchable field) that keeps `searchVector` populated instead of `NULL`. * **`objectSystemSideEffectsOnDelete`** — tears the above down on object deletion. **Search-metadata consistency on relabel / field delete** (new — these are what close the manifest-path gaps) * **`objectSearchVectorOnUpdate`** (`handlers/object-metadata`) — when a searchable object is relabeled onto a new searchable field, provisions the `searchFieldMetadata` row that indexes it. Relabeling is **additive**: existing rows (e.g. the provisioned `name` row) are preserved, so the previous label identifier stays searchable. Mirrors the API update path so a manifest re-sync that changes the label identifier reaches search parity. No-ops for junction objects (`id` label identifier) and non-searchable field types. * **`fieldSearchFieldMetadataOnDelete`** (`handlers/field-metadata`) — when a field is deleted, cascade-deletes every `searchFieldMetadata` row that indexes it. `searchFieldMetadata` is excluded from manifest deletion inference, so this explicit cascade is what covers **both the API and manifest paths** (the object-scoped DB cascade only fires on object deletion). Uses the `searchFieldMetadataUniversalIdentifiers` aggregator on the flat field for an O(k) lookup instead of scanning all rows. The **default `name` field and default relations are now caller-provided default fields** (SDK autocomplete on the manifest path, input transpiler on the API path) rather than system side effects — removing duplicate name generation, the imperative `build-default-*-for-custom-object` utilities, and the ad-hoc system-field integrity validator. ### Deterministic identifiers for the standard app The twenty-standard search GIN index and `searchFieldMetadata` now derive deterministic universal identifiers (`getIndexUniversalIdentifier` / `getSearchFieldUniversalIdentifier`) instead of `v4`, so `twenty apply` converges instead of recreating. ### Upgrade commands (`2-20`) to reconcile existing workspaces **Instance commands** (run once per instance; ordered fast → slow → workspace): 1. **`AddIsSystemSideEffectToSearchFieldMetadata`** (fast) — adds the `isSystemSideEffect` column to `core.searchFieldMetadata`. Defaults to `true`, which also correctly backfills every existing row since `searchFieldMetadata` is always system-derived (never user-authored). 2. **`BackfillNameFieldIsSystemSideEffect`** (slow) — re-flags existing `name` fields from `isSystemSideEffect: true` → `false`, since the default `name` field is now a caller-provided default like any other user-owned field (it was provisioned as `true` in 2.15 → 2.19). This is a pure data backfill, so the bulk `UPDATE` lives in `runDataMigration()` rather than `up()` — keeping it out of the fast schema transaction avoids holding an `ACCESS EXCLUSIVE` lock that could stall reads during the deploy. Slow instance commands still run before every workspace command of the version, so the fresh value is in place before the search-reconcile workspace commands recompute the `fieldMetadata` flat-entity cache. Scoping by name alone is safe (no engine-owned field is named `name`); `down()` is best-effort (pre-2.15 `false` rows are indistinguishable from flipped ones). **Workspace commands** (idempotent, dry-run supported): 1. **`reconcile-search-vector-gin-index-universal-identifier`** — re-owns every searchVector GIN index UID to its deterministic value (all applications), then backfills the missing GIN index for installed-app objects. 2. **`reconcile-search-field-metadata`** — re-owns every `searchFieldMetadata` UID (all applications), then backfills the missing rows for installed-app searchable objects. 3. **`rebuild-installed-app-search-vectors`** — rebuilds the `searchVector` column of every installed-app `TS_VECTOR` field, once the index and rows exist. Design notes: * **Re-own is global** (twenty-standard, workspace-custom, installed) — a UID convergence keyed on each row's own application. * **Backfill is installed-app only** — standard/custom objects already have these rows via the manifest funnel. * Re-own runs **before** backfill and is transaction-guarded; a failure aborts that workspace to avoid a unique-identifier collision. ## Tests * Integration: app manifest sync now asserts system fields + searchable objects (searchVector, GIN index, searchFieldMetadata) are created; a new relabel suite drives three manifest syncs and asserts records stay searchable through the old + new label identifiers and lose searchability when a field is removed; removed the obsolete system-fields-integrity suite/snapshots. * Unit: per-handler side-effect specs (including the new `objectSearchVectorOnUpdate` and `fieldSearchFieldMetadataOnDelete` handlers), and per-util specs for the re-own / backfill operation builders and the GIN-index classifier. ## Upgrade / migration notes * Existing workspaces converge on the next upgrade run via the `2-20` instance + workspace commands (idempotent, dry-run supported). * Backfill and rebuild go through the workspace-migration runner (automatic cache invalidation); the re-own step invalidates only the affected flat-entity maps directly. * The cross-version upgrade CI now flushes the cache before running the upgrade, so the new version recomputes every flat-entity map from the database instead of reading blobs the old version serialized in an older shape. ## Follow-up * `object-metadata.service.ts` still carries a `TODO: remove once default view fields move to the metadata side effect engine` — default view fields are the next candidate to move into the engine. * A single manifest sync cannot yet both create a field and relabel the object onto it, because `objectMetadata.update` is ordered before `fieldMetadata.create` in the migration runner. Tracked in twentyhq/core-team-issues#2655; to be fixed in a follow-up.
This commit is contained in:
@@ -221,6 +221,9 @@ jobs:
|
||||
- name: Server / Build
|
||||
run: npx nx build twenty-server
|
||||
|
||||
- name: Server / Flush cache
|
||||
run: npx nx run twenty-server:command-no-deps -- cache:flush
|
||||
|
||||
- name: Server / Run upgrade command
|
||||
run: npx nx run twenty-server:command-no-deps -- upgrade --verbose
|
||||
|
||||
|
||||
-86
@@ -122,16 +122,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Title',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: 'a717a3ba-e257-5269-95c4-1b8ca4cb8bcf',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
@@ -142,82 +132,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: '9a7f457a-6c15-581e-8f28-e2c680f75f97',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'f24a236a-e36a-5bab-91db-d43e3f4b8247',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '875c1f2e-19f0-5200-96ba-ac1cb4c1fd33',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'cc58b328-40e1-5087-b550-ad700e53490b',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '6f34f940-966a-52c2-a8f2-f2f04115d88e',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '37515dfb-8ae0-5326-a391-9e66205f6530',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: 'a418b503-74d0-500d-b4c0-75f364394e20',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: '45718beb-e0d7-5bde-811c-29ff01b0fcfd',
|
||||
},
|
||||
{
|
||||
name: 'timelineActivities',
|
||||
label: 'Timeline Activities',
|
||||
|
||||
-344
@@ -755,16 +755,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldType.TEXT,
|
||||
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000003',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: 'a4429f96-5745-511a-b246-67da9f920452',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
@@ -775,82 +765,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: '10452ca3-30cb-56fa-9e58-73f7b1c9fd65',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '777fa853-2198-5623-96f3-119ef5707ad3',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'e1715fe9-34e1-572c-b62c-f39d737a17df',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'e3b13370-7241-55f1-ab9a-346b57bf4dcf',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'd2e3e238-9bfd-5845-9fe8-3e58df09f246',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '690003ae-f649-537e-b124-d21ef0ee86a1',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '4b6d383d-4e84-592e-b1ae-30ae27139302',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: 'a92d59aa-23a1-5191-b840-933183c14846',
|
||||
},
|
||||
{
|
||||
description: 'Root notes tied to the RootNote',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
@@ -937,16 +851,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldType.DATE_TIME,
|
||||
universalIdentifier: 'e2a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: 'fe2cec74-a43b-5910-8dd6-7a737fb869ae',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
@@ -957,82 +861,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'f485cd3a-4c55-5b8f-b926-a83e5e6ba651',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '729b0d02-9030-5440-bd4e-f34b46d5d031',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '42381c24-6337-5654-8c44-6649fc6b06a8',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '5117f25c-d3a2-533b-b3d6-39a424e88d2a',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '48df9616-8671-522f-87c7-efcd3f2e2604',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '5c8eb640-8c91-5712-8b73-85ce5e71abb9',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '5a5800dd-743f-533d-ad5c-f983a4bb6324',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: 'c4d0beaf-9035-514b-977c-5059b544c1bb',
|
||||
},
|
||||
{
|
||||
description: 'Post Card Recipients tied to the PostCardRecipient',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
@@ -1186,16 +1014,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldType.DATE_TIME,
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: 'e61745a2-969c-53d1-a809-6c583e9192d5',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
@@ -1206,82 +1024,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: '3e14e30f-8271-5a13-9c1f-ce5edb932325',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'b60eaf02-b008-5ebc-8fe5-123f8e92cc9e',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'd76f15e3-e877-5b62-af97-9dac7c71065b',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'a5ec9b5d-90ed-587e-8a9b-cd9d1ba43116',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '4c72ab36-ca4a-5870-b8cc-652385020c45',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '2f481c85-025c-51cf-bccd-3375d252bf26',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: 'e1178a62-d1a5-5e9c-b730-9118c640711d',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: 'e8569449-73b0-5f19-abc0-f6ad06c40341',
|
||||
},
|
||||
{
|
||||
description: 'Post cards tied to the PostCard',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
@@ -1373,16 +1115,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldType.ADDRESS,
|
||||
universalIdentifier: 'd3a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '6c8d489d-f871-598c-92b7-929b5883ae0f',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
@@ -1393,82 +1125,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'c400ccb8-cd91-5ae1-81e3-2df8b6f2cf53',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '88b7a0a5-f2d2-5dce-89a6-e0f27577dd3f',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '411c5a73-100a-5255-a54d-de20a5c2ae54',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'd3d59a1e-2873-594a-a7de-630cde6980a9',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'f358b82e-1254-581e-8629-4fc4302fdbdf',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '8186fca8-89da-5542-99fe-f0ddf90e1fc1',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '9a81ba2c-e4b2-56f8-a610-015decebd3aa',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: '41eed58f-1016-5483-b3ff-979f270dd9a6',
|
||||
},
|
||||
{
|
||||
description: 'Recipients tied to the Recipient',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
|
||||
-1
@@ -37,7 +37,6 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
||||
"everyEquals",
|
||||
"favoriteRecordIds",
|
||||
"featureFlags",
|
||||
"generateDefaultFieldUniversalIdentifier",
|
||||
"hasAnySoftDeleteFilterOnView",
|
||||
"includes",
|
||||
"includesEvery",
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
|
||||
import { type ObjectFieldManifest } from 'twenty-shared/application';
|
||||
|
||||
const baseObjectConfig: ObjectConfig = {
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
fields: [],
|
||||
};
|
||||
|
||||
const applicationUniversalIdentifier = 'b7f8a9c0-1d2e-4f40-8a6b-7c8d9e0f1a2b';
|
||||
|
||||
const getMockDefaultFieldsInObjectFields = (objectConfig: ObjectConfig) =>
|
||||
getDefaultFieldsInObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const getMockDefaultObjectFields = (objectConfig: ObjectConfig) =>
|
||||
getDefaultObjectFields({ objectConfig, applicationUniversalIdentifier });
|
||||
|
||||
const getMockDefaultRelationObjectFields = (objectConfig: ObjectConfig) =>
|
||||
getDefaultRelationObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
describe('getDefaultFieldsInObjectFields', () => {
|
||||
it('should return all default fields when objectConfig has no fields', () => {
|
||||
const { objectFields, fields } =
|
||||
getMockDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const defaultFields = getMockDefaultObjectFields(baseObjectConfig);
|
||||
const {
|
||||
objectFields: defaultRelationObjectFields,
|
||||
fields: expectedReverseFields,
|
||||
} = getMockDefaultRelationObjectFields(baseObjectConfig);
|
||||
|
||||
expect(objectFields).toEqual([
|
||||
...defaultFields,
|
||||
...defaultRelationObjectFields,
|
||||
]);
|
||||
expect(fields).toEqual(expectedReverseFields);
|
||||
|
||||
for (const field of fields) {
|
||||
if (field.type === FieldMetadataType.RELATION) {
|
||||
expect(field.isNullable).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
for (const objectField of objectFields) {
|
||||
if (objectField.type === FieldMetadataType.RELATION) {
|
||||
expect(objectField.isNullable).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve custom fields and append missing default fields', () => {
|
||||
const customField: ObjectFieldManifest = {
|
||||
universalIdentifier: '11111111-1111-1111-1111-111111111111',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getMockDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields } =
|
||||
getMockDefaultRelationObjectFields(objectConfig);
|
||||
|
||||
expect(objectFields[0]).toEqual(customField);
|
||||
expect(objectFields).toHaveLength(
|
||||
1 + defaultFields.length + defaultRelationObjectFields.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not inject a default field when a field with the same name exists', () => {
|
||||
const customIdField: ObjectFieldManifest = {
|
||||
universalIdentifier: '22222222-2222-2222-2222-222222222222',
|
||||
name: 'id',
|
||||
label: 'Custom Id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customIdField],
|
||||
};
|
||||
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
const idFields = objectFields.filter((f) => f.name === 'id');
|
||||
|
||||
expect(idFields).toHaveLength(1);
|
||||
expect(idFields[0]).toEqual(customIdField);
|
||||
});
|
||||
|
||||
it('should skip multiple default fields when overridden by custom fields', () => {
|
||||
const customFields: ObjectFieldManifest[] = [
|
||||
{
|
||||
universalIdentifier: '33333333-3333-3333-3333-333333333333',
|
||||
name: 'id',
|
||||
label: 'Custom Id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '44444444-4444-4444-4444-444444444444',
|
||||
name: 'name',
|
||||
label: 'Custom Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '55555555-5555-5555-5555-555555555555',
|
||||
name: 'position',
|
||||
label: 'Custom Position',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
},
|
||||
];
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: customFields,
|
||||
};
|
||||
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getMockDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields } =
|
||||
getMockDefaultRelationObjectFields(objectConfig);
|
||||
const overriddenCount = defaultFields.filter((df) =>
|
||||
customFields.some((cf) => cf.name === df.name),
|
||||
).length;
|
||||
|
||||
expect(objectFields).toHaveLength(
|
||||
customFields.length +
|
||||
defaultFields.length +
|
||||
defaultRelationObjectFields.length -
|
||||
overriddenCount,
|
||||
);
|
||||
|
||||
expect(objectFields.filter((f) => f.name === 'id')).toHaveLength(1);
|
||||
expect(objectFields.filter((f) => f.name === 'name')).toHaveLength(1);
|
||||
expect(objectFields.filter((f) => f.name === 'position')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should place custom fields before default fields in the result', () => {
|
||||
const customField: ObjectFieldManifest = {
|
||||
universalIdentifier: '66666666-6666-6666-6666-666666666666',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(objectFields[0]).toEqual(customField);
|
||||
});
|
||||
|
||||
it('should not mutate the original objectConfig fields array', () => {
|
||||
const customField: ObjectFieldManifest = {
|
||||
universalIdentifier: '77777777-7777-7777-7777-777777777777',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const originalLength = objectConfig.fields.length;
|
||||
|
||||
getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(objectConfig.fields).toHaveLength(originalLength);
|
||||
});
|
||||
|
||||
it('should return reverse relation fields for each default relation', () => {
|
||||
const { fields } = getMockDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(4);
|
||||
|
||||
const fieldNames = fields.map((f) => f.name);
|
||||
|
||||
expect(fieldNames).toContain('targetTestObject');
|
||||
});
|
||||
});
|
||||
-224
@@ -1,224 +0,0 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
|
||||
const mockObjectConfig: ObjectConfig = {
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
fields: [],
|
||||
};
|
||||
|
||||
const mockApplicationUniversalIdentifier =
|
||||
'3d05dbc0-4bd8-4041-a944-8cd0e26c2be1';
|
||||
|
||||
const getMockDefaultObjectFields = (objectConfig: ObjectConfig) =>
|
||||
getDefaultObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier: mockApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const expectedUniversalId = (fieldName: string) =>
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: mockApplicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: mockObjectConfig.universalIdentifier,
|
||||
fieldName,
|
||||
});
|
||||
|
||||
describe('getDefaultObjectFields', () => {
|
||||
it('should return an array of 9 default fields', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('should include an id field with UUID type', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const idField = fields.find((field) => field.name === 'id');
|
||||
|
||||
expect(idField).toBeDefined();
|
||||
expect(idField).toEqual({
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
defaultValue: 'uuid',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: expectedUniversalId('id'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a name field with TEXT type', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const nameField = fields.find((field) => field.name === 'name');
|
||||
|
||||
expect(nameField).toBeDefined();
|
||||
expect(nameField).toEqual({
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: expectedUniversalId('name'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include createdAt, updatedAt and deletedAt fields with DATE_TIME type', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const createdAtField = fields.find((field) => field.name === 'createdAt');
|
||||
const updatedAtField = fields.find((field) => field.name === 'updatedAt');
|
||||
const deletedAtField = fields.find((field) => field.name === 'deletedAt');
|
||||
|
||||
expect(createdAtField).toBeDefined();
|
||||
expect(createdAtField).toEqual({
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: expectedUniversalId('createdAt'),
|
||||
});
|
||||
|
||||
expect(updatedAtField).toBeDefined();
|
||||
expect(updatedAtField).toEqual({
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: expectedUniversalId('updatedAt'),
|
||||
});
|
||||
|
||||
expect(deletedAtField).toBeDefined();
|
||||
expect(deletedAtField).toEqual({
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: expectedUniversalId('deletedAt'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include createdBy and updatedBy fields with ACTOR type', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const createdByField = fields.find((field) => field.name === 'createdBy');
|
||||
const updatedByField = fields.find((field) => field.name === 'updatedBy');
|
||||
|
||||
expect(createdByField).toBeDefined();
|
||||
expect(createdByField).toEqual({
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: expectedUniversalId('createdBy'),
|
||||
});
|
||||
|
||||
expect(updatedByField).toBeDefined();
|
||||
expect(updatedByField).toEqual({
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: expectedUniversalId('updatedBy'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a position field with POSITION type', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const positionField = fields.find((field) => field.name === 'position');
|
||||
|
||||
expect(positionField).toBeDefined();
|
||||
expect(positionField).toEqual({
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
defaultValue: 0,
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: expectedUniversalId('position'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a searchVector field with TS_VECTOR type', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const searchVectorField = fields.find(
|
||||
(field) => field.name === 'searchVector',
|
||||
);
|
||||
|
||||
expect(searchVectorField).toBeDefined();
|
||||
expect(searchVectorField).toEqual({
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
description: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: expectedUniversalId('searchVector'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate deterministic universalIdentifiers based on objectConfig', () => {
|
||||
const firstResult = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const secondResult = getMockDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
firstResult.forEach((field, index) => {
|
||||
expect(field.universalIdentifier).toBe(
|
||||
secondResult[index].universalIdentifier,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate different universalIdentifiers for different objectConfigs', () => {
|
||||
const otherObjectConfig: ObjectConfig = {
|
||||
...mockObjectConfig,
|
||||
universalIdentifier: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||
};
|
||||
|
||||
const fieldsA = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const fieldsB = getMockDefaultObjectFields(otherObjectConfig);
|
||||
|
||||
fieldsA.forEach((fieldA, index) => {
|
||||
expect(fieldA.universalIdentifier).not.toBe(
|
||||
fieldsB[index].universalIdentifier,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return fields in the expected order', () => {
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const fieldNames = fields.map((field) => field.name);
|
||||
|
||||
expect(fieldNames).toEqual([
|
||||
'id',
|
||||
'name',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
'searchVector',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+34
-11
@@ -1,10 +1,32 @@
|
||||
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
|
||||
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import {
|
||||
type FieldManifest,
|
||||
getFieldUniversalIdentifier,
|
||||
type ObjectFieldManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
const getDefaultNameObjectField = ({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
objectConfig: ObjectConfig;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): ObjectFieldManifest => ({
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectConfig.universalIdentifier,
|
||||
name: 'name',
|
||||
}),
|
||||
});
|
||||
|
||||
export const getDefaultFieldsInObjectFields = ({
|
||||
objectConfig,
|
||||
@@ -13,24 +35,25 @@ export const getDefaultFieldsInObjectFields = ({
|
||||
objectConfig: ObjectConfig;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
const defaultObjectFields = getDefaultObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
const { objectFields: defaultRelationObjectFields, fields: reverseFields } =
|
||||
getDefaultRelationObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name);
|
||||
const objectConfigFieldNames = (objectConfig.fields ?? []).map(
|
||||
(field) => field.name,
|
||||
);
|
||||
|
||||
const objectFieldsWithDefaults = [...objectConfig.fields];
|
||||
|
||||
for (const defaultField of defaultObjectFields) {
|
||||
if (!objectConfigFieldNames.includes(defaultField.name)) {
|
||||
objectFieldsWithDefaults.push(defaultField);
|
||||
}
|
||||
const defaultNameObjectField = getDefaultNameObjectField({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
if (!objectConfigFieldNames.includes(defaultNameObjectField.name)) {
|
||||
objectFieldsWithDefaults.push(defaultNameObjectField);
|
||||
}
|
||||
|
||||
for (const defaultRelationField of defaultRelationObjectFields) {
|
||||
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
import { type ObjectFieldManifest } from 'twenty-shared/application';
|
||||
|
||||
export const getDefaultObjectFields = ({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
objectConfig: ObjectConfig;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): ObjectFieldManifest[] => {
|
||||
const objectUniversalIdentifier = objectConfig.universalIdentifier;
|
||||
|
||||
const idField: ObjectFieldManifest = {
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
defaultValue: 'uuid',
|
||||
type: FieldMetadataType.UUID as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'id',
|
||||
}),
|
||||
};
|
||||
|
||||
const nameField: ObjectFieldManifest = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'name',
|
||||
}),
|
||||
};
|
||||
|
||||
const createdAtField: ObjectFieldManifest = {
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'createdAt',
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedAtField: ObjectFieldManifest = {
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'updatedAt',
|
||||
}),
|
||||
};
|
||||
|
||||
const deletedAtField: ObjectFieldManifest = {
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'deletedAt',
|
||||
}),
|
||||
};
|
||||
|
||||
const createdByField: ObjectFieldManifest = {
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'createdBy',
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedByField: ObjectFieldManifest = {
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'updatedBy',
|
||||
}),
|
||||
};
|
||||
|
||||
const positionField: ObjectFieldManifest = {
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
description: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
defaultValue: 0,
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'position',
|
||||
}),
|
||||
};
|
||||
|
||||
const searchVectorField: ObjectFieldManifest = {
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
description: 'Search vector',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'searchVector',
|
||||
}),
|
||||
};
|
||||
|
||||
return [
|
||||
idField,
|
||||
nameField,
|
||||
createdAtField,
|
||||
updatedAtField,
|
||||
deletedAtField,
|
||||
createdByField,
|
||||
updatedByField,
|
||||
positionField,
|
||||
searchVectorField,
|
||||
];
|
||||
};
|
||||
+11
-13
@@ -1,8 +1,8 @@
|
||||
import { RelationType } from '@/sdk/define';
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
import type { ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import {
|
||||
type FieldManifest,
|
||||
getFieldUniversalIdentifier,
|
||||
type ObjectFieldManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
@@ -132,19 +132,17 @@ export const getDefaultRelationObjectFields = ({
|
||||
for (const config of DEFAULT_RELATION_CONFIGS) {
|
||||
const standardObject = STANDARD_OBJECTS[config.standardObjectKey];
|
||||
|
||||
const forwardFieldUniversalIdentifier =
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectConfig.universalIdentifier,
|
||||
fieldName: config.fieldName,
|
||||
});
|
||||
const forwardFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectConfig.universalIdentifier,
|
||||
name: config.fieldName,
|
||||
});
|
||||
|
||||
const reverseFieldUniversalIdentifier =
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: standardObject.universalIdentifier,
|
||||
fieldName: config.targetFieldName(objectConfig),
|
||||
});
|
||||
const reverseFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: standardObject.universalIdentifier,
|
||||
name: config.targetFieldName(objectConfig),
|
||||
});
|
||||
|
||||
const forwardField: ObjectFieldManifest = {
|
||||
name: config.fieldName,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'app-abc-123';
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
|
||||
const getTestViewBaseFile = (
|
||||
overrides: Omit<
|
||||
@@ -127,10 +128,11 @@ describe('getViewBaseFile', () => {
|
||||
expect(matches!.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should emit a generateDefaultFieldUniversalIdentifier call for fields using defaultFieldName', () => {
|
||||
it('should resolve a deterministic universal identifier literal for fields using defaultFieldName', () => {
|
||||
const objectUniversalIdentifier = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-default-field',
|
||||
objectUniversalIdentifier: 'obj-abc-123',
|
||||
objectUniversalIdentifier,
|
||||
fields: [
|
||||
{
|
||||
defaultFieldName: 'createdAt',
|
||||
@@ -139,18 +141,20 @@ describe('getViewBaseFile', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const expectedFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier,
|
||||
name: 'createdAt',
|
||||
});
|
||||
|
||||
expect(result).toContain("import { defineView } from 'twenty-sdk/define';");
|
||||
expect(result).not.toContain('generateDefaultFieldUniversalIdentifier');
|
||||
expect(result).toContain(
|
||||
"import {\n defineView,\n generateDefaultFieldUniversalIdentifier,\n} from 'twenty-sdk/define';",
|
||||
`fieldMetadataUniversalIdentifier: '${expectedFieldUniversalIdentifier}'`,
|
||||
);
|
||||
expect(result).toContain(
|
||||
'fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier({',
|
||||
);
|
||||
expect(result).toContain("applicationUniversalIdentifier: 'app-abc-123'");
|
||||
expect(result).toContain("objectUniversalIdentifier: 'obj-abc-123'");
|
||||
expect(result).toContain("fieldName: 'createdAt'");
|
||||
});
|
||||
|
||||
it('should not import generateDefaultFieldUniversalIdentifier when no field uses defaultFieldName', () => {
|
||||
it('should not import any helper when no field uses defaultFieldName', () => {
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-literal-only',
|
||||
fields: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { kebabCase } from '@/cli/utilities/string/kebab-case';
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { type ViewType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -29,14 +30,16 @@ const renderFieldEntry = ({
|
||||
const isVisible = field.isVisible ?? true;
|
||||
const size = field.size ?? 200;
|
||||
|
||||
const fieldMetadataUniversalIdentifierLine =
|
||||
const resolvedFieldMetadataUniversalIdentifier =
|
||||
'defaultFieldName' in field
|
||||
? ` fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: '${applicationUniversalIdentifier}',
|
||||
objectUniversalIdentifier: '${objectUniversalIdentifier}',
|
||||
fieldName: '${field.defaultFieldName}',
|
||||
})`
|
||||
: ` fieldMetadataUniversalIdentifier: '${field.fieldMetadataUniversalIdentifier}'`;
|
||||
? getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: field.defaultFieldName,
|
||||
})
|
||||
: field.fieldMetadataUniversalIdentifier;
|
||||
|
||||
const fieldMetadataUniversalIdentifierLine = ` fieldMetadataUniversalIdentifier: '${resolvedFieldMetadataUniversalIdentifier}'`;
|
||||
|
||||
return ` {
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
@@ -64,10 +67,6 @@ export const getViewBaseFile = ({
|
||||
}) => {
|
||||
const kebabCaseName = kebabCase(name);
|
||||
|
||||
const hasDefaultFieldEntry = fields.some(
|
||||
(field) => 'defaultFieldName' in field,
|
||||
);
|
||||
|
||||
const defaultFields = ` // fields: [
|
||||
// {
|
||||
// universalIdentifier: '...',
|
||||
@@ -95,12 +94,7 @@ ${fields
|
||||
|
||||
const typeBlock = type !== undefined ? ` type: '${type}',\n` : '';
|
||||
|
||||
const imports = hasDefaultFieldEntry
|
||||
? `import {
|
||||
defineView,
|
||||
generateDefaultFieldUniversalIdentifier,
|
||||
} from 'twenty-sdk/define';`
|
||||
: `import { defineView } from 'twenty-sdk/define';`;
|
||||
const imports = `import { defineView } from 'twenty-sdk/define';`;
|
||||
|
||||
return `${imports}
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ export { defineConnectionProvider } from '@/sdk/define/connection-providers/defi
|
||||
export { defineNavigationMenuItem } from '@/sdk/define/navigation-menu-items/define-navigation-menu-item';
|
||||
|
||||
export { defineObject } from '@/sdk/define/objects/define-object';
|
||||
export { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
export {
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS as STANDARD_OBJECT,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
describe('generateDefaultFieldUniversalIdentifier', () => {
|
||||
const applicationUniversalIdentifier = 'c6061e2c-7b5c-4a63-b6a4-6f2ef2f2fefb';
|
||||
const objectUniversalIdentifier = '55b79f88-4094-4b3f-a0ac-1a91a55714f2';
|
||||
|
||||
it('should generate a unique universal identifier', () => {
|
||||
const uId1 = generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'id',
|
||||
});
|
||||
const uId2 = generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'id',
|
||||
});
|
||||
|
||||
const anotherUId = generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'name',
|
||||
});
|
||||
|
||||
expect(uId1).toEqual(uId2);
|
||||
expect(uId1).not.toEqual(anotherUId);
|
||||
});
|
||||
|
||||
it('should match the shared getFieldUniversalIdentifier derivation', () => {
|
||||
const universalIdentifier = generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'createdAt',
|
||||
});
|
||||
|
||||
expect(universalIdentifier).toEqual(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: 'createdAt',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate different identifiers for different applications', () => {
|
||||
const otherApplicationUId = generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: '0b04e15c-27b2-4741-9046-b32e07469072',
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'id',
|
||||
});
|
||||
|
||||
expect(otherApplicationUId).not.toEqual(
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'id',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
export const generateDefaultFieldUniversalIdentifier = ({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
objectUniversalIdentifier: string;
|
||||
fieldName: string;
|
||||
}) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: fieldName,
|
||||
});
|
||||
+23
-16
@@ -5,15 +5,16 @@ import { buildSearchFieldMetadataBackfillOperations } from 'src/database/command
|
||||
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 { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
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';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
const CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER = 'custom-application-uid';
|
||||
const CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'c0c1c2c3-c4c5-4000-8000-000000000001';
|
||||
const CUSTOM_APPLICATION_ID = 'custom-application-id';
|
||||
|
||||
const buildUniversalIdentifiersByApplicationId = (
|
||||
@@ -129,6 +130,7 @@ const buildSearchFieldMetadata = ({
|
||||
applicationId: 'unused-application-id',
|
||||
applicationUniversalIdentifier,
|
||||
position,
|
||||
isSystemSideEffect: true,
|
||||
workspaceId: 'workspace-id',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
@@ -221,8 +223,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
searchVectorField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps(
|
||||
[],
|
||||
),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
@@ -288,8 +291,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([junctionObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([idField]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps(
|
||||
[],
|
||||
),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
@@ -592,8 +596,7 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
fieldMetadataId: nameField.id,
|
||||
objectMetadataUniversalIdentifier: customObject.universalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: nameField.universalIdentifier,
|
||||
applicationUniversalIdentifier:
|
||||
CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
const { flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier } =
|
||||
@@ -606,8 +609,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
existingSearchFieldMetadata,
|
||||
]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps(
|
||||
[],
|
||||
),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
@@ -650,8 +654,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([customObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([relationNameField]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps(
|
||||
[],
|
||||
),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
@@ -685,8 +690,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
nameDescriptionField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps(
|
||||
[],
|
||||
),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
@@ -716,8 +722,9 @@ describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
nameDescriptionField,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps:
|
||||
buildFlatSearchFieldMetadataMaps([]),
|
||||
standardFlatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps(
|
||||
[],
|
||||
),
|
||||
customApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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';
|
||||
|
||||
// searchFieldMetadata rows are always system-derived (never user-authored), so the
|
||||
// new column defaults to true, which also backfills every existing row correctly.
|
||||
@RegisteredInstanceCommand('2.20.0', 1783580127637)
|
||||
export class AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."searchFieldMetadata" ADD COLUMN IF NOT EXISTS "isSystemSideEffect" boolean NOT NULL DEFAULT true',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."searchFieldMetadata" DROP COLUMN IF EXISTS "isSystemSideEffect"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
|
||||
// The default `name` field used to be provisioned with isSystemSideEffect: true
|
||||
// (2.15 → 2.19). It is now a caller-provided default (isSystemSideEffect: false),
|
||||
// like every other user-owned field, so existing rows must be re-flagged.
|
||||
//
|
||||
// Scoping by name alone is safe: no engine-owned field is named `name` (the
|
||||
// reserved set is id/createdAt/updatedAt/deletedAt/createdBy/updatedBy/position/
|
||||
// searchVector), and API-created custom fields are always false already.
|
||||
//
|
||||
// This is a pure data backfill, so the write lives in runDataMigration() (slow
|
||||
// instance command) rather than up(): keeping the bulk UPDATE out of the fast
|
||||
// schema transaction avoids holding an ACCESS EXCLUSIVE lock that could stall
|
||||
// reads during the deploy. Slow instance commands still run before every
|
||||
// workspace command of the version (order is fast → slow → workspace), so the
|
||||
// fresh value is in place before the 2.20 search reconcile workspace commands
|
||||
// flush and recompute the fieldMetadata flat-entity cache (the UPDATE itself
|
||||
// does not invalidate the per-workspace cache).
|
||||
@RegisteredInstanceCommand('2.20.0', 1783529458168, { type: 'slow' })
|
||||
export class BackfillNameFieldIsSystemSideEffectSlowInstanceCommand implements SlowInstanceCommand {
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
await dataSource.query(
|
||||
`UPDATE "core"."fieldMetadata" SET "isSystemSideEffect" = false WHERE "name" = 'name' AND "isSystemSideEffect" = true`,
|
||||
);
|
||||
}
|
||||
|
||||
public async up(_queryRunner: QueryRunner): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intentional no-op: this backfill cannot be safely reversed. Pre-2.15 `name`
|
||||
public async down(_queryRunner: QueryRunner): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+19
-5
@@ -1,30 +1,44 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillActorSourceEnumValuesCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783499671542-backfill-actor-source-enum-values.command';
|
||||
import { BackfillWorkflowVersionToCoreCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783526282685-backfill-workflow-version-to-core.command';
|
||||
import { AddMessageCampaignStatFieldsCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783525261000-add-message-campaign-stat-fields.command';
|
||||
import { CreateMessageListViewCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783525261001-create-message-list-view.command';
|
||||
import { BackfillWorkflowVersionToCoreCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783526282685-backfill-workflow-version-to-core.command';
|
||||
import { ReconcileSearchVectorGinIndexUniversalIdentifierCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783529458169-reconcile-search-vector-gin-index-universal-identifier.command';
|
||||
import { ReconcileSearchFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783529458170-reconcile-search-field-metadata.command';
|
||||
import { RebuildInstalledAppSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-workspace-command-1783529458171-rebuild-installed-app-search-vectors.command';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkflowVersionCoreModule } from 'src/engine/core-modules/workflow/workflow-version-core.module';
|
||||
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 { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ApplicationModule,
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
TypeOrmModule.forFeature([IndexMetadataEntity, SearchFieldMetadataEntity]),
|
||||
WorkflowVersionCoreModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
WorkspaceSchemaManagerModule,
|
||||
],
|
||||
providers: [
|
||||
AddMessageCampaignStatFieldsCommand,
|
||||
CreateMessageListViewCommand,
|
||||
BackfillActorSourceEnumValuesCommand,
|
||||
BackfillWorkflowVersionToCoreCommand,
|
||||
ReconcileSearchVectorGinIndexUniversalIdentifierCommand,
|
||||
ReconcileSearchFieldMetadataCommand,
|
||||
RebuildInstalledAppSearchVectorsCommand,
|
||||
],
|
||||
})
|
||||
export class V2_20_UpgradeVersionCommandModule {}
|
||||
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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 { buildSearchVectorGinIndexBackfillOperations } from 'src/database/commands/upgrade-version-command/2-20/utils/build-search-vector-gin-index-backfill-operations.util';
|
||||
import {
|
||||
buildSearchVectorGinIndexReOwnOperations,
|
||||
type SearchVectorGinIndexUniversalIdentifierUpdate,
|
||||
} from 'src/database/commands/upgrade-version-command/2-20/utils/build-search-vector-gin-index-re-own-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 { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { getMetadataSerializedRelationNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
type SearchVectorGinIndexReconciliationOperations = {
|
||||
indexUniversalIdentifierUpdates: SearchVectorGinIndexUniversalIdentifierUpdate[];
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier: Record<
|
||||
string,
|
||||
UniversalFlatIndexMetadata[]
|
||||
>;
|
||||
};
|
||||
|
||||
@RegisteredWorkspaceCommand('2.20.0', 1783529458169)
|
||||
@Command({
|
||||
name: 'upgrade:2-20:reconcile-search-vector-gin-index-universal-identifier',
|
||||
description:
|
||||
'Converge every searchVector GIN index universal identifier to its deterministic derivation (re-own, all applications) and create the missing GIN index for installed-app objects (backfill). Idempotent, re-own runs before backfill.',
|
||||
})
|
||||
export class ReconcileSearchVectorGinIndexUniversalIdentifierCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
// eslint-disable-next-line twenty/prefer-workspace-scoped-repository
|
||||
@InjectRepository(IndexMetadataEntity)
|
||||
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const {
|
||||
indexUniversalIdentifierUpdates,
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
} = await this.computeOperations({ workspaceId });
|
||||
|
||||
const totalIndexesToBackfill = Object.values(
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
).reduce((total, flatIndexes) => total + flatIndexes.length, 0);
|
||||
|
||||
if (
|
||||
indexUniversalIdentifierUpdates.length === 0 &&
|
||||
totalIndexesToBackfill === 0
|
||||
) {
|
||||
this.logger.log(
|
||||
`No searchVector GIN index to reconcile for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Reconciling ${indexUniversalIdentifierUpdates.length} searchVector GIN index universal identifier(s) and creating ${totalIndexesToBackfill} missing index(es) across ${Object.keys(flatIndexesToCreateByApplicationUniversalIdentifier).length} application(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applyOperations({
|
||||
workspaceId,
|
||||
indexUniversalIdentifierUpdates,
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Reconciled searchVector GIN indexes for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async computeOperations({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<SearchVectorGinIndexReconciliationOperations> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
flatApplicationMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatIndexMaps',
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const applicationUniversalIdentifierById = new Map(
|
||||
Object.values(flatApplicationMaps.byId)
|
||||
.filter(isDefined)
|
||||
.map((flatApplication) => [
|
||||
flatApplication.id,
|
||||
flatApplication.universalIdentifier,
|
||||
]),
|
||||
);
|
||||
|
||||
const indexUniversalIdentifierUpdates =
|
||||
buildSearchVectorGinIndexReOwnOperations({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
applicationUniversalIdentifierById,
|
||||
});
|
||||
|
||||
const flatIndexesToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchVectorGinIndexBackfillOperations({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
return {
|
||||
indexUniversalIdentifierUpdates,
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
private async applyOperations({
|
||||
workspaceId,
|
||||
indexUniversalIdentifierUpdates,
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
} & SearchVectorGinIndexReconciliationOperations): Promise<void> {
|
||||
// Re-own before backfill: a deterministic index created next to a surviving legacy
|
||||
// row would collide on the unique universal identifier constraint.
|
||||
await this.applyReOwn({ workspaceId, indexUniversalIdentifierUpdates });
|
||||
await this.applyBackfill({
|
||||
workspaceId,
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
private async applyReOwn({
|
||||
workspaceId,
|
||||
indexUniversalIdentifierUpdates,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
indexUniversalIdentifierUpdates: SearchVectorGinIndexUniversalIdentifierUpdate[];
|
||||
}): Promise<void> {
|
||||
if (indexUniversalIdentifierUpdates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.indexMetadataRepository.manager.transaction(
|
||||
async (entityManager) => {
|
||||
const transactionalIndexMetadataRepository =
|
||||
entityManager.getRepository(IndexMetadataEntity);
|
||||
|
||||
for (const {
|
||||
id,
|
||||
deterministicUniversalIdentifier,
|
||||
} of indexUniversalIdentifierUpdates) {
|
||||
await transactionalIndexMetadataRepository.update(
|
||||
{ id, workspaceId },
|
||||
{ universalIdentifier: deterministicUniversalIdentifier },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
// Abort before backfill: creating a deterministic index next to a surviving legacy
|
||||
// row (re-own rolled back) would collide on the unique universal identifier
|
||||
// constraint.
|
||||
this.logger.error(
|
||||
`Failed to re-own ${indexUniversalIdentifierUpdates.length} searchVector GIN index universal identifier(s) for workspace ${workspaceId}, aborting: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.flushIndexCacheAndBumpMetadataVersion(workspaceId);
|
||||
}
|
||||
|
||||
private async applyBackfill({
|
||||
workspaceId,
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier: Record<
|
||||
string,
|
||||
UniversalFlatIndexMetadata[]
|
||||
>;
|
||||
}): Promise<void> {
|
||||
for (const [
|
||||
applicationUniversalIdentifier,
|
||||
flatIndexesToCreate,
|
||||
] of Object.entries(flatIndexesToCreateByApplicationUniversalIdentifier)) {
|
||||
if (flatIndexesToCreate.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
index: {
|
||||
flatEntityToCreate: flatIndexesToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create searchVector GIN index(es) for application ${applicationUniversalIdentifier}:\n${JSON.stringify(
|
||||
validateAndBuildResult,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to create searchVector GIN index(es) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async flushIndexCacheAndBumpMetadataVersion(
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const indexRelatedMetadataNames = [
|
||||
'index',
|
||||
...getMetadataRelatedMetadataNames('index'),
|
||||
...getMetadataSerializedRelationNames('index'),
|
||||
] as const;
|
||||
const cacheKeysToFlush = [
|
||||
...new Set(indexRelatedMetadataNames.map(getMetadataFlatEntityMapsKey)),
|
||||
];
|
||||
|
||||
await this.workspaceCacheService.flush(workspaceId, cacheKeysToFlush);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
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-20/utils/build-search-field-metadata-backfill-operations.util';
|
||||
import {
|
||||
buildSearchFieldMetadataReOwnOperations,
|
||||
type SearchFieldMetadataUniversalIdentifierUpdate,
|
||||
} from 'src/database/commands/upgrade-version-command/2-20/utils/build-search-field-metadata-re-own-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 { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
|
||||
import { getMetadataSerializedRelationNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-serialized-relation-names.util';
|
||||
import { SearchFieldMetadataEntity } from 'src/engine/metadata-modules/search-field-metadata/search-field-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
type SearchFieldMetadataReconciliationOperations = {
|
||||
searchFieldMetadataUniversalIdentifierUpdates: SearchFieldMetadataUniversalIdentifierUpdate[];
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier: Record<
|
||||
string,
|
||||
UniversalFlatSearchFieldMetadata[]
|
||||
>;
|
||||
};
|
||||
|
||||
@RegisteredWorkspaceCommand('2.20.0', 1783529458170)
|
||||
@Command({
|
||||
name: 'upgrade:2-20:reconcile-search-field-metadata',
|
||||
description:
|
||||
'Converge every searchFieldMetadata universal identifier to its deterministic derivation (re-own, all applications) and create the missing searchFieldMetadata row for installed-app searchable objects (backfill). Idempotent, re-own runs before backfill to avoid a unique-identifier collision.',
|
||||
})
|
||||
export class ReconcileSearchFieldMetadataCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
// eslint-disable-next-line twenty/prefer-workspace-scoped-repository
|
||||
@InjectRepository(SearchFieldMetadataEntity)
|
||||
private readonly searchFieldMetadataRepository: Repository<SearchFieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const {
|
||||
searchFieldMetadataUniversalIdentifierUpdates,
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
} = await this.computeOperations({ workspaceId });
|
||||
|
||||
const totalRowsToBackfill = Object.values(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
).reduce(
|
||||
(total, flatSearchFieldMetadatas) =>
|
||||
total + flatSearchFieldMetadatas.length,
|
||||
0,
|
||||
);
|
||||
|
||||
if (
|
||||
searchFieldMetadataUniversalIdentifierUpdates.length === 0 &&
|
||||
totalRowsToBackfill === 0
|
||||
) {
|
||||
this.logger.log(
|
||||
`No searchFieldMetadata to reconcile for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Reconciling ${searchFieldMetadataUniversalIdentifierUpdates.length} searchFieldMetadata universal identifier(s) and creating ${totalRowsToBackfill} missing row(s) across ${Object.keys(flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier).length} application(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applyOperations({
|
||||
workspaceId,
|
||||
searchFieldMetadataUniversalIdentifierUpdates,
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Reconciled searchFieldMetadata for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async computeOperations({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<SearchFieldMetadataReconciliationOperations> {
|
||||
const {
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
flatApplicationMaps,
|
||||
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatSearchFieldMetadataMaps',
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const applicationUniversalIdentifierById = new Map(
|
||||
Object.values(flatApplicationMaps.byId)
|
||||
.filter(isDefined)
|
||||
.map((flatApplication) => [
|
||||
flatApplication.id,
|
||||
flatApplication.universalIdentifier,
|
||||
]),
|
||||
);
|
||||
|
||||
const searchFieldMetadataUniversalIdentifierUpdates =
|
||||
buildSearchFieldMetadataReOwnOperations({
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
applicationUniversalIdentifierById,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
return {
|
||||
searchFieldMetadataUniversalIdentifierUpdates,
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
private async applyOperations({
|
||||
workspaceId,
|
||||
searchFieldMetadataUniversalIdentifierUpdates,
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
} & SearchFieldMetadataReconciliationOperations): Promise<void> {
|
||||
// Re-own before backfill: a deterministic row created next to a surviving legacy v4
|
||||
// row (which the deletion-inference short-circuit keeps alive) would collide on the
|
||||
// unique universal identifier constraint.
|
||||
await this.applyReOwn({
|
||||
workspaceId,
|
||||
searchFieldMetadataUniversalIdentifierUpdates,
|
||||
});
|
||||
await this.applyBackfill({
|
||||
workspaceId,
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
private async applyReOwn({
|
||||
workspaceId,
|
||||
searchFieldMetadataUniversalIdentifierUpdates,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
searchFieldMetadataUniversalIdentifierUpdates: SearchFieldMetadataUniversalIdentifierUpdate[];
|
||||
}): Promise<void> {
|
||||
if (searchFieldMetadataUniversalIdentifierUpdates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.searchFieldMetadataRepository.manager.transaction(
|
||||
async (entityManager) => {
|
||||
const transactionalSearchFieldMetadataRepository =
|
||||
entityManager.getRepository(SearchFieldMetadataEntity);
|
||||
|
||||
for (const {
|
||||
id,
|
||||
deterministicUniversalIdentifier,
|
||||
} of searchFieldMetadataUniversalIdentifierUpdates) {
|
||||
await transactionalSearchFieldMetadataRepository.update(
|
||||
{ id, workspaceId },
|
||||
{ universalIdentifier: deterministicUniversalIdentifier },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
// Abort before backfill: creating a deterministic row next to a surviving legacy
|
||||
// row (re-own rolled back) would collide on the unique universal identifier
|
||||
// constraint.
|
||||
this.logger.error(
|
||||
`Failed to re-own ${searchFieldMetadataUniversalIdentifierUpdates.length} searchFieldMetadata universal identifier(s) for workspace ${workspaceId}, aborting: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await this.flushSearchFieldMetadataCacheAndBumpMetadataVersion(workspaceId);
|
||||
}
|
||||
|
||||
private async applyBackfill({
|
||||
workspaceId,
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier: Record<
|
||||
string,
|
||||
UniversalFlatSearchFieldMetadata[]
|
||||
>;
|
||||
}): Promise<void> {
|
||||
for (const [
|
||||
applicationUniversalIdentifier,
|
||||
flatSearchFieldMetadatasToCreate,
|
||||
] of Object.entries(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
)) {
|
||||
if (flatSearchFieldMetadatasToCreate.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
isSystemBuild: true,
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: flatSearchFieldMetadatasToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
this.logger.error(
|
||||
`Failed to create searchFieldMetadata row(s) for application ${applicationUniversalIdentifier}:\n${JSON.stringify(
|
||||
validateAndBuildResult,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Failed to create searchFieldMetadata row(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async flushSearchFieldMetadataCacheAndBumpMetadataVersion(
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const searchFieldMetadataRelatedMetadataNames = [
|
||||
'searchFieldMetadata',
|
||||
...getMetadataRelatedMetadataNames('searchFieldMetadata'),
|
||||
...getMetadataSerializedRelationNames('searchFieldMetadata'),
|
||||
] as const;
|
||||
const cacheKeysToFlush = [
|
||||
...new Set(
|
||||
searchFieldMetadataRelatedMetadataNames.map(getMetadataFlatEntityMapsKey),
|
||||
),
|
||||
];
|
||||
|
||||
await this.workspaceCacheService.flush(workspaceId, cacheKeysToFlush);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { Command } from 'nest-commander';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
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 { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { type UniversalUpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/types/workspace-migration-field-action';
|
||||
import { WORKSPACE_MIGRATION_ACTION_TYPE } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/constants/workspace-migration-action-type.constant';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
|
||||
@RegisteredWorkspaceCommand('2.20.0', 1783529458171)
|
||||
@Command({
|
||||
name: 'upgrade:2-20:rebuild-installed-app-search-vectors',
|
||||
description:
|
||||
'Rebuild the searchVector column of every installed-app TS_VECTOR field from its searchFieldMetadata rows, now that the GIN index and searchFieldMetadata rows exist (commands 1 and 2). Idempotent.',
|
||||
})
|
||||
export class RebuildInstalledAppSearchVectorsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
]);
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const installedAppTsVectorFlatFieldMetadatas = Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(flatFieldMetadata) =>
|
||||
flatFieldMetadata.type === FieldMetadataType.TS_VECTOR &&
|
||||
flatFieldMetadata.applicationId !==
|
||||
twentyStandardFlatApplication.id &&
|
||||
flatFieldMetadata.applicationId !== workspaceCustomFlatApplication.id,
|
||||
);
|
||||
|
||||
if (installedAppTsVectorFlatFieldMetadatas.length === 0) {
|
||||
this.logger.log(
|
||||
`No installed-app searchVector to rebuild for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Rebuilding ${installedAppTsVectorFlatFieldMetadatas.length} installed-app searchVector(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions: UniversalUpdateFieldAction[] =
|
||||
installedAppTsVectorFlatFieldMetadatas.map((flatFieldMetadata) => ({
|
||||
type: WORKSPACE_MIGRATION_ACTION_TYPE.update,
|
||||
metadataName: 'fieldMetadata',
|
||||
universalIdentifier: flatFieldMetadata.universalIdentifier,
|
||||
update: {},
|
||||
rebuildSearchVector: true,
|
||||
}));
|
||||
|
||||
await this.workspaceMigrationRunnerService.run({
|
||||
workspaceMigration: {
|
||||
// Cross-app actions; this is only the runner's existence gate, not a scope filter.
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier,
|
||||
actions,
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Rebuilt ${installedAppTsVectorFlatFieldMetadatas.length} installed-app searchVector(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const ADD_IS_SYSTEM_SIDE_EFFECT_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME =
|
||||
'2.20.0_AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand_1783580127637';
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
getSearchFieldUniversalIdentifier,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { buildSearchFieldMetadataBackfillOperations } from 'src/database/commands/upgrade-version-command/2-20/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 } 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';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
// Application universal identifiers must be valid UUIDs: they are used as the v5
|
||||
// namespace by the deterministic identifier helpers.
|
||||
const STANDARD_APPLICATION_ID = 'standard-application-id';
|
||||
const STANDARD_APPLICATION_UID = '11111111-1111-4111-8111-111111111111';
|
||||
const CUSTOM_APPLICATION_ID = 'custom-application-id';
|
||||
const CUSTOM_APPLICATION_UID = '22222222-2222-4222-8222-222222222222';
|
||||
const INSTALLED_APPLICATION_ID = 'installed-application-id';
|
||||
const INSTALLED_APPLICATION_UID = '33333333-3333-4333-8333-333333333333';
|
||||
|
||||
const applicationUniversalIdentifierById = new Map([
|
||||
[STANDARD_APPLICATION_ID, STANDARD_APPLICATION_UID],
|
||||
[CUSTOM_APPLICATION_ID, CUSTOM_APPLICATION_UID],
|
||||
[INSTALLED_APPLICATION_ID, INSTALLED_APPLICATION_UID],
|
||||
]);
|
||||
|
||||
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: {},
|
||||
});
|
||||
|
||||
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 buildFlatSearchFieldMetadata = ({
|
||||
id,
|
||||
universalIdentifier,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): FlatSearchFieldMetadata => {
|
||||
const createdAt = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
return {
|
||||
id,
|
||||
universalIdentifier,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
objectMetadataUniversalIdentifier: `${objectMetadataId}-uid`,
|
||||
fieldMetadataUniversalIdentifier: `${fieldMetadataId}-uid`,
|
||||
tsVectorFieldMetadataId: `${objectMetadataId}-search-vector-field-id`,
|
||||
tsVectorFieldMetadataUniversalIdentifier: `${objectMetadataId}-search-vector-field-uid`,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
position: 0,
|
||||
isSystemSideEffect: true,
|
||||
workspaceId: 'workspace-id',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
const buildSearchableObjectFixture = ({
|
||||
objectId,
|
||||
objectUniversalIdentifier,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldType = FieldMetadataType.TEXT,
|
||||
}: {
|
||||
objectId: string;
|
||||
objectUniversalIdentifier: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
labelIdentifierFieldType?: FieldMetadataType;
|
||||
}) => {
|
||||
const nameFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: `${objectId}-name-field-id`,
|
||||
universalIdentifier: `${objectUniversalIdentifier}-name-field-uid`,
|
||||
objectMetadataId: objectId,
|
||||
objectMetadataUniversalIdentifier: objectUniversalIdentifier,
|
||||
type: labelIdentifierFieldType,
|
||||
name: 'name',
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const searchVectorFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: `${objectId}-search-vector-field-id`,
|
||||
universalIdentifier: `${objectUniversalIdentifier}-search-vector-field-uid`,
|
||||
objectMetadataId: objectId,
|
||||
objectMetadataUniversalIdentifier: objectUniversalIdentifier,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const flatObjectMetadata = getFlatObjectMetadataMock({
|
||||
id: objectId,
|
||||
universalIdentifier: objectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataId: nameFlatFieldMetadata.id,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
fieldUniversalIdentifiers: [
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
searchVectorFlatFieldMetadata.universalIdentifier,
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
};
|
||||
};
|
||||
|
||||
describe('buildSearchFieldMetadataBackfillOperations', () => {
|
||||
it('creates the searchFieldMetadata row for an installed-app searchable object that has none, grouped under its application', () => {
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
} = buildSearchableObjectFixture({
|
||||
objectId: 'installed-object-id',
|
||||
objectUniversalIdentifier: 'installed-object-uid',
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
const createdRows =
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier[
|
||||
INSTALLED_APPLICATION_UID
|
||||
];
|
||||
|
||||
expect(createdRows).toHaveLength(1);
|
||||
expect(createdRows?.[0].fieldMetadataUniversalIdentifier).toBe(
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
);
|
||||
expect(createdRows?.[0].objectMetadataUniversalIdentifier).toBe(
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
);
|
||||
expect(createdRows?.[0].tsVectorFieldMetadataUniversalIdentifier).toBe(
|
||||
searchVectorFlatFieldMetadata.universalIdentifier,
|
||||
);
|
||||
expect(createdRows?.[0].universalIdentifier).toBe(
|
||||
getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not create a row for an installed-app junction object whose label identifier is the UUID id field', () => {
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
} = buildSearchableObjectFixture({
|
||||
objectId: 'installed-junction-object-id',
|
||||
objectUniversalIdentifier: 'installed-junction-object-uid',
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
});
|
||||
|
||||
// The label identifier resolves to the derived `id` field: no search surface.
|
||||
const junctionObject: FlatObjectMetadata = {
|
||||
...flatObjectMetadata,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: 'id',
|
||||
}),
|
||||
};
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([junctionObject]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not create a row for an installed-app object whose label identifier is a non-searchable type', () => {
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
} = buildSearchableObjectFixture({
|
||||
objectId: 'installed-relation-object-id',
|
||||
objectUniversalIdentifier: 'installed-relation-object-uid',
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
labelIdentifierFieldType: FieldMetadataType.RELATION,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not create a row for a twenty-standard searchable object that has none', () => {
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
} = buildSearchableObjectFixture({
|
||||
objectId: 'standard-object-id',
|
||||
objectUniversalIdentifier: 'standard-object-uid',
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not create a row for a workspace-custom searchable object that has none', () => {
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
} = buildSearchableObjectFixture({
|
||||
objectId: 'custom-object-id',
|
||||
objectUniversalIdentifier: 'custom-object-uid',
|
||||
applicationId: CUSTOM_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: CUSTOM_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([]),
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('is idempotent: no create when the installed-app searchFieldMetadata row already exists', () => {
|
||||
const {
|
||||
flatObjectMetadata,
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
} = buildSearchableObjectFixture({
|
||||
objectId: 'installed-object-id',
|
||||
objectUniversalIdentifier: 'installed-object-uid',
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const existingSearchFieldMetadata = buildFlatSearchFieldMetadata({
|
||||
id: 'existing-search-field-metadata-id',
|
||||
universalIdentifier: getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
fieldMetadataId: nameFlatFieldMetadata.id,
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchFieldMetadataBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
existingSearchFieldMetadata,
|
||||
]),
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
flatSearchFieldMetadatasToCreateByApplicationUniversalIdentifier,
|
||||
),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import { getSearchFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { buildSearchFieldMetadataReOwnOperations } from 'src/database/commands/upgrade-version-command/2-20/utils/build-search-field-metadata-re-own-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 { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
|
||||
const STANDARD_APPLICATION_ID = 'standard-application-id';
|
||||
// Application universal identifiers must be valid UUIDs: they are used as the v5
|
||||
// namespace by the deterministic identifier helpers.
|
||||
const STANDARD_APPLICATION_UID = '11111111-1111-4111-8111-111111111111';
|
||||
|
||||
const applicationUniversalIdentifierById = new Map([
|
||||
[STANDARD_APPLICATION_ID, STANDARD_APPLICATION_UID],
|
||||
]);
|
||||
|
||||
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 buildFlatSearchFieldMetadata = ({
|
||||
id,
|
||||
universalIdentifier,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
id: string;
|
||||
universalIdentifier: string;
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
}): FlatSearchFieldMetadata => {
|
||||
const createdAt = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
return {
|
||||
id,
|
||||
universalIdentifier,
|
||||
objectMetadataId,
|
||||
fieldMetadataId,
|
||||
objectMetadataUniversalIdentifier: `${objectMetadataId}-uid`,
|
||||
fieldMetadataUniversalIdentifier: `${fieldMetadataId}-uid`,
|
||||
tsVectorFieldMetadataId: `${objectMetadataId}-search-vector-field-id`,
|
||||
tsVectorFieldMetadataUniversalIdentifier: `${objectMetadataId}-search-vector-field-uid`,
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
position: 0,
|
||||
isSystemSideEffect: true,
|
||||
workspaceId: 'workspace-id',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
const nameFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: 'standard-object-name-field-id',
|
||||
universalIdentifier: 'standard-object-name-field-uid',
|
||||
objectMetadataId: 'standard-object-id',
|
||||
objectMetadataUniversalIdentifier: 'standard-object-uid',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
});
|
||||
|
||||
describe('buildSearchFieldMetadataReOwnOperations', () => {
|
||||
it('re-owns an existing searchFieldMetadata carrying a non-deterministic universal identifier (all applications)', () => {
|
||||
const existingSearchFieldMetadata = buildFlatSearchFieldMetadata({
|
||||
id: 'existing-search-field-metadata-id',
|
||||
universalIdentifier: 'legacy-v4-search-field-metadata-uid',
|
||||
objectMetadataId: 'standard-object-id',
|
||||
fieldMetadataId: nameFlatFieldMetadata.id,
|
||||
});
|
||||
|
||||
const searchFieldMetadataUniversalIdentifierUpdates =
|
||||
buildSearchFieldMetadataReOwnOperations({
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
existingSearchFieldMetadata,
|
||||
]),
|
||||
applicationUniversalIdentifierById,
|
||||
});
|
||||
|
||||
expect(searchFieldMetadataUniversalIdentifierUpdates).toEqual([
|
||||
{
|
||||
id: existingSearchFieldMetadata.id,
|
||||
deterministicUniversalIdentifier: getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('is a no-op for a searchFieldMetadata already carrying its deterministic universal identifier', () => {
|
||||
const existingSearchFieldMetadata = buildFlatSearchFieldMetadata({
|
||||
id: 'existing-search-field-metadata-id',
|
||||
universalIdentifier: getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
nameFlatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
objectMetadataId: 'standard-object-id',
|
||||
fieldMetadataId: nameFlatFieldMetadata.id,
|
||||
});
|
||||
|
||||
const searchFieldMetadataUniversalIdentifierUpdates =
|
||||
buildSearchFieldMetadataReOwnOperations({
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
nameFlatFieldMetadata,
|
||||
]),
|
||||
flatSearchFieldMetadataMaps: buildFlatSearchFieldMetadataMaps([
|
||||
existingSearchFieldMetadata,
|
||||
]),
|
||||
applicationUniversalIdentifierById,
|
||||
});
|
||||
|
||||
expect(searchFieldMetadataUniversalIdentifierUpdates).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { buildSearchVectorGinIndexBackfillOperations } from 'src/database/commands/upgrade-version-command/2-20/utils/build-search-vector-gin-index-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 { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock';
|
||||
import {
|
||||
type FlatIndexFieldMetadata,
|
||||
type FlatIndexMetadata,
|
||||
} from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { getFlatObjectMetadataMock } 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 { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
// Application universal identifiers must be valid UUIDs: they are used as the v5
|
||||
// namespace by the deterministic identifier helpers.
|
||||
const STANDARD_APPLICATION_ID = 'standard-application-id';
|
||||
const CUSTOM_APPLICATION_ID = 'custom-application-id';
|
||||
const INSTALLED_APPLICATION_ID = 'installed-application-id';
|
||||
const INSTALLED_APPLICATION_UID = '33333333-3333-4333-8333-333333333333';
|
||||
|
||||
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: {},
|
||||
});
|
||||
|
||||
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 buildFlatIndexMaps = (
|
||||
flatIndexMetadatas: FlatIndexMetadata[],
|
||||
): FlatEntityMaps<FlatIndexMetadata> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatIndexMetadatas.map((flatIndexMetadata) => [
|
||||
flatIndexMetadata.universalIdentifier,
|
||||
flatIndexMetadata,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatIndexMetadatas.map((flatIndexMetadata) => [
|
||||
flatIndexMetadata.id,
|
||||
flatIndexMetadata.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const buildFlatIndexFieldMetadata = ({
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
}): FlatIndexFieldMetadata => {
|
||||
const createdAt = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
return {
|
||||
id: `${fieldMetadataId}-index-field-id`,
|
||||
workspaceId: 'workspace-id',
|
||||
indexMetadataId: 'index-metadata-id',
|
||||
fieldMetadataId,
|
||||
order: 0,
|
||||
subFieldName: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
const buildSearchableObjectFixture = ({
|
||||
objectId,
|
||||
objectUniversalIdentifier,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
objectId: string;
|
||||
objectUniversalIdentifier: string;
|
||||
applicationId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}) => {
|
||||
const searchVectorFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: `${objectId}-search-vector-field-id`,
|
||||
universalIdentifier: `${objectUniversalIdentifier}-search-vector-field-uid`,
|
||||
objectMetadataId: objectId,
|
||||
objectMetadataUniversalIdentifier: objectUniversalIdentifier,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const flatObjectMetadata = getFlatObjectMetadataMock({
|
||||
id: objectId,
|
||||
universalIdentifier: objectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
applicationId,
|
||||
applicationUniversalIdentifier,
|
||||
fieldUniversalIdentifiers: [
|
||||
searchVectorFlatFieldMetadata.universalIdentifier,
|
||||
],
|
||||
});
|
||||
|
||||
return { flatObjectMetadata, searchVectorFlatFieldMetadata };
|
||||
};
|
||||
|
||||
describe('buildSearchVectorGinIndexBackfillOperations', () => {
|
||||
it('creates a GIN searchVector index for an installed-app object that has none, grouped under its application', () => {
|
||||
const { flatObjectMetadata, searchVectorFlatFieldMetadata } =
|
||||
buildSearchableObjectFixture({
|
||||
objectId: 'installed-object-id',
|
||||
objectUniversalIdentifier: 'installed-object-uid',
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const flatIndexesToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchVectorGinIndexBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatIndexMaps: buildFlatIndexMaps([]),
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
const createdIndexes =
|
||||
flatIndexesToCreateByApplicationUniversalIdentifier[
|
||||
INSTALLED_APPLICATION_UID
|
||||
];
|
||||
|
||||
expect(createdIndexes).toHaveLength(1);
|
||||
expect(createdIndexes?.[0].indexType).toBe(IndexType.GIN);
|
||||
expect(createdIndexes?.[0].objectMetadataUniversalIdentifier).toBe(
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
);
|
||||
expect(createdIndexes?.[0].applicationUniversalIdentifier).toBe(
|
||||
INSTALLED_APPLICATION_UID,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not create an index for a twenty-standard object that has none', () => {
|
||||
const { flatObjectMetadata, searchVectorFlatFieldMetadata } =
|
||||
buildSearchableObjectFixture({
|
||||
objectId: 'standard-object-id',
|
||||
objectUniversalIdentifier: 'standard-object-uid',
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: '11111111-1111-4111-8111-111111111111',
|
||||
});
|
||||
|
||||
const flatIndexesToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchVectorGinIndexBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatIndexMaps: buildFlatIndexMaps([]),
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(flatIndexesToCreateByApplicationUniversalIdentifier),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not create an index for a workspace-custom object that has none', () => {
|
||||
const { flatObjectMetadata, searchVectorFlatFieldMetadata } =
|
||||
buildSearchableObjectFixture({
|
||||
objectId: 'custom-object-id',
|
||||
objectUniversalIdentifier: 'custom-object-uid',
|
||||
applicationId: CUSTOM_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: '22222222-2222-4222-8222-222222222222',
|
||||
});
|
||||
|
||||
const flatIndexesToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchVectorGinIndexBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatIndexMaps: buildFlatIndexMaps([]),
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(flatIndexesToCreateByApplicationUniversalIdentifier),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('is idempotent: no create when the installed-app object already has a GIN searchVector index', () => {
|
||||
const { flatObjectMetadata, searchVectorFlatFieldMetadata } =
|
||||
buildSearchableObjectFixture({
|
||||
objectId: 'installed-object-id',
|
||||
objectUniversalIdentifier: 'installed-object-uid',
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const ginIndex = getFlatIndexMetadataMock({
|
||||
id: 'installed-gin-index-id',
|
||||
universalIdentifier: 'installed-gin-index-uid',
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
applicationId: INSTALLED_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: INSTALLED_APPLICATION_UID,
|
||||
name: 'IDX_SEARCH_VECTOR_INSTALLED',
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({
|
||||
fieldMetadataId: searchVectorFlatFieldMetadata.id,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const flatIndexesToCreateByApplicationUniversalIdentifier =
|
||||
buildSearchVectorGinIndexBackfillOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatIndexMaps: buildFlatIndexMaps([ginIndex]),
|
||||
twentyStandardApplicationId: STANDARD_APPLICATION_ID,
|
||||
workspaceCustomApplicationId: CUSTOM_APPLICATION_ID,
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(flatIndexesToCreateByApplicationUniversalIdentifier),
|
||||
).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
import { getIndexUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { buildSearchVectorGinIndexReOwnOperations } from 'src/database/commands/upgrade-version-command/2-20/utils/build-search-vector-gin-index-re-own-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 { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock';
|
||||
import {
|
||||
type FlatIndexFieldMetadata,
|
||||
type FlatIndexMetadata,
|
||||
} from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { getFlatObjectMetadataMock } 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 { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
const STANDARD_APPLICATION_ID = 'standard-application-id';
|
||||
// Application universal identifiers must be valid UUIDs: they are used as the v5
|
||||
// namespace by the deterministic identifier helpers.
|
||||
const STANDARD_APPLICATION_UID = '11111111-1111-4111-8111-111111111111';
|
||||
|
||||
const applicationUniversalIdentifierById = new Map([
|
||||
[STANDARD_APPLICATION_ID, STANDARD_APPLICATION_UID],
|
||||
]);
|
||||
|
||||
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: {},
|
||||
});
|
||||
|
||||
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 buildFlatIndexMaps = (
|
||||
flatIndexMetadatas: FlatIndexMetadata[],
|
||||
): FlatEntityMaps<FlatIndexMetadata> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
flatIndexMetadatas.map((flatIndexMetadata) => [
|
||||
flatIndexMetadata.universalIdentifier,
|
||||
flatIndexMetadata,
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
flatIndexMetadatas.map((flatIndexMetadata) => [
|
||||
flatIndexMetadata.id,
|
||||
flatIndexMetadata.universalIdentifier,
|
||||
]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const buildFlatIndexFieldMetadata = ({
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
}): FlatIndexFieldMetadata => {
|
||||
const createdAt = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
return {
|
||||
id: `${fieldMetadataId}-index-field-id`,
|
||||
workspaceId: 'workspace-id',
|
||||
indexMetadataId: 'index-metadata-id',
|
||||
fieldMetadataId,
|
||||
order: 0,
|
||||
subFieldName: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
const buildSearchableObjectFixture = ({
|
||||
objectId,
|
||||
objectUniversalIdentifier,
|
||||
}: {
|
||||
objectId: string;
|
||||
objectUniversalIdentifier: string;
|
||||
}) => {
|
||||
const searchVectorFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: `${objectId}-search-vector-field-id`,
|
||||
universalIdentifier: `${objectUniversalIdentifier}-search-vector-field-uid`,
|
||||
objectMetadataId: objectId,
|
||||
objectMetadataUniversalIdentifier: objectUniversalIdentifier,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
});
|
||||
|
||||
const flatObjectMetadata = getFlatObjectMetadataMock({
|
||||
id: objectId,
|
||||
universalIdentifier: objectUniversalIdentifier,
|
||||
isSearchable: true,
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
fieldUniversalIdentifiers: [
|
||||
searchVectorFlatFieldMetadata.universalIdentifier,
|
||||
],
|
||||
});
|
||||
|
||||
return { flatObjectMetadata, searchVectorFlatFieldMetadata };
|
||||
};
|
||||
|
||||
describe('buildSearchVectorGinIndexReOwnOperations', () => {
|
||||
it('re-owns an existing GIN searchVector index carrying a non-deterministic universal identifier (all applications)', () => {
|
||||
const { flatObjectMetadata, searchVectorFlatFieldMetadata } =
|
||||
buildSearchableObjectFixture({
|
||||
objectId: 'standard-object-id',
|
||||
objectUniversalIdentifier: 'standard-object-uid',
|
||||
});
|
||||
|
||||
const indexName = 'IDX_SEARCH_VECTOR_STANDARD';
|
||||
const ginIndex = getFlatIndexMetadataMock({
|
||||
id: 'standard-gin-index-id',
|
||||
universalIdentifier: 'legacy-v4-index-uid',
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
name: indexName,
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({
|
||||
fieldMetadataId: searchVectorFlatFieldMetadata.id,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const indexUniversalIdentifierUpdates =
|
||||
buildSearchVectorGinIndexReOwnOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatIndexMaps: buildFlatIndexMaps([ginIndex]),
|
||||
applicationUniversalIdentifierById,
|
||||
});
|
||||
|
||||
expect(indexUniversalIdentifierUpdates).toEqual([
|
||||
{
|
||||
id: ginIndex.id,
|
||||
name: indexName,
|
||||
deterministicUniversalIdentifier: getIndexUniversalIdentifier({
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: indexName,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('is a no-op for a GIN searchVector index already carrying its deterministic universal identifier', () => {
|
||||
const { flatObjectMetadata, searchVectorFlatFieldMetadata } =
|
||||
buildSearchableObjectFixture({
|
||||
objectId: 'standard-object-id',
|
||||
objectUniversalIdentifier: 'standard-object-uid',
|
||||
});
|
||||
|
||||
const indexName = 'IDX_SEARCH_VECTOR_STANDARD';
|
||||
const ginIndex = getFlatIndexMetadataMock({
|
||||
id: 'standard-gin-index-id',
|
||||
universalIdentifier: getIndexUniversalIdentifier({
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: indexName,
|
||||
}),
|
||||
objectMetadataId: flatObjectMetadata.id,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
applicationId: STANDARD_APPLICATION_ID,
|
||||
applicationUniversalIdentifier: STANDARD_APPLICATION_UID,
|
||||
name: indexName,
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({
|
||||
fieldMetadataId: searchVectorFlatFieldMetadata.id,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const indexUniversalIdentifierUpdates =
|
||||
buildSearchVectorGinIndexReOwnOperations({
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps([
|
||||
flatObjectMetadata,
|
||||
]),
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
]),
|
||||
flatIndexMaps: buildFlatIndexMaps([ginIndex]),
|
||||
applicationUniversalIdentifierById,
|
||||
});
|
||||
|
||||
expect(indexUniversalIdentifierUpdates).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { isSearchVectorGinFlatIndexMetadata } from 'src/database/commands/upgrade-version-command/2-20/utils/is-search-vector-gin-flat-index-metadata.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 { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock';
|
||||
import { type FlatIndexFieldMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
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 buildFlatIndexFieldMetadata = ({
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
fieldMetadataId: string;
|
||||
}): FlatIndexFieldMetadata => {
|
||||
const createdAt = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
return {
|
||||
id: `${fieldMetadataId}-index-field-id`,
|
||||
workspaceId: 'workspace-id',
|
||||
indexMetadataId: 'index-metadata-id',
|
||||
fieldMetadataId,
|
||||
order: 0,
|
||||
subFieldName: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
const SEARCH_VECTOR_FIELD_ID = 'search-vector-field-id';
|
||||
const TEXT_FIELD_ID = 'text-field-id';
|
||||
const OTHER_TS_VECTOR_FIELD_ID = 'other-ts-vector-field-id';
|
||||
|
||||
const searchVectorFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: SEARCH_VECTOR_FIELD_ID,
|
||||
universalIdentifier: 'search-vector-field-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: SEARCH_VECTOR_FIELD.name,
|
||||
});
|
||||
|
||||
const textFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: TEXT_FIELD_ID,
|
||||
universalIdentifier: 'text-field-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const otherTsVectorFlatFieldMetadata = getFlatFieldMetadataMock({
|
||||
id: OTHER_TS_VECTOR_FIELD_ID,
|
||||
universalIdentifier: 'other-ts-vector-field-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: 'customSearchVector',
|
||||
});
|
||||
|
||||
const flatFieldMetadataMaps = buildFlatFieldMetadataMaps([
|
||||
searchVectorFlatFieldMetadata,
|
||||
textFlatFieldMetadata,
|
||||
otherTsVectorFlatFieldMetadata,
|
||||
]);
|
||||
|
||||
describe('isSearchVectorGinFlatIndexMetadata', () => {
|
||||
it('returns true for a single-field GIN index on the TS_VECTOR field', () => {
|
||||
const flatIndexMetadata = getFlatIndexMetadataMock({
|
||||
universalIdentifier: 'gin-index-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'application-uid',
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({ fieldMetadataId: SEARCH_VECTOR_FIELD_ID }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for a GIN index on a non-TS_VECTOR field', () => {
|
||||
const flatIndexMetadata = getFlatIndexMetadataMock({
|
||||
universalIdentifier: 'gin-index-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'application-uid',
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({ fieldMetadataId: TEXT_FIELD_ID }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a non-GIN (BTREE) index on the TS_VECTOR field', () => {
|
||||
const flatIndexMetadata = getFlatIndexMetadataMock({
|
||||
universalIdentifier: 'btree-index-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'application-uid',
|
||||
indexType: IndexType.BTREE,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({ fieldMetadataId: SEARCH_VECTOR_FIELD_ID }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a single-field GIN index on a TS_VECTOR field that is not the searchVector field', () => {
|
||||
const flatIndexMetadata = getFlatIndexMetadataMock({
|
||||
universalIdentifier: 'gin-index-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'application-uid',
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({
|
||||
fieldMetadataId: OTHER_TS_VECTOR_FIELD_ID,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for a multi-column GIN index', () => {
|
||||
const flatIndexMetadata = getFlatIndexMetadataMock({
|
||||
universalIdentifier: 'gin-index-uid',
|
||||
objectMetadataId: 'object-id',
|
||||
objectMetadataUniversalIdentifier: 'object-uid',
|
||||
applicationUniversalIdentifier: 'application-uid',
|
||||
indexType: IndexType.GIN,
|
||||
flatIndexFieldMetadatas: [
|
||||
buildFlatIndexFieldMetadata({ fieldMetadataId: SEARCH_VECTOR_FIELD_ID }),
|
||||
buildFlatIndexFieldMetadata({ fieldMetadataId: TEXT_FIELD_ID }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(
|
||||
isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import {
|
||||
fromArrayToValuesByKeyRecord,
|
||||
isDefined,
|
||||
isSearchableFieldType,
|
||||
} from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type 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 { findTsVectorFlatFieldMetadataForObject } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/find-ts-vector-flat-field-metadata-for-object.util';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
type BuildSearchFieldMetadataBackfillOperationsArgs = {
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatSearchFieldMetadataMaps: FlatEntityMaps<FlatSearchFieldMetadata>;
|
||||
applicationUniversalIdentifierById: Map<string, string>;
|
||||
twentyStandardApplicationId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
|
||||
// Backfill skips the twenty-standard and workspace-custom applications (their row is
|
||||
// already provisioned by the manifest funnel) and targets every other (installed)
|
||||
// application's searchable objects that have no row yet. The creation rule mirrors the
|
||||
// object-create side effect handler (label-identifier field of a searchable type;
|
||||
// junction/id-label objects have no search surface). Grouped by application universal
|
||||
// identifier for the per-application migration run.
|
||||
export const buildSearchFieldMetadataBackfillOperations = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
applicationUniversalIdentifierById,
|
||||
twentyStandardApplicationId,
|
||||
workspaceCustomApplicationId,
|
||||
}: BuildSearchFieldMetadataBackfillOperationsArgs): Record<
|
||||
string,
|
||||
UniversalFlatSearchFieldMetadata[]
|
||||
> => {
|
||||
const existingSearchFieldMetadataKeys = new Set<string>();
|
||||
|
||||
for (const flatSearchFieldMetadata of Object.values(
|
||||
flatSearchFieldMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
existingSearchFieldMetadataKeys.add(
|
||||
`${flatSearchFieldMetadata.objectMetadataId}:${flatSearchFieldMetadata.fieldMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const flatSearchFieldMetadatasToCreate: UniversalFlatSearchFieldMetadata[] =
|
||||
[];
|
||||
|
||||
for (const flatObjectMetadata of Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
if (
|
||||
flatObjectMetadata.applicationId === twentyStandardApplicationId ||
|
||||
flatObjectMetadata.applicationId === workspaceCustomApplicationId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flatObjectMetadata.isSearchable !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
applicationUniversalIdentifierById.get(flatObjectMetadata.applicationId);
|
||||
|
||||
if (!isDefined(applicationUniversalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (!isDefined(labelIdentifierFieldMetadataUniversalIdentifier)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const derivedIdFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
if (
|
||||
labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
derivedIdFieldUniversalIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelIdentifierFlatFieldMetadata =
|
||||
flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
labelIdentifierFieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (
|
||||
!isDefined(labelIdentifierFlatFieldMetadata) ||
|
||||
!isSearchableFieldType(labelIdentifierFlatFieldMetadata.type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
existingSearchFieldMetadataKeys.has(
|
||||
`${flatObjectMetadata.id}:${labelIdentifierFlatFieldMetadata.id}`,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tsVectorFlatFieldMetadata = findTsVectorFlatFieldMetadataForObject({
|
||||
fieldUniversalIdentifiers: flatObjectMetadata.fieldUniversalIdentifiers,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(tsVectorFlatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flatSearchFieldMetadatasToCreate.push(
|
||||
buildFlatSearchFieldMetadataForField({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadata: labelIdentifierFlatFieldMetadata,
|
||||
tsVectorFlatFieldMetadata,
|
||||
position: 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return fromArrayToValuesByKeyRecord({
|
||||
array: flatSearchFieldMetadatasToCreate,
|
||||
key: 'applicationUniversalIdentifier',
|
||||
});
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { getSearchFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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 { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatSearchFieldMetadata } from 'src/engine/metadata-modules/flat-search-field-metadata/types/flat-search-field-metadata.type';
|
||||
|
||||
export type SearchFieldMetadataUniversalIdentifierUpdate = {
|
||||
id: string;
|
||||
deterministicUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
type BuildSearchFieldMetadataReOwnOperationsArgs = {
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatSearchFieldMetadataMaps: FlatEntityMaps<FlatSearchFieldMetadata>;
|
||||
applicationUniversalIdentifierById: Map<string, string>;
|
||||
};
|
||||
|
||||
// Re-own is global (every application, including twenty-standard and workspace-custom):
|
||||
// every existing searchFieldMetadata row whose universal identifier drifted from its
|
||||
// getSearchFieldUniversalIdentifier derivation is converged back to it.
|
||||
export const buildSearchFieldMetadataReOwnOperations = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps,
|
||||
applicationUniversalIdentifierById,
|
||||
}: BuildSearchFieldMetadataReOwnOperationsArgs): SearchFieldMetadataUniversalIdentifierUpdate[] => {
|
||||
const searchFieldMetadataUniversalIdentifierUpdates: SearchFieldMetadataUniversalIdentifierUpdate[] =
|
||||
[];
|
||||
|
||||
for (const flatSearchFieldMetadata of Object.values(
|
||||
flatSearchFieldMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
const applicationUniversalIdentifier =
|
||||
applicationUniversalIdentifierById.get(
|
||||
flatSearchFieldMetadata.applicationId,
|
||||
);
|
||||
const indexedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: flatSearchFieldMetadata.fieldMetadataId,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(applicationUniversalIdentifier) ||
|
||||
!isDefined(indexedFlatFieldMetadata)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const deterministicUniversalIdentifier = getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
indexedFlatFieldMetadata.universalIdentifier,
|
||||
});
|
||||
|
||||
if (
|
||||
deterministicUniversalIdentifier ===
|
||||
flatSearchFieldMetadata.universalIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchFieldMetadataUniversalIdentifierUpdates.push({
|
||||
id: flatSearchFieldMetadata.id,
|
||||
deterministicUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
return searchFieldMetadataUniversalIdentifierUpdates;
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { fromArrayToValuesByKeyRecord, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isSearchVectorGinFlatIndexMetadata } from 'src/database/commands/upgrade-version-command/2-20/utils/is-search-vector-gin-flat-index-metadata.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { findTsVectorFlatFieldMetadataForObject } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/find-ts-vector-flat-field-metadata-for-object.util';
|
||||
import { buildSearchVectorGinIndexForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-search-vector-gin-index-for-custom-object.util';
|
||||
import { type UniversalFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-index-metadata.type';
|
||||
|
||||
type BuildSearchVectorGinIndexBackfillOperationsArgs = {
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
twentyStandardApplicationId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
};
|
||||
|
||||
// Backfill skips the twenty-standard and workspace-custom applications (their GIN index is
|
||||
// already provisioned by the manifest funnel) and targets every other (installed)
|
||||
// application object that has a searchVector field but no GIN index yet. Grouped by
|
||||
// application universal identifier for the per-application migration run.
|
||||
export const buildSearchVectorGinIndexBackfillOperations = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
twentyStandardApplicationId,
|
||||
workspaceCustomApplicationId,
|
||||
}: BuildSearchVectorGinIndexBackfillOperationsArgs): Record<
|
||||
string,
|
||||
UniversalFlatIndexMetadata[]
|
||||
> => {
|
||||
const objectMetadataIdsWithSearchVectorGinIndex = new Set<string>();
|
||||
|
||||
for (const flatIndexMetadata of Object.values(
|
||||
flatIndexMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
if (
|
||||
isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
})
|
||||
) {
|
||||
objectMetadataIdsWithSearchVectorGinIndex.add(
|
||||
flatIndexMetadata.objectMetadataId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const flatIndexesToCreate: UniversalFlatIndexMetadata[] = [];
|
||||
|
||||
for (const flatObjectMetadata of Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
if (
|
||||
flatObjectMetadata.applicationId === twentyStandardApplicationId ||
|
||||
flatObjectMetadata.applicationId === workspaceCustomApplicationId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (objectMetadataIdsWithSearchVectorGinIndex.has(flatObjectMetadata.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tsVectorFlatFieldMetadata = findTsVectorFlatFieldMetadataForObject({
|
||||
fieldUniversalIdentifiers: flatObjectMetadata.fieldUniversalIdentifiers,
|
||||
flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(tsVectorFlatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
flatIndexesToCreate.push(
|
||||
buildSearchVectorGinIndexForCustomObject({
|
||||
flatObjectMetadata,
|
||||
searchVectorFlatFieldMetadata: tsVectorFlatFieldMetadata,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return fromArrayToValuesByKeyRecord({
|
||||
array: flatIndexesToCreate,
|
||||
key: 'applicationUniversalIdentifier',
|
||||
});
|
||||
};
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { getIndexUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isSearchVectorGinFlatIndexMetadata } from 'src/database/commands/upgrade-version-command/2-20/utils/is-search-vector-gin-flat-index-metadata.util';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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 { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
export type SearchVectorGinIndexUniversalIdentifierUpdate = {
|
||||
id: string;
|
||||
name: string;
|
||||
deterministicUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
type BuildSearchVectorGinIndexReOwnOperationsArgs = {
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatIndexMaps: FlatEntityMaps<FlatIndexMetadata>;
|
||||
applicationUniversalIdentifierById: Map<string, string>;
|
||||
};
|
||||
|
||||
// Re-own is global (every application, including twenty-standard and workspace-custom):
|
||||
// every existing searchVector GIN index whose universal identifier drifted from its
|
||||
// getIndexUniversalIdentifier derivation is converged back to it.
|
||||
export const buildSearchVectorGinIndexReOwnOperations = ({
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
flatIndexMaps,
|
||||
applicationUniversalIdentifierById,
|
||||
}: BuildSearchVectorGinIndexReOwnOperationsArgs): SearchVectorGinIndexUniversalIdentifierUpdate[] => {
|
||||
const indexUniversalIdentifierUpdates: SearchVectorGinIndexUniversalIdentifierUpdate[] =
|
||||
[];
|
||||
|
||||
for (const flatIndexMetadata of Object.values(
|
||||
flatIndexMaps.byUniversalIdentifier,
|
||||
).filter(isDefined)) {
|
||||
if (
|
||||
!isSearchVectorGinFlatIndexMetadata({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const applicationUniversalIdentifier =
|
||||
applicationUniversalIdentifierById.get(flatIndexMetadata.applicationId);
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: flatIndexMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(applicationUniversalIdentifier) ||
|
||||
!isDefined(flatObjectMetadata)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const deterministicUniversalIdentifier = getIndexUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: flatIndexMetadata.name,
|
||||
});
|
||||
|
||||
if (
|
||||
deterministicUniversalIdentifier === flatIndexMetadata.universalIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
indexUniversalIdentifierUpdates.push({
|
||||
id: flatIndexMetadata.id,
|
||||
name: flatIndexMetadata.name,
|
||||
deterministicUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
return indexUniversalIdentifierUpdates;
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/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 { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/search-field-metadata/constants/search-vector-field.constants';
|
||||
|
||||
// The searchVector GIN index is the single-column GIN index whose only field is
|
||||
// the object's TS_VECTOR searchVector field (see build-search-vector-gin-index-for-custom-object.util).
|
||||
export const isSearchVectorGinFlatIndexMetadata = ({
|
||||
flatIndexMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
}: {
|
||||
flatIndexMetadata: FlatIndexMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
}): boolean => {
|
||||
if (flatIndexMetadata.indexType !== IndexType.GIN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (flatIndexMetadata.flatIndexFieldMetadatas.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const indexedFlatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: flatIndexMetadata.flatIndexFieldMetadatas[0].fieldMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
isDefined(indexedFlatFieldMetadata) &&
|
||||
indexedFlatFieldMetadata.type === FieldMetadataType.TS_VECTOR &&
|
||||
indexedFlatFieldMetadata.name === SEARCH_VECTOR_FIELD.name
|
||||
);
|
||||
};
|
||||
+4
@@ -101,7 +101,9 @@ import { BackfillDisplayFieldsOnApplicationRegistrationSlowInstanceCommand } fro
|
||||
import { BackfillIsFeaturedOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783120000000-backfill-is-featured-on-application-registration';
|
||||
import { AllowServerScopedFileFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783499671541-allow-server-scoped-file';
|
||||
import { CreateWorkflowVersionCoreTableFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783512000000-create-workflow-version-core-table';
|
||||
import { BackfillNameFieldIsSystemSideEffectSlowInstanceCommand } from './2-20/2-20-instance-command-slow-1783529458168-backfill-name-field-is-system-side-effect';
|
||||
import { RenameIsFeaturedToIsVettedOnApplicationRegistrationFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783527064000-rename-is-featured-to-is-vetted-on-application-registration';
|
||||
import { AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783580127637-add-is-system-side-effect-to-search-field-metadata';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||
@@ -205,5 +207,7 @@ export const INSTANCE_COMMANDS = [
|
||||
BackfillIsFeaturedOnApplicationRegistrationSlowInstanceCommand,
|
||||
AllowServerScopedFileFastInstanceCommand,
|
||||
CreateWorkflowVersionCoreTableFastInstanceCommand,
|
||||
BackfillNameFieldIsSystemSideEffectSlowInstanceCommand,
|
||||
RenameIsFeaturedToIsVettedOnApplicationRegistrationFastInstanceCommand,
|
||||
AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand,
|
||||
];
|
||||
|
||||
+1
-1
@@ -9,8 +9,8 @@ import {
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { type CompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/composite-field-metadata-type.type';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { nullifyEmptyCompositeDefaultValue } from 'src/engine/metadata-modules/flat-field-metadata/utils/nullify-empty-composite-default-value.util';
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
|
||||
|
||||
+7
-13
@@ -11,7 +11,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { isSystemUniqueFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/is-system-unique-flat-index-metadata.util';
|
||||
import { isSystemSideEffectFlatEntity } from 'src/engine/metadata-modules/flat-entity/utils/is-system-side-effect-flat-entity.util';
|
||||
import { type MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
|
||||
import { compareTwoFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/compare-two-universal-flat-entity.util';
|
||||
import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util';
|
||||
@@ -60,18 +60,12 @@ const buildFlatEntityOperationRecordForMetadata = <T extends AllMetadataName>({
|
||||
toByUniversalIdentifier[fromFlatEntity.universalIdentifier],
|
||||
),
|
||||
)
|
||||
.filter((fromFlatEntity) => {
|
||||
if (metadataName !== ALL_METADATA_NAME.index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isSystemUniqueFlatIndexMetadata(
|
||||
fromFlatEntity as unknown as {
|
||||
isSystemSideEffect: boolean;
|
||||
isUnique: boolean;
|
||||
},
|
||||
);
|
||||
})
|
||||
.filter(
|
||||
(fromFlatEntity) =>
|
||||
!isSystemSideEffectFlatEntity(
|
||||
fromFlatEntity as unknown as MetadataUniversalFlatEntity<AllMetadataName>,
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
const flatEntityToUpdate = Object.values(fromByUniversalIdentifier)
|
||||
|
||||
-9
@@ -94,7 +94,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatPageLayoutWidgetMaps: existingFlatPageLayoutWidgetMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
@@ -103,7 +102,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
'flatIndexMaps',
|
||||
'flatFieldMetadataMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatSearchFieldMetadataMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -112,13 +110,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatFieldMetadatasToDelete,
|
||||
flatIndexesToDelete,
|
||||
flatIndexesToUpdate,
|
||||
searchFieldMetadatasToDelete,
|
||||
} = fromDeleteFieldInputToFlatFieldMetadatasToDelete({
|
||||
deleteOneFieldInput,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const deletedFlatFieldMetadata = findFlatEntityByUniversalIdentifierOrThrow(
|
||||
@@ -167,11 +163,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
flatEntityToDelete: flatIndexesToDelete,
|
||||
flatEntityToUpdate: flatIndexesToUpdate,
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToDelete: searchFieldMetadatasToDelete,
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
...(flatPageLayoutWidgetsToDelete.length > 0
|
||||
? {
|
||||
pageLayoutWidget: {
|
||||
|
||||
+5
@@ -1807,6 +1807,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
},
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
isSystemSideEffect: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
objectMetadataId: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
export const ALL_METADATA_SIDE_EFFECT_COMPANION_METADATA_NAMES = {
|
||||
fieldMetadata: ['index'],
|
||||
fieldMetadata: ['index', 'searchFieldMetadata'],
|
||||
objectMetadata: ['fieldMetadata', 'index', 'searchFieldMetadata'],
|
||||
} as const satisfies Partial<
|
||||
Record<AllMetadataName, readonly AllMetadataName[]>
|
||||
>;
|
||||
|
||||
-6
@@ -46,7 +46,6 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps('obj-1'),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
@@ -70,7 +69,6 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
@@ -94,7 +92,6 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(new RegExp('Cannot delete standard field "city"'));
|
||||
});
|
||||
@@ -113,7 +110,6 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([customField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
});
|
||||
|
||||
expect(result.flatFieldMetadatasToDelete).toContainEqual(
|
||||
@@ -136,7 +132,6 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(FieldMetadataException);
|
||||
});
|
||||
@@ -160,7 +155,6 @@ describe('fromDeleteFieldInputToFlatFieldMetadatasToDelete', () => {
|
||||
flatFieldMetadataMaps: buildFlatFieldMetadataMaps([standardAppField]),
|
||||
flatObjectMetadataMaps: buildFlatObjectMetadataMaps(objectId),
|
||||
flatIndexMaps: createEmptyFlatEntityMaps(),
|
||||
flatSearchFieldMetadataMaps: createEmptyFlatEntityMaps(),
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
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 { 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,
|
||||
flatSearchFieldMetadataMaps,
|
||||
}: {
|
||||
flatFieldMetadatasToDelete: FlatFieldMetadata[];
|
||||
} & Pick<
|
||||
AllFlatEntityMaps,
|
||||
'flatObjectMetadataMaps' | 'flatSearchFieldMetadataMaps'
|
||||
>): {
|
||||
searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[];
|
||||
} => {
|
||||
const deletedFieldIds = new Set(
|
||||
flatFieldMetadatasToDelete.map((flatFieldMetadata) => flatFieldMetadata.id),
|
||||
);
|
||||
const affectedObjectMetadataIds = new Set(
|
||||
flatFieldMetadatasToDelete.map(
|
||||
(flatFieldMetadata) => flatFieldMetadata.objectMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[] = [];
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
searchFieldMetadatasToDelete.push(...rowsToDelete);
|
||||
}
|
||||
|
||||
return {
|
||||
searchFieldMetadatasToDelete,
|
||||
};
|
||||
};
|
||||
+1
-16
@@ -13,7 +13,6 @@ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/
|
||||
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 { 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 { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { isSystemUniqueFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/is-system-unique-flat-index-metadata.util';
|
||||
@@ -21,16 +20,12 @@ import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-mo
|
||||
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'
|
||||
| 'flatSearchFieldMetadataMaps'
|
||||
'flatFieldMetadataMaps' | 'flatIndexMaps' | 'flatObjectMetadataMaps'
|
||||
>;
|
||||
// TODO refactor as a side effect service
|
||||
export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
|
||||
@@ -38,12 +33,10 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
|
||||
deleteOneFieldInput: rawDeleteOneInput,
|
||||
flatIndexMaps: existingFlatIndexMaps,
|
||||
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
}: FromDeleteFieldInputToFlatFieldMetadatasToDeleteArgs): {
|
||||
flatFieldMetadatasToDelete: UniversalFlatFieldMetadata[];
|
||||
flatIndexesToUpdate: UniversalFlatIndexMetadata[];
|
||||
flatIndexesToDelete: UniversalFlatIndexMetadata[];
|
||||
searchFieldMetadatasToDelete: UniversalFlatSearchFieldMetadata[];
|
||||
} => {
|
||||
const { id: fieldMetadataToDeleteId } =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
@@ -214,17 +207,9 @@ export const fromDeleteFieldInputToFlatFieldMetadatasToDelete = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { searchFieldMetadatasToDelete } =
|
||||
computeSearchFieldMetadataDeletionForDeletedFields({
|
||||
flatFieldMetadatasToDelete,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
flatSearchFieldMetadataMaps: existingFlatSearchFieldMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
flatFieldMetadatasToDelete,
|
||||
flatIndexesToDelete,
|
||||
flatIndexesToUpdate,
|
||||
searchFieldMetadatasToDelete,
|
||||
};
|
||||
};
|
||||
|
||||
+25
-39
@@ -1,5 +1,7 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import {
|
||||
capitalize,
|
||||
isDefined,
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties,
|
||||
} from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
@@ -7,20 +9,18 @@ import { v4 } from 'uuid';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||
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 { buildNameFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-name-flat-field-metadata-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 =
|
||||
{
|
||||
createObjectInput: CreateObjectInput;
|
||||
flatApplication: FlatApplication;
|
||||
} & Pick<AllFlatEntityMaps, 'flatObjectMetadataMaps'>;
|
||||
|
||||
export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate =
|
||||
({
|
||||
createObjectInput: rawCreateObjectInput,
|
||||
@@ -28,10 +28,9 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
}: FromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreateArgs): {
|
||||
flatObjectMetadataToCreate: UniversalFlatObjectMetadata & { id: string };
|
||||
relationTargetFlatFieldMetadataToCreate: UniversalFlatFieldMetadata[];
|
||||
flatFieldMetadataToCreateOnObject: UniversalFlatFieldMetadata[];
|
||||
relationTargetFlatFieldMetadataToCreate: UniversalFlatFieldMetadata[];
|
||||
flatIndexMetadataToCreate: UniversalFlatIndexMetadata[];
|
||||
flatSearchFieldMetadataToCreate: UniversalFlatSearchFieldMetadata[];
|
||||
} => {
|
||||
const createObjectInput =
|
||||
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
|
||||
@@ -49,21 +48,14 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
|
||||
const objectMetadataId = v4();
|
||||
const universalIdentifier = createObjectInput.universalIdentifier ?? v4();
|
||||
const defaultFlatFieldForCustomObjectMaps =
|
||||
buildDefaultFlatFieldMetadatasForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
universalIdentifier,
|
||||
},
|
||||
skipNameField: createObjectInput.skipNameField,
|
||||
});
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
// Use nameField.id if it exists, otherwise use idField.id (for junction tables without name)
|
||||
const nameField = defaultFlatFieldForCustomObjectMaps.fields.nameField;
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
nameField?.universalIdentifier ??
|
||||
defaultFlatFieldForCustomObjectMaps.fields.id.universalIdentifier;
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
objectUniversalIdentifier: universalIdentifier,
|
||||
name: createObjectInput.skipNameField ? 'id' : 'name',
|
||||
});
|
||||
|
||||
const universalFlatObjectMetadataToCreate: UniversalFlatObjectMetadata & {
|
||||
id: string;
|
||||
@@ -102,6 +94,17 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
imageIdentifierFieldMetadataUniversalIdentifier: null,
|
||||
};
|
||||
|
||||
const nameFlatFieldMetadata =
|
||||
createObjectInput.skipNameField === true
|
||||
? null
|
||||
: buildNameFlatFieldMetadataForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier:
|
||||
flatApplication.universalIdentifier,
|
||||
universalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
standardSourceFlatFieldMetadatas,
|
||||
standardTargetFlatFieldMetadatas,
|
||||
@@ -112,32 +115,15 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
flatApplication,
|
||||
});
|
||||
|
||||
const objectFlatFieldMetadatas: UniversalFlatFieldMetadata[] = [
|
||||
...Object.values(defaultFlatFieldForCustomObjectMaps.fields),
|
||||
const flatFieldMetadataToCreateOnObject: UniversalFlatFieldMetadata[] = [
|
||||
...(isDefined(nameFlatFieldMetadata) ? [nameFlatFieldMetadata] : []),
|
||||
...standardSourceFlatFieldMetadatas,
|
||||
];
|
||||
|
||||
const defaultIndexesForCustomObject = buildDefaultIndexesForCustomObject({
|
||||
objectFlatFieldMetadatas,
|
||||
defaultFlatFieldForCustomObjectMaps,
|
||||
flatObjectMetadata: universalFlatObjectMetadataToCreate,
|
||||
});
|
||||
|
||||
const defaultSearchFieldMetadatasForCustomObject =
|
||||
buildDefaultSearchFieldMetadatasForCustomObject({
|
||||
defaultFlatFieldForCustomObjectMaps,
|
||||
flatObjectMetadata: universalFlatObjectMetadataToCreate,
|
||||
});
|
||||
|
||||
return {
|
||||
flatObjectMetadataToCreate: universalFlatObjectMetadataToCreate,
|
||||
flatIndexMetadataToCreate: [
|
||||
...Object.values(defaultIndexesForCustomObject.indexes),
|
||||
...standardTargetFlatIndexMetadatas,
|
||||
],
|
||||
flatSearchFieldMetadataToCreate:
|
||||
defaultSearchFieldMetadatasForCustomObject.searchFieldMetadatas,
|
||||
flatFieldMetadataToCreateOnObject,
|
||||
relationTargetFlatFieldMetadataToCreate: standardTargetFlatFieldMetadatas,
|
||||
flatFieldMetadataToCreateOnObject: objectFlatFieldMetadatas,
|
||||
flatIndexMetadataToCreate: standardTargetFlatIndexMetadatas,
|
||||
};
|
||||
};
|
||||
|
||||
-9
@@ -3,7 +3,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
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 { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-identifiers.util';
|
||||
import { validateObjectMetadataSystemFieldsIntegrity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity.util';
|
||||
import {
|
||||
type OrchestratorActionsReport,
|
||||
type OrchestratorFailureReport,
|
||||
@@ -57,14 +56,6 @@ export const validateObjectMetadataCrossEntity = ({
|
||||
},
|
||||
);
|
||||
|
||||
createFailedFlatEntityValidations.errors.push(
|
||||
...validateObjectMetadataSystemFieldsIntegrity({
|
||||
universalFlatFieldMetadataMaps:
|
||||
optimisticUniversalFlatMaps.flatFieldMetadataMaps,
|
||||
universalFlatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
|
||||
createFailedFlatEntityValidations.errors.push(
|
||||
...validateFlatObjectMetadataIdentifiers({
|
||||
universalFlatObjectMetadata,
|
||||
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.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';
|
||||
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { buildUniversalFlatObjectFieldByNameAndJoinColumnMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/build-universal-flat-object-field-by-name-and-join-column-maps.util';
|
||||
|
||||
type ValidateObjectMetadataSystemFieldsIntegrityArgs = {
|
||||
universalFlatObjectMetadata: UniversalFlatObjectMetadata;
|
||||
universalFlatFieldMetadataMaps: AllUniversalFlatEntityMaps['flatFieldMetadataMaps'];
|
||||
};
|
||||
export const validateObjectMetadataSystemFieldsIntegrity = ({
|
||||
universalFlatFieldMetadataMaps,
|
||||
universalFlatObjectMetadata,
|
||||
}: ValidateObjectMetadataSystemFieldsIntegrityArgs): FlatEntityValidationError[] => {
|
||||
const errors: FlatEntityValidationError[] = [];
|
||||
|
||||
const { fieldUniversalIdentifierByName } =
|
||||
buildUniversalFlatObjectFieldByNameAndJoinColumnMaps({
|
||||
flatFieldMetadataMaps: universalFlatFieldMetadataMaps,
|
||||
flatObjectMetadata: universalFlatObjectMetadata,
|
||||
});
|
||||
|
||||
for (const expectedSystemField of Object.values(
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS,
|
||||
)) {
|
||||
const matchingFieldUniversalIdentifier =
|
||||
fieldUniversalIdentifierByName[expectedSystemField.name];
|
||||
|
||||
const expectedFieldName = expectedSystemField.name;
|
||||
|
||||
if (!isDefined(matchingFieldUniversalIdentifier)) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD,
|
||||
message: `System field ${expectedFieldName} is missing`,
|
||||
userFriendlyMessage: msg`System field ${expectedFieldName} is missing`,
|
||||
value: expectedFieldName,
|
||||
});
|
||||
} else {
|
||||
const universalFlatFieldMetadata =
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: universalFlatFieldMetadataMaps,
|
||||
universalIdentifier: matchingFieldUniversalIdentifier,
|
||||
});
|
||||
|
||||
const propertiesToValidate = [
|
||||
'type',
|
||||
'isSystem',
|
||||
] as const satisfies (keyof UniversalFlatFieldMetadata)[];
|
||||
|
||||
for (const property of propertiesToValidate) {
|
||||
const expectedValue = expectedSystemField[property];
|
||||
const actualValue = universalFlatFieldMetadata[property];
|
||||
|
||||
if (actualValue !== expectedValue) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD,
|
||||
message: `System field ${expectedFieldName} has invalid ${property}: expected ${String(expectedValue)}, got ${String(actualValue)}`,
|
||||
userFriendlyMessage: msg`System field ${expectedFieldName} has invalid ${property}`,
|
||||
value: actualValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// System field universal identifiers are server-owned and must match
|
||||
// the deterministic derivation; clients cannot provide custom ones.
|
||||
// TODO: remove once system fields are generated server side only by
|
||||
// the metadata side effect engine and stripped from client inputs.
|
||||
const expectedUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
universalFlatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier:
|
||||
universalFlatObjectMetadata.universalIdentifier,
|
||||
name: expectedFieldName,
|
||||
});
|
||||
|
||||
if (
|
||||
universalFlatFieldMetadata.universalIdentifier !==
|
||||
expectedUniversalIdentifier
|
||||
) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD,
|
||||
message: `System field ${expectedFieldName} has invalid universalIdentifier: expected ${expectedUniversalIdentifier}, got ${universalFlatFieldMetadata.universalIdentifier}`,
|
||||
userFriendlyMessage: msg`System field ${expectedFieldName} universal identifier is not deterministic; it is derived by the server and cannot be customized`,
|
||||
value: universalFlatFieldMetadata.universalIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+7
-2
@@ -1,4 +1,4 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { getSearchFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
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';
|
||||
@@ -20,10 +20,15 @@ export const buildFlatSearchFieldMetadataForField = ({
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
return {
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: flatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
position,
|
||||
isSystemSideEffect: true,
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { FieldSearchFieldMetadataOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-search-field-metadata-on-delete-side-effect-handler.service';
|
||||
|
||||
const DELETED_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000001';
|
||||
const OTHER_FIELD_UNIVERSAL_IDENTIFIER = 'd1d2d3d4-d5d6-4000-8000-000000000002';
|
||||
const SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER =
|
||||
'f1f2f3f4-f5f6-4000-8000-000000000001';
|
||||
const OTHER_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER =
|
||||
'f1f2f3f4-f5f6-4000-8000-000000000002';
|
||||
|
||||
const buildArgs = ({
|
||||
searchFieldMetadataUniversalIdentifiers,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
searchFieldMetadataUniversalIdentifiers: string[];
|
||||
relatedFlatEntityMaps: object;
|
||||
}): BuildSideEffectsArgs<'fieldMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
universalIdentifier: DELETED_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
searchFieldMetadataUniversalIdentifiers,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {},
|
||||
relatedFlatEntityMaps,
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'fieldMetadata'>;
|
||||
|
||||
describe('FieldSearchFieldMetadataOnDeleteSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (FieldSearchFieldMetadataOnDeleteSideEffectHandlerService as unknown as new () => FieldSearchFieldMetadataOnDeleteSideEffectHandlerService)();
|
||||
|
||||
it('should cascade-delete every searchFieldMetadata row indexing the deleted field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
searchFieldMetadataUniversalIdentifiers: [
|
||||
SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
relatedFlatEntityMaps: {
|
||||
flatSearchFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
DELETED_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
result.operations.searchFieldMetadata?.flatEntityToDelete ?? {},
|
||||
),
|
||||
).toEqual([SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]);
|
||||
});
|
||||
|
||||
it('should not delete searchFieldMetadata rows indexing a different field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
relatedFlatEntityMaps: {
|
||||
flatSearchFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[OTHER_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier:
|
||||
OTHER_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
OTHER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should be a noop when the workspace has no searchFieldMetadata rows', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
searchFieldMetadataUniversalIdentifiers: [
|
||||
SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
relatedFlatEntityMaps: {
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
|
||||
@Injectable()
|
||||
export class FieldSearchFieldMetadataOnDeleteSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'delete',
|
||||
metadataName: 'fieldMetadata',
|
||||
name: 'fieldSearchFieldMetadataOnDelete',
|
||||
description:
|
||||
'When a field is deleted, cascade-delete every searchFieldMetadata row that indexes it. searchFieldMetadata is excluded from manifest deletion inference, so the cascade must be explicit here to cover both the API and manifest paths (the object-scoped cascade only fires on object deletion).',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatFieldMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'fieldMetadata'>): MetadataSideEffectResult {
|
||||
const searchFieldMetadataToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'searchFieldMetadata'>
|
||||
> = {};
|
||||
|
||||
for (const searchFieldMetadataUniversalIdentifier of flatFieldMetadata.searchFieldMetadataUniversalIdentifiers) {
|
||||
const flatSearchFieldMetadata =
|
||||
relatedFlatEntityMaps.flatSearchFieldMetadataMaps.byUniversalIdentifier[
|
||||
searchFieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(flatSearchFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchFieldMetadataToDelete[flatSearchFieldMetadata.universalIdentifier] =
|
||||
flatSearchFieldMetadata;
|
||||
}
|
||||
|
||||
if (Object.keys(searchFieldMetadataToDelete).length === 0) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
searchFieldMetadata: {
|
||||
flatEntityToDelete: searchFieldMetadataToDelete,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+10
@@ -1,14 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { FieldSearchFieldMetadataOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-search-field-metadata-on-delete-side-effect-handler.service';
|
||||
import { FieldUniqueBackingIndexOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-create-side-effect-handler.service';
|
||||
import { FieldUniqueBackingIndexOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-delete-side-effect-handler.service';
|
||||
import { FieldUniqueBackingIndexOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/field-metadata/services/field-unique-backing-index-on-update-side-effect-handler.service';
|
||||
import { ObjectSearchVectorOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-create-side-effect-handler.service';
|
||||
import { ObjectSearchVectorOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-update-side-effect-handler.service';
|
||||
import { ObjectSystemFieldsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-on-create-side-effect-handler.service';
|
||||
import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-side-effects-on-delete-side-effect-handler.service';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
FieldUniqueBackingIndexOnCreateSideEffectHandlerService,
|
||||
FieldUniqueBackingIndexOnUpdateSideEffectHandlerService,
|
||||
FieldUniqueBackingIndexOnDeleteSideEffectHandlerService,
|
||||
FieldSearchFieldMetadataOnDeleteSideEffectHandlerService,
|
||||
ObjectSystemFieldsOnCreateSideEffectHandlerService,
|
||||
ObjectSearchVectorOnCreateSideEffectHandlerService,
|
||||
ObjectSearchVectorOnUpdateSideEffectHandlerService,
|
||||
ObjectSystemSideEffectsOnDeleteSideEffectHandlerService,
|
||||
],
|
||||
})
|
||||
export class MetadataSideEffectHandlersModule {}
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { ObjectSearchVectorOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-create-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'searchVector',
|
||||
});
|
||||
|
||||
type PendingFieldMetadata = {
|
||||
universalIdentifier: string;
|
||||
type: FieldMetadataType;
|
||||
};
|
||||
|
||||
const buildArgs = ({
|
||||
isSearchable,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
pendingFieldMetadatas = [],
|
||||
relatedFlatEntityMaps = {
|
||||
flatFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
flatIndexMaps: { byUniversalIdentifier: {} },
|
||||
},
|
||||
}: {
|
||||
isSearchable: boolean;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string | null;
|
||||
pendingFieldMetadatas?: PendingFieldMetadata[];
|
||||
relatedFlatEntityMaps?: object;
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'ticket',
|
||||
isSearchable,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {
|
||||
...(pendingFieldMetadatas.length > 0 && {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingFieldMetadatas.map((pendingFieldMetadata) => [
|
||||
pendingFieldMetadata.universalIdentifier,
|
||||
pendingFieldMetadata,
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
}),
|
||||
} as unknown as AllFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
const PENDING_TEXT_NAME_FIELD: PendingFieldMetadata = {
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
describe('ObjectSearchVectorOnCreateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectSearchVectorOnCreateSideEffectHandlerService as unknown as new () => ObjectSearchVectorOnCreateSideEffectHandlerService)();
|
||||
|
||||
it('should always provision the searchVector field and its GIN index, plus a searchFieldMetadata for a searchable object with a searchable label identifier', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_NAME_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const createdFieldUniversalIdentifiers = Object.keys(
|
||||
result.operations.fieldMetadata?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(createdFieldUniversalIdentifiers).toEqual([
|
||||
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
]);
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.index?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(1);
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
result.operations.searchFieldMetadata?.flatEntityToCreate ?? {},
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should still provision the searchVector field and GIN index but no searchFieldMetadata when the object is not searchable', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: false,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_NAME_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.fieldMetadata?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
Object.keys(result.operations.index?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(1);
|
||||
expect(result.operations.searchFieldMetadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not provision a searchFieldMetadata for junction objects whose label identifier is the id field', () => {
|
||||
const idFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
idFieldUniversalIdentifier,
|
||||
pendingFieldMetadatas: [
|
||||
{
|
||||
universalIdentifier: idFieldUniversalIdentifier,
|
||||
type: FieldMetadataType.UUID,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(result.operations.searchFieldMetadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not provision a searchFieldMetadata when the caller published a non-searchable field at the derived name identifier', () => {
|
||||
// The universal identifier derivation is name-based, so a caller-provided
|
||||
// `name` field of any type lands on the derived identifier. The handler must
|
||||
// resolve the actual type instead of assuming TEXT.
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(result.operations.searchFieldMetadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not provision a searchFieldMetadata when the label identifier field cannot be resolved', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(result.operations.searchFieldMetadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should resolve the label identifier field type from the existing maps when it is not part of the pending operations', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[NAME_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
},
|
||||
},
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
flatIndexMaps: { byUniversalIdentifier: {} },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
result.operations.searchFieldMetadata?.flatEntityToCreate ?? {},
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be deterministic across invocations', () => {
|
||||
const firstResult = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_NAME_FIELD],
|
||||
}),
|
||||
);
|
||||
const secondResult = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: true,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_NAME_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
if (firstResult.status !== 'success' || secondResult.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(firstResult.operations.index?.flatEntityToCreate ?? {}),
|
||||
).toEqual(
|
||||
Object.keys(secondResult.operations.index?.flatEntityToCreate ?? {}),
|
||||
);
|
||||
});
|
||||
});
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { ObjectSearchVectorOnUpdateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-search-vector-on-update-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const TOTO_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'toto',
|
||||
});
|
||||
|
||||
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'searchVector',
|
||||
});
|
||||
|
||||
const NAME_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER =
|
||||
'c1c2c3c4-c5c6-4000-8000-000000000001';
|
||||
|
||||
type PendingFieldMetadata = {
|
||||
universalIdentifier: string;
|
||||
type: FieldMetadataType;
|
||||
};
|
||||
|
||||
const SEARCH_VECTOR_FLAT_FIELD = {
|
||||
universalIdentifier: SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: 'searchVector',
|
||||
};
|
||||
|
||||
const NAME_SEARCH_FIELD_METADATA = {
|
||||
universalIdentifier: NAME_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 0,
|
||||
};
|
||||
|
||||
const buildArgs = ({
|
||||
isSearchable = true,
|
||||
toLabelIdentifier,
|
||||
fromLabelIdentifier,
|
||||
pendingFieldMetadatas = [],
|
||||
existingFlatFieldMetadataByUniversalIdentifier = {
|
||||
[SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER]: SEARCH_VECTOR_FLAT_FIELD,
|
||||
},
|
||||
existingSearchFieldMetadataByUniversalIdentifier = {
|
||||
[NAME_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]:
|
||||
NAME_SEARCH_FIELD_METADATA,
|
||||
},
|
||||
existingObjectSearchFieldMetadataUniversalIdentifiers = [
|
||||
NAME_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
existingObjectFieldUniversalIdentifiers = [
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
}: {
|
||||
isSearchable?: boolean;
|
||||
toLabelIdentifier: string | null;
|
||||
fromLabelIdentifier: string | null;
|
||||
pendingFieldMetadatas?: PendingFieldMetadata[];
|
||||
existingFlatFieldMetadataByUniversalIdentifier?: object;
|
||||
existingSearchFieldMetadataByUniversalIdentifier?: object;
|
||||
existingObjectSearchFieldMetadataUniversalIdentifiers?: string[];
|
||||
existingObjectFieldUniversalIdentifiers?: string[];
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'ticket',
|
||||
isSearchable,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: toLabelIdentifier,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {
|
||||
...(pendingFieldMetadatas.length > 0 && {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: Object.fromEntries(
|
||||
pendingFieldMetadatas.map((pendingFieldMetadata) => [
|
||||
pendingFieldMetadata.universalIdentifier,
|
||||
pendingFieldMetadata,
|
||||
]),
|
||||
),
|
||||
flatEntityToUpdate: {},
|
||||
flatEntityToDelete: {},
|
||||
},
|
||||
}),
|
||||
} as unknown as AllFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps: {
|
||||
flatObjectMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[OBJECT_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
fromLabelIdentifier,
|
||||
fieldUniversalIdentifiers: existingObjectFieldUniversalIdentifiers,
|
||||
searchFieldMetadataUniversalIdentifiers:
|
||||
existingObjectSearchFieldMetadataUniversalIdentifiers,
|
||||
},
|
||||
},
|
||||
},
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: existingFlatFieldMetadataByUniversalIdentifier,
|
||||
},
|
||||
flatSearchFieldMetadataMaps: {
|
||||
byUniversalIdentifier: existingSearchFieldMetadataByUniversalIdentifier,
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
const PENDING_TEXT_TOTO_FIELD: PendingFieldMetadata = {
|
||||
universalIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
describe('ObjectSearchVectorOnUpdateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectSearchVectorOnUpdateSideEffectHandlerService as unknown as new () => ObjectSearchVectorOnUpdateSideEffectHandlerService)();
|
||||
|
||||
it('should provision a searchFieldMetadata for the new searchable label identifier while preserving the existing surface', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_TOTO_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const createdSearchFieldMetadatas =
|
||||
result.operations.searchFieldMetadata?.flatEntityToCreate ?? {};
|
||||
|
||||
expect(Object.keys(createdSearchFieldMetadatas)).toHaveLength(1);
|
||||
|
||||
const [createdSearchFieldMetadata] = Object.values(
|
||||
createdSearchFieldMetadatas,
|
||||
);
|
||||
|
||||
expect(createdSearchFieldMetadata.fieldMetadataUniversalIdentifier).toBe(
|
||||
TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(
|
||||
createdSearchFieldMetadata.tsVectorFieldMetadataUniversalIdentifier,
|
||||
).toBe(SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER);
|
||||
// Appended after the pre-existing name row (position 0).
|
||||
expect(createdSearchFieldMetadata.position).toBe(1);
|
||||
});
|
||||
|
||||
it('should resolve the new label identifier field type from the existing maps', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
existingFlatFieldMetadataByUniversalIdentifier: {
|
||||
[SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER]: SEARCH_VECTOR_FLAT_FIELD,
|
||||
[TOTO_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(
|
||||
result.operations.searchFieldMetadata?.flatEntityToCreate ?? {},
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should be a noop when the object is not searchable', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
isSearchable: false,
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_TOTO_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should be a noop when the label identifier did not change', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_TOTO_FIELD],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should be a noop when the new label identifier is the system id field (junction object)', () => {
|
||||
const idFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: idFieldUniversalIdentifier,
|
||||
pendingFieldMetadatas: [
|
||||
{
|
||||
universalIdentifier: idFieldUniversalIdentifier,
|
||||
type: FieldMetadataType.UUID,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should be a noop when the new label identifier field is not a searchable type', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [
|
||||
{
|
||||
universalIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should be a noop when the new label identifier is already indexed', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_TOTO_FIELD],
|
||||
existingSearchFieldMetadataByUniversalIdentifier: {
|
||||
[NAME_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]:
|
||||
NAME_SEARCH_FIELD_METADATA,
|
||||
'toto-search-row': {
|
||||
universalIdentifier: 'toto-search-row',
|
||||
fieldMetadataUniversalIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
existingObjectSearchFieldMetadataUniversalIdentifiers: [
|
||||
NAME_SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
'toto-search-row',
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
|
||||
it('should be a noop when the object has no searchVector field to attach to', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
fromLabelIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
toLabelIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
pendingFieldMetadatas: [PENDING_TEXT_TOTO_FIELD],
|
||||
existingFlatFieldMetadataByUniversalIdentifier: {},
|
||||
existingObjectFieldUniversalIdentifiers: [
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
import { type AllFlatEntityOperationRecordByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-operation-record-by-metadata-name.type';
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { ObjectSystemFieldsOnCreateSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-fields-on-create-side-effect-handler.service';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'a1a2a3a4-a5a6-4000-8000-000000000001';
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
|
||||
const SYSTEM_FIELD_NAMES = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
] as const;
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'searchVector',
|
||||
});
|
||||
|
||||
const buildArgs = ({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName = {} as unknown as AllFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps = {},
|
||||
}: {
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string;
|
||||
allFlatEntityOperationRecordByMetadataName?: AllFlatEntityOperationRecordByMetadataName;
|
||||
relatedFlatEntityMaps?: object;
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
describe('ObjectSystemFieldsOnCreateSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectSystemFieldsOnCreateSideEffectHandlerService as unknown as new () => ObjectSystemFieldsOnCreateSideEffectHandlerService)();
|
||||
|
||||
it('should synthesize exactly the 7 reserved system fields and never the caller-provided name field nor the searchVector field', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const createdUniversalIdentifiers = Object.keys(
|
||||
result.operations.fieldMetadata?.flatEntityToCreate ?? {},
|
||||
);
|
||||
|
||||
expect(createdUniversalIdentifiers).toHaveLength(7);
|
||||
expect(createdUniversalIdentifiers).not.toContain(
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(createdUniversalIdentifiers).not.toContain(
|
||||
SEARCH_VECTOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
for (const name of SYSTEM_FIELD_NAMES) {
|
||||
expect(createdUniversalIdentifiers).toContain(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should still emit all system fields even when they already exist in the from-state (dedup delegated to the engine merge)', () => {
|
||||
const alreadyPresentByUniversalIdentifier: Record<string, unknown> = {};
|
||||
|
||||
for (const name of SYSTEM_FIELD_NAMES) {
|
||||
const universalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name,
|
||||
});
|
||||
|
||||
alreadyPresentByUniversalIdentifier[universalIdentifier] = {
|
||||
universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: alreadyPresentByUniversalIdentifier,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
expect(
|
||||
Object.keys(result.operations.fieldMetadata?.flatEntityToCreate ?? {}),
|
||||
).toHaveLength(7);
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { type BuildSideEffectsArgs } from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { ObjectSystemSideEffectsOnDeleteSideEffectHandlerService } from 'src/engine/metadata-modules/metadata-side-effect/handlers/object-metadata/services/object-system-side-effects-on-delete-side-effect-handler.service';
|
||||
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = 'b1b2b3b4-b5b6-4000-8000-000000000001';
|
||||
const OTHER_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'c1c2c3c4-c5c6-4000-8000-000000000001';
|
||||
|
||||
const SYSTEM_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000001';
|
||||
const AUTHOR_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd1d2d3d4-d5d6-4000-8000-000000000002';
|
||||
const GIN_INDEX_UNIVERSAL_IDENTIFIER = 'e1e2e3e4-e5e6-4000-8000-000000000001';
|
||||
const SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER =
|
||||
'f1f2f3f4-f5f6-4000-8000-000000000001';
|
||||
|
||||
const buildArgs = ({
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
relatedFlatEntityMaps: object;
|
||||
}): BuildSideEffectsArgs<'objectMetadata'> =>
|
||||
({
|
||||
flatEntity: {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
allFlatEntityOperationRecordByMetadataName: {},
|
||||
relatedFlatEntityMaps,
|
||||
context: {},
|
||||
}) as unknown as BuildSideEffectsArgs<'objectMetadata'>;
|
||||
|
||||
describe('ObjectSystemSideEffectsOnDeleteSideEffectHandlerService', () => {
|
||||
const handler =
|
||||
new (ObjectSystemSideEffectsOnDeleteSideEffectHandlerService as unknown as new () => ObjectSystemSideEffectsOnDeleteSideEffectHandlerService)();
|
||||
|
||||
it('should cascade-delete engine-owned fields, indexes and searchFieldMetadata while leaving author fields untouched', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[SYSTEM_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
},
|
||||
[AUTHOR_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: false,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
flatIndexMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[GIN_INDEX_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: GIN_INDEX_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
universalFlatIndexFieldMetadatas: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
flatSearchFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER,
|
||||
objectMetadataUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('success');
|
||||
|
||||
if (result.status !== 'success') {
|
||||
throw new Error('expected success');
|
||||
}
|
||||
|
||||
const deletedFieldUniversalIdentifiers = Object.keys(
|
||||
result.operations.fieldMetadata?.flatEntityToDelete ?? {},
|
||||
);
|
||||
|
||||
expect(deletedFieldUniversalIdentifiers).toEqual([
|
||||
SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
]);
|
||||
expect(deletedFieldUniversalIdentifiers).not.toContain(
|
||||
AUTHOR_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
expect(
|
||||
Object.keys(result.operations.index?.flatEntityToDelete ?? {}),
|
||||
).toEqual([GIN_INDEX_UNIVERSAL_IDENTIFIER]);
|
||||
expect(
|
||||
Object.keys(
|
||||
result.operations.searchFieldMetadata?.flatEntityToDelete ?? {},
|
||||
),
|
||||
).toEqual([SEARCH_FIELD_METADATA_UNIVERSAL_IDENTIFIER]);
|
||||
});
|
||||
|
||||
it('should not delete entities belonging to a different object', () => {
|
||||
const result = handler.buildSideEffects(
|
||||
buildArgs({
|
||||
relatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps: {
|
||||
byUniversalIdentifier: {
|
||||
[SYSTEM_FIELD_UNIVERSAL_IDENTIFIER]: {
|
||||
universalIdentifier: SYSTEM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier:
|
||||
OTHER_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
flatIndexMaps: { byUniversalIdentifier: {} },
|
||||
flatSearchFieldMetadataMaps: { byUniversalIdentifier: {} },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.status).toBe('noop');
|
||||
});
|
||||
});
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined, isSearchableFieldType } 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 BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectOperationsByMetadataName } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-operations-by-metadata-name.type';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
import { buildSearchVectorFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-search-vector-flat-field-metadata-for-custom-object.util';
|
||||
import { buildSearchVectorGinIndexForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-search-vector-gin-index-for-custom-object.util';
|
||||
import { type UniversalFlatSearchFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-search-field-metadata.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSearchVectorOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSearchVectorOnCreate',
|
||||
description:
|
||||
'When an object is created, provision its full-text search surface as a single self-contained side effect: the searchVector system field, the GIN index backing it, and (for searchable objects whose label identifier is a searchable field) the searchFieldMetadata row that keeps the searchVector populated instead of NULL.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatObjectMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const { applicationUniversalIdentifier, universalIdentifier } =
|
||||
flatObjectMetadata;
|
||||
|
||||
const searchVectorFlatFieldMetadata =
|
||||
buildSearchVectorFlatFieldMetadataForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
const tsVectorFlatIndex = buildSearchVectorGinIndexForCustomObject({
|
||||
flatObjectMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
});
|
||||
|
||||
const operations: MetadataSideEffectOperationsByMetadataName = {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: {
|
||||
[searchVectorFlatFieldMetadata.universalIdentifier]:
|
||||
searchVectorFlatFieldMetadata,
|
||||
},
|
||||
},
|
||||
index: {
|
||||
flatEntityToCreate: {
|
||||
[tsVectorFlatIndex.universalIdentifier]: tsVectorFlatIndex,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const searchFieldMetadata = this.buildSearchFieldMetadata({
|
||||
flatObjectMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
|
||||
if (isDefined(searchFieldMetadata)) {
|
||||
operations.searchFieldMetadata = {
|
||||
flatEntityToCreate: {
|
||||
[searchFieldMetadata.universalIdentifier]: searchFieldMetadata,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations,
|
||||
};
|
||||
}
|
||||
|
||||
private buildSearchFieldMetadata({
|
||||
flatObjectMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
flatObjectMetadata: BuildSideEffectsArgs<'objectMetadata'>['flatEntity'];
|
||||
searchVectorFlatFieldMetadata: { universalIdentifier: string };
|
||||
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'objectMetadata'>['allFlatEntityOperationRecordByMetadataName'];
|
||||
relatedFlatEntityMaps: BuildSideEffectsArgs<'objectMetadata'>['relatedFlatEntityMaps'];
|
||||
}): UniversalFlatSearchFieldMetadata | undefined {
|
||||
if (flatObjectMetadata.isSearchable !== true) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
flatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (!isDefined(labelIdentifierFieldMetadataUniversalIdentifier)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const derivedIdFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
if (
|
||||
labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
derivedIdFieldUniversalIdentifier
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const labelIdentifierFieldType = this.resolveLabelIdentifierFieldType({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(labelIdentifierFieldType) ||
|
||||
!isSearchableFieldType(labelIdentifierFieldType)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildFlatSearchFieldMetadataForField({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadata: {
|
||||
universalIdentifier: labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
tsVectorFlatFieldMetadata: {
|
||||
universalIdentifier: searchVectorFlatFieldMetadata.universalIdentifier,
|
||||
},
|
||||
position: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private resolveLabelIdentifierFieldType({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string;
|
||||
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'objectMetadata'>['allFlatEntityOperationRecordByMetadataName'];
|
||||
relatedFlatEntityMaps: BuildSideEffectsArgs<'objectMetadata'>['relatedFlatEntityMaps'];
|
||||
}): FieldMetadataType | undefined {
|
||||
const pendingField =
|
||||
allFlatEntityOperationRecordByMetadataName.fieldMetadata
|
||||
?.flatEntityToCreate[labelIdentifierFieldMetadataUniversalIdentifier];
|
||||
|
||||
if (isDefined(pendingField)) {
|
||||
return pendingField.type;
|
||||
}
|
||||
|
||||
const existingField =
|
||||
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
labelIdentifierFieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
return existingField?.type;
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined, isSearchableFieldType } 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 { findTsVectorFlatFieldMetadataForObject } from 'src/engine/metadata-modules/flat-search-field-metadata/utils/find-ts-vector-flat-field-metadata-for-object.util';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSearchVectorOnUpdateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'update',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSearchVectorOnUpdate',
|
||||
description:
|
||||
'When a searchable object is relabeled onto a new searchable field, provision the searchFieldMetadata row that indexes it. Relabeling is additive: existing search rows (e.g. the provisioned name row) are preserved so the previous label identifier stays searchable. Mirrors the API update path so a manifest re-sync that changes the label identifier reaches search parity.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: updatedFlatObjectMetadata,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
if (updatedFlatObjectMetadata.isSearchable !== true) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const newLabelIdentifierFieldMetadataUniversalIdentifier =
|
||||
updatedFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier;
|
||||
|
||||
if (!isDefined(newLabelIdentifierFieldMetadataUniversalIdentifier)) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
// The trigger entity is the incoming (manifest/API) object with empty foreign
|
||||
// key aggregators; the existing search rows, positions and searchVector field
|
||||
// are resolved from the current cached object.
|
||||
const existingFlatObjectMetadata =
|
||||
relatedFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
updatedFlatObjectMetadata.universalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(existingFlatObjectMetadata)) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
if (
|
||||
existingFlatObjectMetadata.labelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
newLabelIdentifierFieldMetadataUniversalIdentifier
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
// Junction objects use the system id field as label identifier; UUID is a
|
||||
// searchable type, so a type-based check would wrongly index them.
|
||||
const derivedIdFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
updatedFlatObjectMetadata.applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: updatedFlatObjectMetadata.universalIdentifier,
|
||||
name: 'id',
|
||||
});
|
||||
|
||||
if (
|
||||
newLabelIdentifierFieldMetadataUniversalIdentifier ===
|
||||
derivedIdFieldUniversalIdentifier
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const newLabelIdentifierFieldType = this.resolveFieldType({
|
||||
fieldMetadataUniversalIdentifier:
|
||||
newLabelIdentifierFieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(newLabelIdentifierFieldType) ||
|
||||
!isSearchableFieldType(newLabelIdentifierFieldType)
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const existingSearchFieldMetadatas =
|
||||
existingFlatObjectMetadata.searchFieldMetadataUniversalIdentifiers
|
||||
.map(
|
||||
(searchFieldMetadataUniversalIdentifier) =>
|
||||
relatedFlatEntityMaps.flatSearchFieldMetadataMaps
|
||||
.byUniversalIdentifier[searchFieldMetadataUniversalIdentifier],
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const newLabelIdentifierAlreadyIndexed = existingSearchFieldMetadatas.some(
|
||||
(searchFieldMetadata) =>
|
||||
searchFieldMetadata.fieldMetadataUniversalIdentifier ===
|
||||
newLabelIdentifierFieldMetadataUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (newLabelIdentifierAlreadyIndexed) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const tsVectorFlatFieldMetadata = findTsVectorFlatFieldMetadataForObject({
|
||||
fieldUniversalIdentifiers:
|
||||
existingFlatObjectMetadata.fieldUniversalIdentifiers,
|
||||
flatFieldMetadataMaps: relatedFlatEntityMaps.flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(tsVectorFlatFieldMetadata)) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const newLabelIdentifierPosition =
|
||||
existingSearchFieldMetadatas.reduce(
|
||||
(maxPosition, searchFieldMetadata) =>
|
||||
Math.max(maxPosition, searchFieldMetadata.position),
|
||||
-1,
|
||||
) + 1;
|
||||
|
||||
const searchFieldMetadata = buildFlatSearchFieldMetadataForField({
|
||||
flatObjectMetadata: updatedFlatObjectMetadata,
|
||||
flatFieldMetadata: {
|
||||
universalIdentifier: newLabelIdentifierFieldMetadataUniversalIdentifier,
|
||||
},
|
||||
tsVectorFlatFieldMetadata: {
|
||||
universalIdentifier: tsVectorFlatFieldMetadata.universalIdentifier,
|
||||
},
|
||||
position: newLabelIdentifierPosition,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: {
|
||||
[searchFieldMetadata.universalIdentifier]: searchFieldMetadata,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private resolveFieldType({
|
||||
fieldMetadataUniversalIdentifier,
|
||||
allFlatEntityOperationRecordByMetadataName,
|
||||
relatedFlatEntityMaps,
|
||||
}: {
|
||||
fieldMetadataUniversalIdentifier: string;
|
||||
allFlatEntityOperationRecordByMetadataName: BuildSideEffectsArgs<'objectMetadata'>['allFlatEntityOperationRecordByMetadataName'];
|
||||
relatedFlatEntityMaps: BuildSideEffectsArgs<'objectMetadata'>['relatedFlatEntityMaps'];
|
||||
}): FieldMetadataType | undefined {
|
||||
const pendingField =
|
||||
allFlatEntityOperationRecordByMetadataName.fieldMetadata
|
||||
?.flatEntityToCreate[fieldMetadataUniversalIdentifier];
|
||||
|
||||
if (isDefined(pendingField)) {
|
||||
return pendingField.type;
|
||||
}
|
||||
|
||||
const existingField =
|
||||
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier[
|
||||
fieldMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
return existingField?.type;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSystemFieldsOnCreateSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSystemFieldsOnCreate',
|
||||
description:
|
||||
'When an object is created, generate its 7 reserved system fields (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position). The searchVector field is provisioned by the self-contained objectSearchVectorOnCreate handler alongside its GIN index and searchFieldMetadata. The default name field is NOT synthesized here: it is a caller-provided default field (SDK auto-complete on the manifest path, input transpiler on the API path).',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatObjectMetadata,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const { applicationUniversalIdentifier, universalIdentifier } =
|
||||
flatObjectMetadata;
|
||||
|
||||
const reservedSystemFlatFieldMetadatas =
|
||||
buildReservedSystemFlatFieldMetadatasForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
const flatEntityToCreate: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'fieldMetadata'>
|
||||
> = {};
|
||||
|
||||
for (const flatFieldMetadata of Object.values(
|
||||
reservedSystemFlatFieldMetadatas,
|
||||
)) {
|
||||
flatEntityToCreate[flatFieldMetadata.universalIdentifier] =
|
||||
flatFieldMetadata;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations: {
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type MetadataUniversalFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-universal-flat-entity.type';
|
||||
import {
|
||||
type BuildSideEffectsArgs,
|
||||
MetadataSideEffectHandler,
|
||||
} from 'src/engine/metadata-modules/metadata-side-effect/interfaces/base-metadata-side-effect-handler.service';
|
||||
import { type MetadataSideEffectOperationsByMetadataName } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-operations-by-metadata-name.type';
|
||||
import { type MetadataSideEffectResult } from 'src/engine/metadata-modules/metadata-side-effect/types/metadata-side-effect-result.type';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectSystemSideEffectsOnDeleteSideEffectHandlerService extends MetadataSideEffectHandler(
|
||||
{
|
||||
operation: 'delete',
|
||||
metadataName: 'objectMetadata',
|
||||
name: 'objectSystemSideEffectsOnDelete',
|
||||
description:
|
||||
'When an object is deleted, cascade-delete its engine-owned side effects: the reserved system fields, every system index (including the GIN searchVector index), and its searchFieldMetadata rows. These entities are excluded from manifest deletion inference, so the cascade must be explicit. Caller-provided defaults (name, default relations) are NOT engine-owned and are deleted through normal deletion inference / the object delete transpiler.',
|
||||
},
|
||||
) {
|
||||
buildSideEffects({
|
||||
flatEntity: flatObjectMetadata,
|
||||
relatedFlatEntityMaps,
|
||||
}: BuildSideEffectsArgs<'objectMetadata'>): MetadataSideEffectResult {
|
||||
const objectMetadataUniversalIdentifier =
|
||||
flatObjectMetadata.universalIdentifier;
|
||||
|
||||
const fieldMetadataToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'fieldMetadata'>
|
||||
> = {};
|
||||
const deletedFieldUniversalIdentifiers = new Set<string>();
|
||||
|
||||
for (const flatFieldMetadata of Object.values(
|
||||
relatedFlatEntityMaps.flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flatFieldMetadata.isSystemSideEffect !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const belongsToObject =
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier;
|
||||
const targetsObject =
|
||||
flatFieldMetadata.relationTargetObjectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier;
|
||||
|
||||
if (!belongsToObject && !targetsObject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldMetadataToDelete[flatFieldMetadata.universalIdentifier] =
|
||||
flatFieldMetadata;
|
||||
deletedFieldUniversalIdentifiers.add(
|
||||
flatFieldMetadata.universalIdentifier,
|
||||
);
|
||||
}
|
||||
|
||||
const indexToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'index'>
|
||||
> = {};
|
||||
|
||||
for (const flatIndexMetadata of Object.values(
|
||||
relatedFlatEntityMaps.flatIndexMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatIndexMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (flatIndexMetadata.isSystemSideEffect !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const belongsToObject =
|
||||
flatIndexMetadata.objectMetadataUniversalIdentifier ===
|
||||
objectMetadataUniversalIdentifier;
|
||||
const referencesDeletedField =
|
||||
flatIndexMetadata.universalFlatIndexFieldMetadatas.some(
|
||||
(universalFlatIndexFieldMetadata) =>
|
||||
deletedFieldUniversalIdentifiers.has(
|
||||
universalFlatIndexFieldMetadata.fieldMetadataUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
if (!belongsToObject && !referencesDeletedField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
indexToDelete[flatIndexMetadata.universalIdentifier] = flatIndexMetadata;
|
||||
}
|
||||
|
||||
const searchFieldMetadataToDelete: Record<
|
||||
string,
|
||||
MetadataUniversalFlatEntity<'searchFieldMetadata'>
|
||||
> = {};
|
||||
|
||||
for (const flatSearchFieldMetadata of Object.values(
|
||||
relatedFlatEntityMaps.flatSearchFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatSearchFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
flatSearchFieldMetadata.objectMetadataUniversalIdentifier !==
|
||||
objectMetadataUniversalIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
searchFieldMetadataToDelete[flatSearchFieldMetadata.universalIdentifier] =
|
||||
flatSearchFieldMetadata;
|
||||
}
|
||||
|
||||
const hasFieldMetadataToDelete =
|
||||
Object.keys(fieldMetadataToDelete).length > 0;
|
||||
const hasIndexToDelete = Object.keys(indexToDelete).length > 0;
|
||||
const hasSearchFieldMetadataToDelete =
|
||||
Object.keys(searchFieldMetadataToDelete).length > 0;
|
||||
|
||||
if (
|
||||
!hasFieldMetadataToDelete &&
|
||||
!hasIndexToDelete &&
|
||||
!hasSearchFieldMetadataToDelete
|
||||
) {
|
||||
return { status: 'noop' };
|
||||
}
|
||||
|
||||
const operations: MetadataSideEffectOperationsByMetadataName = {
|
||||
...(hasFieldMetadataToDelete && {
|
||||
fieldMetadata: { flatEntityToDelete: fieldMetadataToDelete },
|
||||
}),
|
||||
...(hasIndexToDelete && {
|
||||
index: { flatEntityToDelete: indexToDelete },
|
||||
}),
|
||||
...(hasSearchFieldMetadataToDelete && {
|
||||
searchFieldMetadata: {
|
||||
flatEntityToDelete: searchFieldMetadataToDelete,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
operations,
|
||||
};
|
||||
}
|
||||
}
|
||||
-6
@@ -15,8 +15,6 @@ export enum ObjectMetadataExceptionCode {
|
||||
INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT',
|
||||
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||
NAME_CONFLICT = 'NAME_CONFLICT',
|
||||
MISSING_SYSTEM_FIELD = 'MISSING_SYSTEM_FIELD',
|
||||
INVALID_SYSTEM_FIELD = 'INVALID_SYSTEM_FIELD',
|
||||
}
|
||||
|
||||
const getObjectMetadataExceptionUserFriendlyMessage = (
|
||||
@@ -41,10 +39,6 @@ const getObjectMetadataExceptionUserFriendlyMessage = (
|
||||
return STANDARD_ERROR_MESSAGE;
|
||||
case ObjectMetadataExceptionCode.NAME_CONFLICT:
|
||||
return msg`A name conflict occurred.`;
|
||||
case ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
return msg`A system field is missing.`;
|
||||
case ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD:
|
||||
return msg`A system field has invalid properties.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+42
-25
@@ -42,6 +42,7 @@ import {
|
||||
ObjectMetadataException,
|
||||
ObjectMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { buildReservedSystemFlatFieldMetadatasForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-reserved-system-flat-field-metadatas-for-custom-object.util';
|
||||
import { computeFlatDefaultRecordPageLayoutToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-default-record-page-layout-to-create.util';
|
||||
import { computeFlatRecordPageFieldsViewToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-record-page-fields-view-to-create.util';
|
||||
import { computeFlatViewFieldsToCreate } from 'src/engine/metadata-modules/object-metadata/utils/compute-flat-view-fields-to-create.util';
|
||||
@@ -483,22 +484,43 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
ownerFlatApplication ?? workspaceCustomFlatApplication;
|
||||
|
||||
const { flatObjectMetadataMaps: existingFlatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
flatObjectMetadataToCreate,
|
||||
flatIndexMetadataToCreate,
|
||||
flatSearchFieldMetadataToCreate,
|
||||
flatFieldMetadataToCreateOnObject,
|
||||
relationTargetFlatFieldMetadataToCreate,
|
||||
flatIndexMetadataToCreate,
|
||||
} = fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate({
|
||||
createObjectInput,
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
// Default view fields reference the caller-provided fields (reused as-is from
|
||||
// the transpiler output) plus the engine-owned reserved system fields, whose
|
||||
// deterministic identifiers we re-derive here (searchVector is never shown).
|
||||
// TODO: remove once default view fields move to the metadata side effect engine.
|
||||
const defaultFlatFieldMetadatasForViewFields: UniversalFlatFieldMetadata[] =
|
||||
[
|
||||
...flatFieldMetadataToCreateOnObject,
|
||||
...Object.values(
|
||||
buildReservedSystemFlatFieldMetadatasForCustomObject({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier:
|
||||
resolvedOwnerFlatApplication.universalIdentifier,
|
||||
universalIdentifier:
|
||||
flatObjectMetadataToCreate.universalIdentifier,
|
||||
},
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
const flatDefaultViewToCreate = this.computeFlatViewToCreate({
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
@@ -506,7 +528,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
const flatDefaultViewFieldsToCreate = computeFlatViewFieldsToCreate({
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
|
||||
objectFlatFieldMetadatas: defaultFlatFieldMetadatasForViewFields,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier: flatDefaultViewToCreate.universalIdentifier,
|
||||
@@ -549,7 +571,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
const flatRecordPageFieldsViewFieldsToCreate =
|
||||
computeFlatViewFieldsToCreate({
|
||||
flatApplication: resolvedOwnerFlatApplication,
|
||||
objectFlatFieldMetadatas: flatFieldMetadataToCreateOnObject,
|
||||
objectFlatFieldMetadatas: defaultFlatFieldMetadatasForViewFields,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
flatObjectMetadataToCreate.labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
viewUniversalIdentifier:
|
||||
@@ -574,6 +596,19 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: [
|
||||
...flatFieldMetadataToCreateOnObject,
|
||||
...relationTargetFlatFieldMetadataToCreate,
|
||||
],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
index: {
|
||||
flatEntityToCreate: flatIndexMetadataToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
view: {
|
||||
flatEntityToCreate: [
|
||||
flatDefaultViewToCreate,
|
||||
@@ -590,19 +625,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
fieldMetadata: {
|
||||
flatEntityToCreate: [
|
||||
...flatFieldMetadataToCreateOnObject,
|
||||
...relationTargetFlatFieldMetadataToCreate,
|
||||
],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
index: {
|
||||
flatEntityToCreate: flatIndexMetadataToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
commandMenuItem: {
|
||||
flatEntityToCreate: [flatCommandMenuItemToCreate],
|
||||
flatEntityToDelete: [],
|
||||
@@ -626,11 +648,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
searchFieldMetadata: {
|
||||
flatEntityToCreate: flatSearchFieldMetadataToCreate,
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
...(isDefined(flatNavigationMenuItemToCreate)
|
||||
? {
|
||||
navigationMenuItem: {
|
||||
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
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';
|
||||
|
||||
type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
|
||||
flatObjectMetadata: Pick<
|
||||
UniversalFlatObjectMetadata,
|
||||
'universalIdentifier' | 'applicationUniversalIdentifier'
|
||||
>;
|
||||
skipNameField?: boolean;
|
||||
};
|
||||
|
||||
export type DefaultFlatFieldForCustomObjectMaps = ReturnType<
|
||||
typeof buildDefaultFlatFieldMetadatasForCustomObject
|
||||
>;
|
||||
|
||||
const buildObjectSystemFlatFieldMetadatas = ({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
now,
|
||||
searchVectorUniversalSettings,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
now: string;
|
||||
searchVectorUniversalSettings: UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR>['universalSettings'];
|
||||
}) => {
|
||||
const {
|
||||
createdAt,
|
||||
createdBy,
|
||||
deletedAt,
|
||||
id,
|
||||
position,
|
||||
searchVector,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
} = PARTIAL_SYSTEM_FLAT_FIELD_METADATAS;
|
||||
|
||||
const computeFieldUniversalIdentifier = (name: string) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name,
|
||||
});
|
||||
|
||||
return {
|
||||
id: {
|
||||
...id,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(id.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
createdAt: {
|
||||
...createdAt,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(createdAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
createdBy: {
|
||||
...createdBy,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(createdBy.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
deletedAt: {
|
||||
...deletedAt,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(deletedAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
position: {
|
||||
...position,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(position.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
searchVector: {
|
||||
...searchVector,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(searchVector.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
universalSettings: searchVectorUniversalSettings,
|
||||
},
|
||||
updatedAt: {
|
||||
...updatedAt,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(updatedAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
updatedBy: {
|
||||
...updatedBy,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(updatedBy.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
} as const satisfies Record<string, UniversalFlatFieldMetadata>;
|
||||
};
|
||||
|
||||
// This could be replaced totally by an import schema + its transpilation when it's ready
|
||||
export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier: objectMetadataUniversalIdentifier,
|
||||
},
|
||||
skipNameField = false,
|
||||
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const nameField: UniversalFlatFieldMetadata<FieldMetadataType.TEXT> | null =
|
||||
skipNameField
|
||||
? null
|
||||
: {
|
||||
type: FieldMetadataType.TEXT,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name: 'name',
|
||||
}),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
icon: 'IconAbc',
|
||||
description: 'Name',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isSystemSideEffect: true,
|
||||
isUIEditable: true,
|
||||
defaultValue: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
options: null,
|
||||
overrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
viewSortUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
};
|
||||
|
||||
const searchVectorUniversalSettings: UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR>['universalSettings'] =
|
||||
null;
|
||||
|
||||
return {
|
||||
fields: {
|
||||
...(nameField && { nameField }),
|
||||
...buildObjectSystemFlatFieldMetadatas({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
now,
|
||||
searchVectorUniversalSettings,
|
||||
}),
|
||||
},
|
||||
} as const satisfies {
|
||||
fields: Record<string, UniversalFlatFieldMetadata>;
|
||||
};
|
||||
};
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateFlatIndexMetadataWithNameOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util';
|
||||
import { type DefaultFlatFieldForCustomObjectMaps } from 'src/engine/metadata-modules/object-metadata/utils/build-default-flat-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';
|
||||
|
||||
export const buildDefaultIndexesForCustomObject = ({
|
||||
flatObjectMetadata,
|
||||
defaultFlatFieldForCustomObjectMaps,
|
||||
objectFlatFieldMetadatas,
|
||||
}: {
|
||||
flatObjectMetadata: UniversalFlatObjectMetadata & { id: string };
|
||||
objectFlatFieldMetadatas: UniversalFlatFieldMetadata[];
|
||||
defaultFlatFieldForCustomObjectMaps: DefaultFlatFieldForCustomObjectMaps;
|
||||
}) => {
|
||||
const tsFlatVectorIndexUniversalIdentifier = v4();
|
||||
const createdAt = new Date();
|
||||
const tsVectorFlatIndex = generateFlatIndexMetadataWithNameOrThrow({
|
||||
objectFlatFieldMetadatas,
|
||||
flatIndex: {
|
||||
createdAt: createdAt.toISOString(),
|
||||
universalFlatIndexFieldMetadatas: [
|
||||
{
|
||||
createdAt: createdAt.toISOString(),
|
||||
fieldMetadataUniversalIdentifier:
|
||||
defaultFlatFieldForCustomObjectMaps.fields.searchVector
|
||||
.universalIdentifier,
|
||||
indexMetadataUniversalIdentifier:
|
||||
tsFlatVectorIndexUniversalIdentifier,
|
||||
order: 0,
|
||||
subFieldName: null,
|
||||
updatedAt: createdAt.toISOString(),
|
||||
},
|
||||
],
|
||||
|
||||
indexType: IndexType.GIN,
|
||||
indexWhereClause: null,
|
||||
isCustom: false,
|
||||
isUnique: false,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
universalIdentifier: tsFlatVectorIndexUniversalIdentifier,
|
||||
updatedAt: createdAt.toISOString(),
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
},
|
||||
flatObjectMetadata,
|
||||
});
|
||||
|
||||
return {
|
||||
indexes: {
|
||||
tsVectorFlatIndex,
|
||||
},
|
||||
} as const satisfies { indexes: Record<string, UniversalFlatIndexMetadata> };
|
||||
};
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
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,
|
||||
tsVectorFlatFieldMetadata:
|
||||
defaultFlatFieldForCustomObjectMaps.fields.searchVector,
|
||||
position: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
searchFieldMetadatas: [nameSearchFieldMetadata],
|
||||
};
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
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';
|
||||
|
||||
type BuildNameFlatFieldMetadataForCustomObjectArgs = {
|
||||
flatObjectMetadata: Pick<
|
||||
UniversalFlatObjectMetadata,
|
||||
'universalIdentifier' | 'applicationUniversalIdentifier'
|
||||
>;
|
||||
};
|
||||
|
||||
export const buildNameFlatFieldMetadataForCustomObject = ({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier: objectMetadataUniversalIdentifier,
|
||||
},
|
||||
}: BuildNameFlatFieldMetadataForCustomObjectArgs): UniversalFlatFieldMetadata<FieldMetadataType.TEXT> => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return {
|
||||
type: FieldMetadataType.TEXT,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name: 'name',
|
||||
}),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
icon: 'IconAbc',
|
||||
description: 'Name',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isSystemSideEffect: false,
|
||||
isUIEditable: true,
|
||||
defaultValue: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
options: null,
|
||||
overrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
fieldPermissionUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
viewSortUniversalIdentifiers: [],
|
||||
searchFieldMetadataUniversalIdentifiers: [],
|
||||
};
|
||||
};
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
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';
|
||||
|
||||
type BuildReservedSystemFlatFieldMetadatasForCustomObjectArgs = {
|
||||
flatObjectMetadata: Pick<
|
||||
UniversalFlatObjectMetadata,
|
||||
'universalIdentifier' | 'applicationUniversalIdentifier'
|
||||
>;
|
||||
};
|
||||
|
||||
export const buildReservedSystemFlatFieldMetadatasForCustomObject = ({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier: objectMetadataUniversalIdentifier,
|
||||
},
|
||||
}: BuildReservedSystemFlatFieldMetadatasForCustomObjectArgs): Record<
|
||||
string,
|
||||
UniversalFlatFieldMetadata
|
||||
> => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const {
|
||||
createdAt,
|
||||
createdBy,
|
||||
deletedAt,
|
||||
id,
|
||||
position,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
} = PARTIAL_SYSTEM_FLAT_FIELD_METADATAS;
|
||||
|
||||
const computeFieldUniversalIdentifier = (name: string) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name,
|
||||
});
|
||||
|
||||
return {
|
||||
id: {
|
||||
...id,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(id.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
createdAt: {
|
||||
...createdAt,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(createdAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
createdBy: {
|
||||
...createdBy,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(createdBy.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
deletedAt: {
|
||||
...deletedAt,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(deletedAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
position: {
|
||||
...position,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(position.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
updatedAt: {
|
||||
...updatedAt,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(updatedAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
updatedBy: {
|
||||
...updatedBy,
|
||||
universalIdentifier: computeFieldUniversalIdentifier(updatedBy.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
} as const satisfies Record<string, UniversalFlatFieldMetadata>;
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
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';
|
||||
|
||||
type BuildSearchVectorFlatFieldMetadataForCustomObjectArgs = {
|
||||
flatObjectMetadata: Pick<
|
||||
UniversalFlatObjectMetadata,
|
||||
'universalIdentifier' | 'applicationUniversalIdentifier'
|
||||
>;
|
||||
};
|
||||
|
||||
export const buildSearchVectorFlatFieldMetadataForCustomObject = ({
|
||||
flatObjectMetadata: {
|
||||
applicationUniversalIdentifier,
|
||||
universalIdentifier: objectMetadataUniversalIdentifier,
|
||||
},
|
||||
}: BuildSearchVectorFlatFieldMetadataForCustomObjectArgs): UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR> => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const { searchVector } = PARTIAL_SYSTEM_FLAT_FIELD_METADATAS;
|
||||
|
||||
return {
|
||||
...searchVector,
|
||||
universalIdentifier: getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name: searchVector.name,
|
||||
}),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
universalSettings: null,
|
||||
};
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { generateDeterministicFlatIndexMetadataOrThrow } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-flat-index.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';
|
||||
|
||||
export const buildSearchVectorGinIndexForCustomObject = ({
|
||||
flatObjectMetadata,
|
||||
searchVectorFlatFieldMetadata,
|
||||
}: {
|
||||
flatObjectMetadata: UniversalFlatObjectMetadata;
|
||||
searchVectorFlatFieldMetadata: UniversalFlatFieldMetadata;
|
||||
}): UniversalFlatIndexMetadata => {
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
return generateDeterministicFlatIndexMetadataOrThrow({
|
||||
objectFlatFieldMetadatas: [searchVectorFlatFieldMetadata],
|
||||
flatIndex: {
|
||||
createdAt,
|
||||
universalFlatIndexFieldMetadatas: [
|
||||
{
|
||||
createdAt,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
searchVectorFlatFieldMetadata.universalIdentifier,
|
||||
order: 0,
|
||||
subFieldName: null,
|
||||
updatedAt: createdAt,
|
||||
},
|
||||
],
|
||||
indexType: IndexType.GIN,
|
||||
indexWhereClause: null,
|
||||
isCustom: false,
|
||||
isUnique: false,
|
||||
isSystemSideEffect: true,
|
||||
objectMetadataUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
updatedAt: createdAt,
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
},
|
||||
flatObjectMetadata,
|
||||
});
|
||||
};
|
||||
-2
@@ -14,8 +14,6 @@ export const objectMetadataExceptionCodeToHttpStatus = (
|
||||
case ObjectMetadataExceptionCode.NAME_CONFLICT:
|
||||
return 403;
|
||||
case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT:
|
||||
case ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
case ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD:
|
||||
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
||||
case ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND:
|
||||
return 400;
|
||||
|
||||
-3
@@ -41,9 +41,6 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
||||
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
||||
case ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND:
|
||||
throw error;
|
||||
case ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
case ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
+8
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import { ADD_UNIVERSAL_IDENTIFIER_AND_APPLICATION_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-16/add-universal-identifier-and-application-id-to-search-field-metadata-upgrade-command-name.constant';
|
||||
import { ADD_TS_VECTOR_FIELD_METADATA_ID_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-18/add-ts-vector-field-metadata-id-to-search-field-metadata-upgrade-command-name.constant';
|
||||
import { ADD_IS_SYSTEM_SIDE_EFFECT_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-20/add-is-system-side-effect-to-search-field-metadata-upgrade-command-name.constant';
|
||||
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -69,6 +70,13 @@ export class SearchFieldMetadataEntity extends SyncableEntity {
|
||||
@Column({ nullable: false, type: 'float' })
|
||||
position: number;
|
||||
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName:
|
||||
ADD_IS_SYSTEM_SIDE_EFFECT_TO_SEARCH_FIELD_METADATA_UPGRADE_COMMAND_NAME,
|
||||
})
|
||||
@Column({ nullable: false, default: true, type: 'boolean' })
|
||||
isSystemSideEffect: boolean;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ export const buildCompanyStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export const buildDashboardStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ export const buildMessageCampaignStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export const buildMessageListStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export const buildNoteStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -60,6 +60,7 @@ export const buildOpportunityStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ export const buildPersonStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ export const buildTaskStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ export const buildWorkflowRunStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export const buildWorkflowStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ export const buildWorkflowVersionStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ export const buildWorkspaceMemberStandardFlatIndexMetadatas = ({
|
||||
indexName: 'searchVectorGinIndex',
|
||||
relatedFieldNames: ['searchVector'],
|
||||
indexType: IndexType.GIN,
|
||||
hasDeterministicUniversalIdentifier: true,
|
||||
},
|
||||
standardObjectMetadataRelatedEntityIds,
|
||||
dependencyFlatEntityMaps,
|
||||
|
||||
+7
-1
@@ -1,3 +1,4 @@
|
||||
import { getSearchFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -67,7 +68,11 @@ export const createStandardSearchFieldFlatMetadata = <
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: getSearchFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
fieldMetadataUniversalIdentifier: flatFieldMetadata.universalIdentifier,
|
||||
}),
|
||||
applicationId: twentyStandardApplicationId,
|
||||
applicationUniversalIdentifier:
|
||||
flatObjectMetadata.applicationUniversalIdentifier,
|
||||
@@ -78,6 +83,7 @@ export const createStandardSearchFieldFlatMetadata = <
|
||||
tsVectorFieldMetadataId,
|
||||
tsVectorFieldMetadataUniversalIdentifier,
|
||||
position,
|
||||
isSystemSideEffect: true,
|
||||
workspaceId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
-577
@@ -1,577 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity should fail when trying to delete a system field after a successful sync 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_MUTATION_NOT_ALLOWED",
|
||||
"message": "System fields cannot be deleted",
|
||||
"userFriendlyMessage": "System fields cannot be deleted",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "id",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "fieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "delete",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 fieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System fields cannot be deleted",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity should fail when trying to update a system field after a successful sync 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "FIELD_MUTATION_NOT_ALLOWED",
|
||||
"message": "System fields only allow updating: universalSettings, isActive. Forbidden properties: label",
|
||||
"userFriendlyMessage": "System fields cannot be updated",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "id",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "fieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "update",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 fieldMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System fields cannot be updated",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when label identifier is non-searchable type (searchVector has no expression) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_OBJECT_INPUT",
|
||||
"message": "labelIdentifierFieldMetadataUniversalIdentifier validation failed: field type not compatible",
|
||||
"userFriendlyMessage": "Field cannot be used as label identifier due to its type: should be of type UUID, text or full name",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "noSearchVectorExpressions",
|
||||
"nameSingular": "noSearchVectorExpression",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "Field cannot be used as label identifier due to its type: should be of type UUID, text or full name",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has TS_VECTOR field with wrong name (not searchVector) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"fieldMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Field type TS_VECTOR must be named "searchVector", got "wrongSearchVector"",
|
||||
"userFriendlyMessage": "Field type TS_VECTOR must be named "searchVector"",
|
||||
"value": "wrongSearchVector",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_FIELD_INPUT",
|
||||
"message": "Field type TS_VECTOR must be a system field",
|
||||
"userFriendlyMessage": "Field type TS_VECTOR must be a system field",
|
||||
"value": false,
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "wrongSearchVector",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "fieldMetadata",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field searchVector is missing",
|
||||
"userFriendlyMessage": "System field searchVector is missing",
|
||||
"value": "searchVector",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongTsVectorNames",
|
||||
"nameSingular": "wrongTsVectorName",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 fieldMetadata, 1 objectMetadata",
|
||||
"summary": {
|
||||
"fieldMetadata": 1,
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 2,
|
||||
},
|
||||
"userFriendlyMessage": "Many validation errors",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has a system field with a custom (non-derived) universal identifier 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_SYSTEM_FIELD",
|
||||
"message": "System field createdAt has invalid universalIdentifier: expected 62e9d388-38c0-5cd8-b18d-db186455a563, got 899ac540-3a1f-42cf-9f99-8a55e79d0d9e",
|
||||
"userFriendlyMessage": "System field createdAt universal identifier is not deterministic; it is derived by the server and cannot be customized",
|
||||
"value": "899ac540-3a1f-42cf-9f99-8a55e79d0d9e",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "customSystemFieldUidObjects",
|
||||
"nameSingular": "customSystemFieldUidObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System field createdAt universal identifier is not deterministic; it is derived by the server and cannot be customized",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has id field with wrong type (TEXT instead of UUID) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_SYSTEM_FIELD",
|
||||
"message": "System field id has invalid type: expected UUID, got TEXT",
|
||||
"userFriendlyMessage": "System field id has invalid type",
|
||||
"value": "TEXT",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_SYSTEM_FIELD",
|
||||
"message": "System field id has invalid universalIdentifier: expected 64e60b60-2bc9-5eac-a98b-83c1d628e326, got 4254b620-34f1-57d1-a155-527015f523db",
|
||||
"userFriendlyMessage": "System field id universal identifier is not deterministic; it is derived by the server and cannot be customized",
|
||||
"value": "4254b620-34f1-57d1-a155-527015f523db",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdAt is missing",
|
||||
"userFriendlyMessage": "System field createdAt is missing",
|
||||
"value": "createdAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedAt is missing",
|
||||
"userFriendlyMessage": "System field updatedAt is missing",
|
||||
"value": "updatedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field deletedAt is missing",
|
||||
"userFriendlyMessage": "System field deletedAt is missing",
|
||||
"value": "deletedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdBy is missing",
|
||||
"userFriendlyMessage": "System field createdBy is missing",
|
||||
"value": "createdBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedBy is missing",
|
||||
"userFriendlyMessage": "System field updatedBy is missing",
|
||||
"value": "updatedBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field position is missing",
|
||||
"userFriendlyMessage": "System field position is missing",
|
||||
"value": "position",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field searchVector is missing",
|
||||
"userFriendlyMessage": "System field searchVector is missing",
|
||||
"value": "searchVector",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongIdTypeObjects",
|
||||
"nameSingular": "wrongIdTypeObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System field id has invalid type",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has position field with wrong type (TEXT instead of POSITION) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field id is missing",
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
"value": "id",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdAt is missing",
|
||||
"userFriendlyMessage": "System field createdAt is missing",
|
||||
"value": "createdAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedAt is missing",
|
||||
"userFriendlyMessage": "System field updatedAt is missing",
|
||||
"value": "updatedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field deletedAt is missing",
|
||||
"userFriendlyMessage": "System field deletedAt is missing",
|
||||
"value": "deletedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdBy is missing",
|
||||
"userFriendlyMessage": "System field createdBy is missing",
|
||||
"value": "createdBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedBy is missing",
|
||||
"userFriendlyMessage": "System field updatedBy is missing",
|
||||
"value": "updatedBy",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_SYSTEM_FIELD",
|
||||
"message": "System field position has invalid type: expected POSITION, got TEXT",
|
||||
"userFriendlyMessage": "System field position has invalid type",
|
||||
"value": "TEXT",
|
||||
},
|
||||
{
|
||||
"code": "INVALID_SYSTEM_FIELD",
|
||||
"message": "System field position has invalid universalIdentifier: expected dbd9f170-1d32-5267-b76e-e1f172ab617a, got 65eb86b6-1b74-58bd-8c02-7f79b018359d",
|
||||
"userFriendlyMessage": "System field position universal identifier is not deterministic; it is derived by the server and cannot be customized",
|
||||
"value": "65eb86b6-1b74-58bd-8c02-7f79b018359d",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field searchVector is missing",
|
||||
"userFriendlyMessage": "System field searchVector is missing",
|
||||
"value": "searchVector",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongPositionObjects",
|
||||
"nameSingular": "wrongPositionObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object has searchVector field with wrong type (TEXT instead of TS_VECTOR) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INVALID_SYSTEM_FIELD",
|
||||
"message": "System field searchVector has invalid type: expected TS_VECTOR, got TEXT",
|
||||
"userFriendlyMessage": "System field searchVector has invalid type",
|
||||
"value": "TEXT",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongSearchVectorTypes",
|
||||
"nameSingular": "wrongSearchVectorType",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System field searchVector has invalid type",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object is created without any system fields (missing all 8 system fields) 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field id is missing",
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
"value": "id",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdAt is missing",
|
||||
"userFriendlyMessage": "System field createdAt is missing",
|
||||
"value": "createdAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedAt is missing",
|
||||
"userFriendlyMessage": "System field updatedAt is missing",
|
||||
"value": "updatedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field deletedAt is missing",
|
||||
"userFriendlyMessage": "System field deletedAt is missing",
|
||||
"value": "deletedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdBy is missing",
|
||||
"userFriendlyMessage": "System field createdBy is missing",
|
||||
"value": "createdBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedBy is missing",
|
||||
"userFriendlyMessage": "System field updatedBy is missing",
|
||||
"value": "updatedBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field position is missing",
|
||||
"userFriendlyMessage": "System field position is missing",
|
||||
"value": "position",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field searchVector is missing",
|
||||
"userFriendlyMessage": "System field searchVector is missing",
|
||||
"value": "searchVector",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "noSystemFieldsObjects",
|
||||
"nameSingular": "noSystemFieldsObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Sync application should fail due to object system fields integrity when object miss default fields 1`] = `
|
||||
{
|
||||
"extensions": {
|
||||
"code": "METADATA_VALIDATION_FAILED",
|
||||
"errors": {
|
||||
"objectMetadata": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field id is missing",
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
"value": "id",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdAt is missing",
|
||||
"userFriendlyMessage": "System field createdAt is missing",
|
||||
"value": "createdAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedAt is missing",
|
||||
"userFriendlyMessage": "System field updatedAt is missing",
|
||||
"value": "updatedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field deletedAt is missing",
|
||||
"userFriendlyMessage": "System field deletedAt is missing",
|
||||
"value": "deletedAt",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field createdBy is missing",
|
||||
"userFriendlyMessage": "System field createdBy is missing",
|
||||
"value": "createdBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field updatedBy is missing",
|
||||
"userFriendlyMessage": "System field updatedBy is missing",
|
||||
"value": "updatedBy",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field position is missing",
|
||||
"userFriendlyMessage": "System field position is missing",
|
||||
"value": "position",
|
||||
},
|
||||
{
|
||||
"code": "MISSING_SYSTEM_FIELD",
|
||||
"message": "System field searchVector is missing",
|
||||
"userFriendlyMessage": "System field searchVector is missing",
|
||||
"value": "searchVector",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"namePlural": "wrongCreatedAtObjects",
|
||||
"nameSingular": "wrongCreatedAtObject",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "objectMetadata",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
},
|
||||
"message": "Validation failed for 1 objectMetadata",
|
||||
"summary": {
|
||||
"objectMetadata": 1,
|
||||
"totalErrors": 1,
|
||||
},
|
||||
"userFriendlyMessage": "System field id is missing",
|
||||
},
|
||||
"message": "Validation errors occurred while syncing application manifest metadata",
|
||||
"name": "GraphQLError",
|
||||
}
|
||||
`;
|
||||
+54
@@ -347,6 +347,33 @@ exports[`syncApplication should delete old field and create equivalent one when
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
"createdAt": Any<String>,
|
||||
"indexType": "GIN",
|
||||
"indexWhereClause": null,
|
||||
"isCustom": false,
|
||||
"isSystemSideEffect": true,
|
||||
"isUnique": false,
|
||||
"name": "IDX_72b74c26016bcd3cd20b85c4a28",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalFlatIndexFieldMetadatas": [
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"fieldMetadataUniversalIdentifier": Any<String>,
|
||||
"indexMetadataUniversalIdentifier": Any<String>,
|
||||
"order": 0,
|
||||
"subFieldName": null,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
],
|
||||
"universalIdentifier": Any<String>,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
@@ -754,6 +781,33 @@ exports[`syncApplication should return workspace migration actions on initial sy
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
"createdAt": Any<String>,
|
||||
"indexType": "GIN",
|
||||
"indexWhereClause": null,
|
||||
"isCustom": false,
|
||||
"isSystemSideEffect": true,
|
||||
"isUnique": false,
|
||||
"name": "IDX_72b74c26016bcd3cd20b85c4a28",
|
||||
"objectMetadataUniversalIdentifier": Any<String>,
|
||||
"universalFlatIndexFieldMetadatas": [
|
||||
{
|
||||
"createdAt": Any<String>,
|
||||
"fieldMetadataUniversalIdentifier": Any<String>,
|
||||
"indexMetadataUniversalIdentifier": Any<String>,
|
||||
"order": 0,
|
||||
"subFieldName": null,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
],
|
||||
"universalIdentifier": Any<String>,
|
||||
"updatedAt": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"flatEntity": {
|
||||
"applicationUniversalIdentifier": Any<String>,
|
||||
|
||||
-460
@@ -1,460 +0,0 @@
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { type Manifest, type ObjectManifest } from 'twenty-shared/application';
|
||||
import {
|
||||
type EachTestingContext,
|
||||
eachTestingContextFilter,
|
||||
} from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
|
||||
|
||||
// Identifiers are pinned so validation error messages embedding expected and
|
||||
// actual universal identifiers stay stable across snapshot runs.
|
||||
const TEST_APP_ID = '4e0e42a8-8f9c-4a48-9d43-5e0c5c2f4a10';
|
||||
const TEST_ROLE_ID = 'd0a24fbc-4b26-42a5-a4ff-2e142f8e2f6d';
|
||||
const TEST_UUID_NAMESPACE = '6a9c8f74-4b7a-4a86-90f4-2f4dbb1c2a30';
|
||||
|
||||
const computeDeterministicTestUuid = (seed: string) =>
|
||||
uuidv5(seed, TEST_UUID_NAMESPACE);
|
||||
|
||||
type TestContext = {
|
||||
manifest: Manifest;
|
||||
};
|
||||
|
||||
type SyncApplicationTestingContext = EachTestingContext<TestContext>[];
|
||||
|
||||
const buildManifest = (overrides: Pick<Manifest, 'objects' | 'fields'>) =>
|
||||
buildBaseManifest({
|
||||
appId: TEST_APP_ID,
|
||||
roleId: TEST_ROLE_ID,
|
||||
overrides,
|
||||
});
|
||||
|
||||
const buildObjectWithLabelField = ({
|
||||
nameSingular,
|
||||
namePlural,
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
description,
|
||||
additionalFields = [],
|
||||
}: {
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description: string;
|
||||
additionalFields?: ObjectManifest['fields'];
|
||||
}): Pick<Manifest, 'objects' | 'fields'> => {
|
||||
const objectId = computeDeterministicTestUuid(nameSingular);
|
||||
const labelFieldId = computeDeterministicTestUuid(`${nameSingular}-title`);
|
||||
|
||||
return {
|
||||
objects: [
|
||||
{
|
||||
universalIdentifier: objectId,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: labelFieldId,
|
||||
nameSingular,
|
||||
namePlural,
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
description,
|
||||
icon: 'IconTicket',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: labelFieldId,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
description: 'Label identifier field',
|
||||
icon: 'IconTextCaption',
|
||||
},
|
||||
...additionalFields,
|
||||
],
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
};
|
||||
};
|
||||
|
||||
const buildDefaultObjectWithModifiedSearchVector = ({
|
||||
nameSingular,
|
||||
namePlural,
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
description,
|
||||
searchVectorOverrides,
|
||||
}: {
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description: string;
|
||||
searchVectorOverrides: Partial<ObjectManifest['fields'][number]>;
|
||||
}): Pick<Manifest, 'objects' | 'fields'> => {
|
||||
const defaultObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
universalIdentifier: computeDeterministicTestUuid(nameSingular),
|
||||
nameSingular,
|
||||
namePlural,
|
||||
labelSingular,
|
||||
labelPlural,
|
||||
description,
|
||||
});
|
||||
|
||||
return {
|
||||
objects: [
|
||||
{
|
||||
...defaultObject,
|
||||
fields: defaultObject.fields.map((field) =>
|
||||
field.name === 'searchVector'
|
||||
? ({
|
||||
...field,
|
||||
...searchVectorOverrides,
|
||||
} as (typeof defaultObject.fields)[number])
|
||||
: field,
|
||||
),
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
};
|
||||
};
|
||||
|
||||
const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext =
|
||||
[
|
||||
{
|
||||
title:
|
||||
'when object is created without any system fields (missing all 8 system fields)',
|
||||
context: {
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'noSystemFieldsObject',
|
||||
namePlural: 'noSystemFieldsObjects',
|
||||
labelSingular: 'No System Fields Object',
|
||||
labelPlural: 'No System Fields Objects',
|
||||
description: 'Object with no system fields',
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when object has id field with wrong type (TEXT instead of UUID)',
|
||||
context: {
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'wrongIdTypeObject',
|
||||
namePlural: 'wrongIdTypeObjects',
|
||||
labelSingular: 'Wrong Id Type Object',
|
||||
labelPlural: 'Wrong Id Type Objects',
|
||||
description: 'Object with wrong id field type',
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier:
|
||||
computeDeterministicTestUuid('wrongIdTypeObject-id'),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id field with wrong type',
|
||||
icon: 'IconKey',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when object miss default fields',
|
||||
context: {
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'wrongCreatedAtObject',
|
||||
namePlural: 'wrongCreatedAtObjects',
|
||||
labelSingular: 'Wrong CreatedAt Object',
|
||||
labelPlural: 'Wrong CreatedAt Objects',
|
||||
description: 'Object with wrong createdAt field type',
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when object has position field with wrong type (TEXT instead of POSITION)',
|
||||
context: {
|
||||
manifest: buildManifest(
|
||||
buildObjectWithLabelField({
|
||||
nameSingular: 'wrongPositionObject',
|
||||
namePlural: 'wrongPositionObjects',
|
||||
labelSingular: 'Wrong Position Object',
|
||||
labelPlural: 'Wrong Position Objects',
|
||||
description: 'Object with wrong position field type',
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: computeDeterministicTestUuid(
|
||||
'wrongPositionObject-position',
|
||||
),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
description: 'Position field with wrong type',
|
||||
icon: 'IconArrowsSort',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when object has searchVector field with wrong type (TEXT instead of TS_VECTOR)',
|
||||
context: {
|
||||
manifest: buildManifest(
|
||||
buildDefaultObjectWithModifiedSearchVector({
|
||||
nameSingular: 'wrongSearchVectorType',
|
||||
namePlural: 'wrongSearchVectorTypes',
|
||||
labelSingular: 'Wrong SearchVector Type',
|
||||
labelPlural: 'Wrong SearchVector Types',
|
||||
description: 'Object with wrong searchVector field type',
|
||||
searchVectorOverrides: {
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when object has TS_VECTOR field with wrong name (not searchVector)',
|
||||
context: {
|
||||
manifest: buildManifest(
|
||||
buildDefaultObjectWithModifiedSearchVector({
|
||||
nameSingular: 'wrongTsVectorName',
|
||||
namePlural: 'wrongTsVectorNames',
|
||||
labelSingular: 'Wrong TsVector Name',
|
||||
labelPlural: 'Wrong TsVector Names',
|
||||
description: 'Object with TS_VECTOR field named incorrectly',
|
||||
searchVectorOverrides: {
|
||||
name: 'wrongSearchVector',
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when object has a system field with a custom (non-derived) universal identifier',
|
||||
context: (() => {
|
||||
const defaultObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
universalIdentifier: computeDeterministicTestUuid(
|
||||
'customSystemFieldUidObject',
|
||||
),
|
||||
nameSingular: 'customSystemFieldUidObject',
|
||||
namePlural: 'customSystemFieldUidObjects',
|
||||
labelSingular: 'Custom System Field Uid Object',
|
||||
labelPlural: 'Custom System Field Uid Objects',
|
||||
description:
|
||||
'Object with a createdAt system field carrying a custom universal identifier',
|
||||
});
|
||||
|
||||
return {
|
||||
manifest: buildManifest({
|
||||
objects: [
|
||||
{
|
||||
...defaultObject,
|
||||
fields: defaultObject.fields.map((field) =>
|
||||
field.name === 'createdAt'
|
||||
? {
|
||||
...field,
|
||||
universalIdentifier:
|
||||
'899ac540-3a1f-42cf-9f99-8a55e79d0d9e',
|
||||
}
|
||||
: field,
|
||||
),
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
}),
|
||||
};
|
||||
})(),
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when label identifier is non-searchable type (searchVector has no expression)',
|
||||
context: (() => {
|
||||
const nonSearchableFieldId = computeDeterministicTestUuid(
|
||||
'noSearchVectorExpression-quantity',
|
||||
);
|
||||
|
||||
return {
|
||||
manifest: buildManifest({
|
||||
objects: [
|
||||
buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
universalIdentifier: computeDeterministicTestUuid(
|
||||
'noSearchVectorExpression',
|
||||
),
|
||||
nameSingular: 'noSearchVectorExpression',
|
||||
namePlural: 'noSearchVectorExpressions',
|
||||
labelSingular: 'No SearchVector Expression',
|
||||
labelPlural: 'No SearchVector Expressions',
|
||||
description:
|
||||
'Object whose label identifier is non-searchable, so searchVector has no expression',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
nonSearchableFieldId,
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: nonSearchableFieldId,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'quantity',
|
||||
label: 'Quantity',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
fields: [],
|
||||
}),
|
||||
};
|
||||
})(),
|
||||
},
|
||||
];
|
||||
|
||||
describe('Sync application should fail due to object system fields integrity', () => {
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
name: 'Test System Fields App',
|
||||
description: 'App for testing system field validation',
|
||||
sourcePath: 'test-system-fields',
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(
|
||||
eachTestingContextFilter(failingSyncApplicationSystemFieldsTestCases),
|
||||
)(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const { errors } = await syncApplication({
|
||||
manifest: context.manifest,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
},
|
||||
60000,
|
||||
);
|
||||
|
||||
it('should fail when trying to delete a system field after a successful sync', async () => {
|
||||
const labelIdentifierFieldUniversalIdentifier = uuidv4();
|
||||
const testObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'deleteSystemFieldObject',
|
||||
namePlural: 'deleteSystemFieldObjects',
|
||||
labelSingular: 'Delete System Field Object',
|
||||
labelPlural: 'Delete System Field Objects',
|
||||
description: 'Object for testing system field deletion',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelIdentifierFieldUniversalIdentifier,
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: labelIdentifierFieldUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'labelIdentifierField',
|
||||
label: 'Label Identifier Field',
|
||||
description: 'Label identifier field',
|
||||
icon: 'IconTextCaption',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const validManifest = buildManifest({
|
||||
objects: [testObject],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: validManifest,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const manifestWithDeletedIdField = buildManifest({
|
||||
objects: [
|
||||
{
|
||||
...testObject,
|
||||
fields: testObject.fields.filter((field) => field.name !== 'id'),
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest: manifestWithDeletedIdField,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
}, 60000);
|
||||
|
||||
it('should fail when trying to update a system field after a successful sync', async () => {
|
||||
const labelIdentifierFieldUniversalIdentifier = uuidv4();
|
||||
const testObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'updateSystemFieldObject',
|
||||
namePlural: 'updateSystemFieldObjects',
|
||||
labelSingular: 'Update System Field Object',
|
||||
labelPlural: 'Update System Field Objects',
|
||||
description: 'Object for testing system field update',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelIdentifierFieldUniversalIdentifier,
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: labelIdentifierFieldUniversalIdentifier,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'labelIdentifierField',
|
||||
label: 'Label Identifier Field',
|
||||
description: 'Label identifier field',
|
||||
icon: 'IconTextCaption',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const validManifest = buildManifest({
|
||||
objects: [testObject],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: validManifest,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const manifestWithUpdatedIdField = buildManifest({
|
||||
objects: [
|
||||
{
|
||||
...testObject,
|
||||
fields: testObject.fields.map((field) =>
|
||||
field.name === 'id'
|
||||
? { ...field, label: 'Modified Id Label' }
|
||||
: field,
|
||||
),
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
});
|
||||
|
||||
const { errors } = await syncApplication({
|
||||
manifest: manifestWithUpdatedIdField,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({ errors });
|
||||
}, 60000);
|
||||
});
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
|
||||
import { findManyObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata.util';
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
type ObjectManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const SYSTEM_FIELD_NAMES = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
'searchVector',
|
||||
];
|
||||
|
||||
const TEST_APP_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const TEST_ROLE_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
|
||||
const OBJECT_NAME_SINGULAR = 'rocketForManifestUniversalIdentifier';
|
||||
|
||||
const TEST_OBJECT: ObjectManifest = {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: `${OBJECT_NAME_SINGULAR}s`,
|
||||
labelSingular: 'Rocket For Manifest Universal Identifier',
|
||||
labelPlural: 'Rockets For Manifest Universal Identifier',
|
||||
description: 'A rocket synced through the manifest funnel',
|
||||
icon: 'IconRocket',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
type FetchedField = {
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
|
||||
describe('Application manifest sync deterministic system field universal identifiers', () => {
|
||||
let objectUniversalIdentifier: string;
|
||||
let fetchedFields: FetchedField[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Test Application',
|
||||
description: 'A test application',
|
||||
sourcePath: 'test-sync-deterministic',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
expectToFail: false,
|
||||
manifest: buildBaseManifest({
|
||||
appId: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
roleId: TEST_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
overrides: { objects: [TEST_OBJECT] },
|
||||
}),
|
||||
});
|
||||
|
||||
const { objects } = await findManyObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { filter: {}, paging: { first: 100 } },
|
||||
gqlFields: 'id nameSingular universalIdentifier',
|
||||
});
|
||||
|
||||
const syncedObject = objects.find(
|
||||
(object) => object.universalIdentifier === OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
if (!isDefined(syncedObject)) {
|
||||
throw new Error(
|
||||
'Could not resolve the object synced through the manifest funnel',
|
||||
);
|
||||
}
|
||||
|
||||
objectUniversalIdentifier = syncedObject.universalIdentifier;
|
||||
|
||||
const { fields } = await findManyFieldsMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: { objectMetadataId: { eq: syncedObject.id } },
|
||||
paging: { first: 100 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
`,
|
||||
});
|
||||
|
||||
fetchedFields = fields.map((edge: { node: FetchedField }) => edge.node);
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(SYSTEM_FIELD_NAMES)(
|
||||
'should derive the %s system field universal identifier deterministically through the manifest sync funnel',
|
||||
(systemFieldName) => {
|
||||
const systemField = fetchedFields.find(
|
||||
(field) => field.name === systemFieldName,
|
||||
);
|
||||
|
||||
expect(systemField).toBeDefined();
|
||||
expect(systemField?.universalIdentifier).toBe(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier,
|
||||
name: systemFieldName,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util';
|
||||
import { search } from 'test/integration/graphql/utils/search.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
type ObjectManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const TEST_ROLE_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
|
||||
const OBJECT_NAME_SINGULAR = 'relabelableRocket';
|
||||
const OBJECT_NAME_PLURAL = 'relabelableRockets';
|
||||
|
||||
const RECORD_NAME_VALUE = 'AlphaManifestRelabelNameTerm';
|
||||
const RECORD_TOTO_VALUE = 'BravoManifestRelabelTotoTerm';
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const TOTO_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'toto',
|
||||
});
|
||||
|
||||
const NAME_FIELD_MANIFEST = {
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
} as const;
|
||||
|
||||
const TOTO_FIELD_MANIFEST = {
|
||||
universalIdentifier: TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'toto',
|
||||
label: 'Toto',
|
||||
} as const;
|
||||
|
||||
const buildObjectManifest = ({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
fields,
|
||||
}: {
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: string;
|
||||
fields: ObjectManifest['fields'];
|
||||
}): ObjectManifest => ({
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: OBJECT_NAME_PLURAL,
|
||||
labelSingular: 'Relabelable Rocket',
|
||||
labelPlural: 'Relabelable Rockets',
|
||||
description: 'A rocket whose label identifier is relabeled across syncs',
|
||||
icon: 'IconRocket',
|
||||
isSearchable: true,
|
||||
fields,
|
||||
});
|
||||
|
||||
const syncObjectManifest = async (objectManifest: ObjectManifest) => {
|
||||
await syncApplication({
|
||||
expectToFail: false,
|
||||
manifest: buildBaseManifest({
|
||||
appId: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
roleId: TEST_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
overrides: { objects: [objectManifest] },
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const searchRecordIds = async (searchInput: string): Promise<string[]> => {
|
||||
const searchResult = await search({
|
||||
searchInput,
|
||||
includedObjectNameSingulars: [OBJECT_NAME_SINGULAR],
|
||||
limit: 10,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
return searchResult.data.search.edges.map((edge) => edge.node.recordId);
|
||||
};
|
||||
|
||||
describe('Application manifest sync - search field metadata on label identifier relabel', () => {
|
||||
let recordId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Test Application',
|
||||
description: 'A test application',
|
||||
sourcePath: 'test-sync-search-relabel',
|
||||
});
|
||||
|
||||
// First sync: object with `name` and `toto` text fields, `name` as label
|
||||
// identifier. `toto` exists from the start but is not the label identifier,
|
||||
// so only `name` is indexed for search initially.
|
||||
// NOTE: `toto` is created here rather than in the relabel sync because a
|
||||
// single sync cannot both create a field and relabel the object onto it
|
||||
// yet (objectMetadata.update is ordered before fieldMetadata.create in the
|
||||
// migration runner). Tracked for a follow-up core fix:
|
||||
// https://github.com/twentyhq/core-team-issues/issues/2655
|
||||
await syncObjectManifest(
|
||||
buildObjectManifest({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [NAME_FIELD_MANIFEST, TOTO_FIELD_MANIFEST],
|
||||
}),
|
||||
);
|
||||
|
||||
const { data } = await createManyOperation({
|
||||
objectMetadataSingularName: OBJECT_NAME_SINGULAR,
|
||||
objectMetadataPluralName: OBJECT_NAME_PLURAL,
|
||||
gqlFields: 'id name toto',
|
||||
data: [{ name: RECORD_NAME_VALUE, toto: RECORD_TOTO_VALUE }],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
recordId = data.createdRecords[0].id;
|
||||
}, 120000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
});
|
||||
|
||||
it('should make records searchable through the name label identifier only', async () => {
|
||||
expect(await searchRecordIds(RECORD_NAME_VALUE)).toEqual([recordId]);
|
||||
// `toto` is not the label identifier yet, so it is not part of the search surface.
|
||||
expect(await searchRecordIds(RECORD_TOTO_VALUE)).toEqual([]);
|
||||
}, 120000);
|
||||
|
||||
it('should make records searchable through both name and toto after relabeling onto toto', async () => {
|
||||
// Second sync: relabel the object onto the pre-existing `toto` field.
|
||||
await syncObjectManifest(
|
||||
buildObjectManifest({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
TOTO_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [NAME_FIELD_MANIFEST, TOTO_FIELD_MANIFEST],
|
||||
}),
|
||||
);
|
||||
|
||||
// Relabeling is additive: the previous `name` surface is preserved.
|
||||
expect(await searchRecordIds(RECORD_NAME_VALUE)).toEqual([recordId]);
|
||||
expect(await searchRecordIds(RECORD_TOTO_VALUE)).toEqual([recordId]);
|
||||
}, 120000);
|
||||
|
||||
it('should only remain searchable through name after toto is removed', async () => {
|
||||
// Third sync: relabel back onto `name` and remove the `toto` field. Removing
|
||||
// the field must drop its searchFieldMetadata row so it leaves the search surface.
|
||||
await syncObjectManifest(
|
||||
buildObjectManifest({
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [NAME_FIELD_MANIFEST],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(await searchRecordIds(RECORD_NAME_VALUE)).toEqual([recordId]);
|
||||
expect(await searchRecordIds(RECORD_TOTO_VALUE)).toEqual([]);
|
||||
}, 120000);
|
||||
});
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util';
|
||||
import { search } from 'test/integration/graphql/utils/search.util';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
type ObjectManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const TEST_APP_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const TEST_ROLE_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
const OBJECT_UNIVERSAL_IDENTIFIER = uuidv4();
|
||||
|
||||
const OBJECT_NAME_SINGULAR = 'searchableRocket';
|
||||
const OBJECT_NAME_PLURAL = 'searchableRockets';
|
||||
|
||||
const RECORD_NAME_VALUE = 'FalconHeavyManifestSearchTerm';
|
||||
const OTHER_RECORD_NAME_VALUE = 'AtlasFiveManifestSearchTerm';
|
||||
|
||||
const NAME_FIELD_UNIVERSAL_IDENTIFIER = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
const TEST_OBJECT: ObjectManifest = {
|
||||
universalIdentifier: OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: OBJECT_NAME_PLURAL,
|
||||
labelSingular: 'Searchable Rocket',
|
||||
labelPlural: 'Searchable Rockets',
|
||||
description: 'A rocket synced through the manifest funnel',
|
||||
icon: 'IconRocket',
|
||||
isSearchable: true,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Application manifest sync search field metadata population', () => {
|
||||
let matchingRecordId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Test Application',
|
||||
description: 'A test application',
|
||||
sourcePath: 'test-sync-search',
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
expectToFail: false,
|
||||
manifest: buildBaseManifest({
|
||||
appId: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
roleId: TEST_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
overrides: { objects: [TEST_OBJECT] },
|
||||
}),
|
||||
});
|
||||
|
||||
const { data } = await createManyOperation({
|
||||
objectMetadataSingularName: OBJECT_NAME_SINGULAR,
|
||||
objectMetadataPluralName: OBJECT_NAME_PLURAL,
|
||||
gqlFields: 'id name',
|
||||
data: [
|
||||
{ name: RECORD_NAME_VALUE },
|
||||
{ name: OTHER_RECORD_NAME_VALUE },
|
||||
],
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const matchingRecord = data.createdRecords.find(
|
||||
(record) => record.name === RECORD_NAME_VALUE,
|
||||
);
|
||||
|
||||
if (matchingRecord === undefined) {
|
||||
throw new Error('Could not create the record to search for');
|
||||
}
|
||||
|
||||
matchingRecordId = matchingRecord.id;
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: TEST_APP_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
});
|
||||
|
||||
it('should make the synced object searchable through its server-synthesized search field metadata', async () => {
|
||||
const searchResult = await search({
|
||||
searchInput: RECORD_NAME_VALUE,
|
||||
includedObjectNameSingulars: [OBJECT_NAME_SINGULAR],
|
||||
limit: 10,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
expect(searchResult.data.search.edges).toHaveLength(1);
|
||||
|
||||
const searchResultNode = searchResult.data.search.edges[0].node;
|
||||
|
||||
expect(searchResultNode.recordId).toBe(matchingRecordId);
|
||||
expect(searchResultNode.objectNameSingular).toBe(OBJECT_NAME_SINGULAR);
|
||||
expect(searchResultNode.label).toBe(RECORD_NAME_VALUE);
|
||||
}, 60000);
|
||||
|
||||
it('should not return records whose label identifier does not match the search term', async () => {
|
||||
const searchResult = await search({
|
||||
searchInput: RECORD_NAME_VALUE,
|
||||
includedObjectNameSingulars: [OBJECT_NAME_SINGULAR],
|
||||
limit: 10,
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const returnedRecordIds = searchResult.data.search.edges.map(
|
||||
(edge) => edge.node.recordId,
|
||||
);
|
||||
|
||||
expect(returnedRecordIds).not.toContain(OTHER_RECORD_NAME_VALUE);
|
||||
expect(returnedRecordIds).toEqual([matchingRecordId]);
|
||||
}, 60000);
|
||||
});
|
||||
+315
-315
@@ -297,27 +297,6 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -382,6 +361,27 @@ exports[`Object metadata creation should fail v2 when labelPlural contains only
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -978,27 +978,6 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -1063,6 +1042,27 @@ exports[`Object metadata creation should fail v2 when labelPlural exceeds maximu
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -1670,27 +1670,6 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -1755,6 +1734,27 @@ exports[`Object metadata creation should fail v2 when labelSingular contains onl
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -2351,27 +2351,6 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -2436,6 +2415,27 @@ exports[`Object metadata creation should fail v2 when labelSingular exceeds maxi
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -3067,27 +3067,6 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_da4ca4dc0c441477f29e4a384b0",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -3152,6 +3131,27 @@ exports[`Object metadata creation should fail v2 when name exceeds maximum lengt
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_da4ca4dc0c441477f29e4a384b0",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -3748,27 +3748,6 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -3833,6 +3812,27 @@ exports[`Object metadata creation should fail v2 when namePlural has invalid cha
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -4429,27 +4429,6 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -4514,6 +4493,27 @@ exports[`Object metadata creation should fail v2 when namePlural is a reserved k
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -5121,27 +5121,6 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -5206,6 +5185,27 @@ exports[`Object metadata creation should fail v2 when namePlural is not camelCas
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_ffebb98ec981552dbecc71223c8",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -5826,27 +5826,6 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_2e70d840ef39341865172574229",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -5911,6 +5890,27 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_2e70d840ef39341865172574229",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -6507,27 +6507,6 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_e0b8367f7f89bf39915991232b6",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -6592,6 +6571,27 @@ exports[`Object metadata creation should fail v2 when nameSingular contains only
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_e0b8367f7f89bf39915991232b6",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -7218,27 +7218,6 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_e895f7d7a9fc30bad070509afab",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -7303,6 +7282,27 @@ exports[`Object metadata creation should fail v2 when nameSingular has invalid c
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_e895f7d7a9fc30bad070509afab",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -7899,27 +7899,6 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_00aaf7dba3ae2d70218bdebd383",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -7984,6 +7963,27 @@ exports[`Object metadata creation should fail v2 when nameSingular is a reserved
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_00aaf7dba3ae2d70218bdebd383",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -8615,27 +8615,6 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_17a51d4440e492a8ec4d594c445",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -8700,6 +8679,27 @@ exports[`Object metadata creation should fail v2 when nameSingular is not camelC
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_17a51d4440e492a8ec4d594c445",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -9296,27 +9296,6 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_acbec9630e5330ad7c000993538",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -9381,6 +9360,27 @@ exports[`Object metadata creation should fail v2 when names are identical 1`] =
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_acbec9630e5330ad7c000993538",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
@@ -9977,27 +9977,6 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
|
||||
},
|
||||
],
|
||||
"index": [
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_acbec9630e5330ad7c000993538",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
@@ -10062,6 +10041,27 @@ exports[`Object metadata creation should fail v2 when names with whitespaces res
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"code": "INDEX_OBJECT_NOT_FOUND",
|
||||
"message": "Could not find index related object metadata",
|
||||
"userFriendlyMessage": "Index related object not found",
|
||||
},
|
||||
{
|
||||
"code": "INDEX_FIELD_NOT_FOUND",
|
||||
"message": "Could not find index field related field metadata",
|
||||
"userFriendlyMessage": "Field referenced in index does not exist",
|
||||
},
|
||||
],
|
||||
"flatEntityMinimalInformation": {
|
||||
"name": "IDX_acbec9630e5330ad7c000993538",
|
||||
"universalIdentifier": Any<String>,
|
||||
},
|
||||
"metadataName": "index",
|
||||
"status": "fail",
|
||||
"type": "create",
|
||||
},
|
||||
],
|
||||
"objectMetadata": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user