Deterministic system field universal identifier (#22565)
# Introduction Close twentyhq/core-team-issues#2641 Auto-provisioned field metadata used to get its `universalIdentifier` from three unrelated sources: random `v4()` on the server when creating custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc `v5` derivation in the SDK manifest build. This PR unifies all of them behind the shared `getFieldUniversalIdentifier` derivation: ``` universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName) ``` ## Ownership model The rollout is built on an explicit split of who owns a field's universal identifier: - **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are **server-owned**. Their universal identifiers are always the deterministic derivation, on **every** application (standard, workspace-custom, installed). Clients cannot provide custom values: a temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects any non-derived system field identifier at migration build time. This check stands in until system fields are generated exclusively server side by the metadata side-effect engine and stripped from client inputs — at which point it becomes structurally impossible to send one. - **`name` is a default field, not a system field**: it is auto-provisioned when absent (server side for custom objects, SDK side for application objects) but authors can define their own. It is only derived where it is guaranteed to be auto-provisioned. In particular, standard objects keep their **historical hardcoded** `name` identifiers: the standard app authors its `name` fields like any installed app would, and moving those identifiers would break every installed application referencing them (e.g. views on `opportunity.name`). - **User-created and author-provided fields** keep random / explicit identifiers, untouched. ## Server - `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of the existing type/`isSystem` checks, that each system field's `universalIdentifier` equals the deterministic derivation. Runs for every object creation going through the migration orchestrator: app sync, custom object creation, standard provisioning - `build-default-flat-field-metadatas-for-custom-object.util.ts` derives the system field identifiers (and the auto-provisioned `name`) with `getFieldUniversalIdentifier` instead of `v4()` - `build-default-relation-flat-field-metadatas-for-custom-object.util.ts` derives both the forward and the reverse default relation field identifiers deterministically - `generateMorphOrRelationFlatFieldMetadataPair` accepts optional `sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so callers can inject deterministic values; user-created relations still default to `v4()` ## twenty-shared - `STANDARD_OBJECTS` system field identifiers (the 8) are now computed at module load via `buildStandardObjectSystemFields`; `name` and every other identifier keep their hardcoded values - New snapshot test pinning **every** universal identifier of `STANDARD_OBJECTS`: any identifier change now requires an explicit snapshot update and should ship with a coordinated backfill ## SDK (breaking, pre-GA) - `generateDefaultFieldUniversalIdentifier` delegates to `getFieldUniversalIdentifier` and now requires `applicationUniversalIdentifier` - Reverse default relation field identifiers are derived from the field's real coordinates (standard object UID + actual field name, e.g. `targetRocket` on `attachment`) instead of the legacy custom-object UID + synthetic `${fieldName}Inverse` hash input. Field *names* are unchanged - The manifest build threads the application universal identifier through default field injection (two-pass over object configs) - `twenty dev:add` now resolves the application universal identifier upfront and refuses to scaffold anything until `defineApplication` declares one — no more `fill-later` placeholder for the app UID in generated files ## Upgrade A 2.19 **workspace command** backfills existing `fieldMetadata.universalIdentifier` rows to the deterministic derivation. Coverage follows the ownership model: - **The 8 system fields**: taken over for **every application**, whatever value they currently hold. This is both safe and required now that sync rejects non-derived values — leaving a row unconverged would make its application unsyncable - **`name`**: workspace-custom app → always taken over (server-generated, no author to clobber); installed applications → only rows still carrying the legacy SDK derivation are recomputed, author-provided identifiers are never touched; standard app → never touched (hardcoded in `STANDARD_OBJECTS`) - **Default relation fields**: workspace-custom app → forward fields on custom objects and reverse fields on the standard relation objects; installed applications → legacy-derivation probe only All identifiers of a workspace are updated inside a single transaction, then the command flushes the field-metadata-related workspace caches and bumps the metadata version. Stored `applicationRegistration.manifest` snapshots are intentionally **not** rewritten: installs and upgrades always sync from the `manifest.json` inside the resolved package (npm/tarball), the stored column is only used for display/marketplace purposes. ## Breaking behavior for old packages (fail closed) Packages built with an older SDK carry legacy system field identifiers in their tarball `manifest.json`. Installing or upgrading such a package now fails with an explicit `INVALID_SYSTEM_FIELD` validation error ("universal identifier is not deterministic") instead of silently mismatching against the backfilled rows and triggering a destructive delete+create. The remediation is to rebuild the package with the new SDK; the backfill has already converged the installed rows, so the rebuilt manifest syncs cleanly. ## Test plan - [x] `twenty-sdk` unit tests (526 tests) and typecheck - [x] `twenty-shared` unit tests (1635 tests) including the `STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte identical to `main` - [x] Lint and typecheck clean on all touched packages - [x] Integration: create a custom object and verify system + default relation field identifiers match the deterministic derivation (`create-one-object-metadata-deterministic-field-universal-identifiers`, 13 assertions passing) - [x] Integration: `failing-sync-application-object-system-fields` extended with a non-derived system field identifier case; all identifiers in the spec pinned deterministically so snapshots embedding expected/actual values are stable across runs (verified with a double run) - [x] Integration: all application sync suites pass with the derived system field identifiers now required by the `buildDefaultObjectManifest` test helper (9 suites, 20 tests) - [x] Full test-database reset: standard app provisioning and seeded workspaces pass the new validation - [x] SDK manifest build verified on the postcard example app: all auto-generated default field identifiers match the derivation - [ ] Run `upgrade:2-19:backfill-deterministic-field-universal-identifiers` (dry-run then real) on a seeded workspace and verify identifier convergence with a rebuilt app manifest
This commit is contained in:
@@ -11,7 +11,9 @@ import { PARTNER_ON_OPPORTUNITY_FIELD_ID } from 'src/fields/partner-on-opportuni
|
||||
export const MY_DEALS_VIEW_UNIVERSAL_IDENTIFIER =
|
||||
'09c9b985-6dce-43ec-a6b3-5e878e1aec74';
|
||||
|
||||
const OPPORTUNITY_NAME_FIELD_ID = '20202020-8609-4f65-a2d9-44009eb422b5';
|
||||
const OPPORTUNITY_NAME_FIELD_ID =
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.fields.name
|
||||
.universalIdentifier;
|
||||
const OPPORTUNITY_AMOUNT_FIELD_ID = '20202020-583e-4642-8533-db761d5fa82f';
|
||||
|
||||
// Partner-facing list of opportunities assigned to the partner (partner relation set).
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineView({
|
||||
position: 5,
|
||||
fields: [
|
||||
// Name
|
||||
{ universalIdentifier: '74595afa-c9c4-45b6-b362-803fb5574fbd', fieldMetadataUniversalIdentifier: '20202020-8609-4f65-a2d9-44009eb422b5', position: 0, isVisible: true, size: 200 },
|
||||
{ universalIdentifier: '74595afa-c9c4-45b6-b362-803fb5574fbd', fieldMetadataUniversalIdentifier: STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.fields.name.universalIdentifier, position: 0, isVisible: true, size: 200 },
|
||||
// Need (B1 brief field)
|
||||
{ universalIdentifier: 'a24d52c4-9d0b-4145-a8fb-a4e21e002f58', fieldMetadataUniversalIdentifier: OPPORTUNITY_NEED_FIELD_ID, position: 1, isVisible: true, size: 280 },
|
||||
// Requirements (B1 brief field)
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/je
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggr
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
|
||||
const PERSON_ID = 'a7286b9a-c039-4a89-9567-2dfa7953cda9';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'a7286b9a-c039-4a89-9567-2dfa7953cda9'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
|
||||
const COMPANY_ID = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ import {
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'mock-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -16,6 +16,7 @@ import {
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ import {
|
||||
} from './PageLayoutTestWrapper';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/je
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/je
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ import { getJestMetadataAndApolloMocksAndCommandMenuWrapper } from '~/testing/je
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn().mockReturnValue('mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@ import {
|
||||
} from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
|
||||
const mockedUuid = 'mocked-uuid';
|
||||
jest.mock('uuid');
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
(uuidv4 as jest.Mock).mockReturnValue(mockedUuid);
|
||||
|
||||
|
||||
+4
-1
@@ -7,7 +7,10 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { StepStatus, type WorkflowRunStepInfos } from 'twenty-shared/workflow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
jest.mock('uuid');
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
let counter = 0;
|
||||
|
||||
+4
-1
@@ -1,7 +1,10 @@
|
||||
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
jest.mock('uuid');
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
let counter = 0;
|
||||
|
||||
+4
-1
@@ -32,7 +32,10 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('uuid', () => ({ v4: () => 'step-id' }));
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: () => 'step-id',
|
||||
}));
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const workflowVisualizerComponentInstanceId =
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { v4 } from 'uuid';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'test-uuid-123'),
|
||||
}));
|
||||
|
||||
|
||||
+25
-25
@@ -33,10 +33,10 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'targetMyNoteId',
|
||||
},
|
||||
universalIdentifier: '7e28b1ea-9a94-5d36-bc03-3f12f476867a',
|
||||
universalIdentifier: '55153e55-1bdc-590d-8fb1-40eafec88ef0',
|
||||
objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35',
|
||||
'559713af-d9c0-5894-a972-9a3951661a2b',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000030',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
@@ -53,10 +53,10 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'targetMyNoteId',
|
||||
},
|
||||
universalIdentifier: '721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5',
|
||||
universalIdentifier: '80596d88-f896-5108-8cff-302a2eb61668',
|
||||
objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'56a872c8-7a1e-563d-8772-3978c2c5c220',
|
||||
'8f8e42e3-77de-5b0a-b444-d96b6d3c7600',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000030',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
@@ -73,10 +73,10 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'targetMyNoteId',
|
||||
},
|
||||
universalIdentifier: '94b6f269-fc9a-5bcd-af5b-2bdac69f276b',
|
||||
universalIdentifier: '67bf90bd-f3d0-58ce-a96e-648364249812',
|
||||
objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'011595e8-7524-5ec7-9fbc-4bd3c42d7d24',
|
||||
'0cbc9a5c-34b2-5d39-97a2-878662cb13d5',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000030',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
@@ -93,10 +93,10 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
joinColumnName: 'targetMyNoteId',
|
||||
},
|
||||
universalIdentifier: '50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3',
|
||||
universalIdentifier: '2689365b-e6ce-5b14-a2b5-6daafbfe91f8',
|
||||
objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'dd734fa3-26c3-53dd-91aa-577c313e1fa4',
|
||||
'affdd37c-4460-53fc-998b-ffb04bb3a470',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1e2e3e4-e5e6-4000-8000-000000000030',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
@@ -129,7 +129,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '5bdb4f57-a349-56d0-b421-845fd55b3026',
|
||||
universalIdentifier: 'a717a3ba-e257-5269-95c4-1b8ca4cb8bcf',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -139,7 +139,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: '86cc7b06-26de-5b7b-bf4a-31fa246dfd90',
|
||||
universalIdentifier: '9a7f457a-6c15-581e-8f28-e2c680f75f97',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -149,7 +149,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'abe3bc69-1a51-5814-a722-74f5a025e92a',
|
||||
universalIdentifier: 'f24a236a-e36a-5bab-91db-d43e3f4b8247',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -159,7 +159,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'c296d408-9720-5a86-8586-3490fbf04b86',
|
||||
universalIdentifier: '875c1f2e-19f0-5200-96ba-ac1cb4c1fd33',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -169,7 +169,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '488333a2-f4ff-5790-88ef-21f07f8f1985',
|
||||
universalIdentifier: 'cc58b328-40e1-5087-b550-ad700e53490b',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -182,7 +182,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'fba6e01b-6579-554e-ad64-a676003c8e68',
|
||||
universalIdentifier: '6f34f940-966a-52c2-a8f2-f2f04115d88e',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -195,7 +195,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'fa60f87d-15c8-5ff7-9da0-191fa14a922f',
|
||||
universalIdentifier: '37515dfb-8ae0-5326-a391-9e66205f6530',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
@@ -205,7 +205,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '4a5d7791-ddc9-53ee-bc46-699da7373290',
|
||||
universalIdentifier: 'a418b503-74d0-500d-b4c0-75f364394e20',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -215,7 +215,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: '0f00bc3b-f313-5206-8496-842af20a538a',
|
||||
universalIdentifier: '45718beb-e0d7-5bde-811c-29ff01b0fcfd',
|
||||
},
|
||||
{
|
||||
name: 'timelineActivities',
|
||||
@@ -227,9 +227,9 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
universalIdentifier: '12c84c1c-6e3f-54c5-a9a8-e93c7b46ac35',
|
||||
universalIdentifier: '559713af-d9c0-5894-a972-9a3951661a2b',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'7e28b1ea-9a94-5d36-bc03-3f12f476867a',
|
||||
'55153e55-1bdc-590d-8fb1-40eafec88ef0',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
},
|
||||
@@ -243,9 +243,9 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
universalIdentifier: '56a872c8-7a1e-563d-8772-3978c2c5c220',
|
||||
universalIdentifier: '8f8e42e3-77de-5b0a-b444-d96b6d3c7600',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'721b12d8-7a3d-5e13-b8d5-62b3ad8cb8b5',
|
||||
'80596d88-f896-5108-8cff-302a2eb61668',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
},
|
||||
@@ -259,9 +259,9 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
universalIdentifier: '011595e8-7524-5ec7-9fbc-4bd3c42d7d24',
|
||||
universalIdentifier: '0cbc9a5c-34b2-5d39-97a2-878662cb13d5',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'94b6f269-fc9a-5bcd-af5b-2bdac69f276b',
|
||||
'67bf90bd-f3d0-58ce-a96e-648364249812',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-fff0-4b44-be82-bda313884400',
|
||||
},
|
||||
@@ -275,9 +275,9 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
universalIdentifier: 'dd734fa3-26c3-53dd-91aa-577c313e1fa4',
|
||||
universalIdentifier: 'affdd37c-4460-53fc-998b-ffb04bb3a470',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'50da3cfa-dea5-5c9b-b1b7-cb615e1df4c3',
|
||||
'2689365b-e6ce-5b14-a2b5-6daafbfe91f8',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
},
|
||||
|
||||
+102
-102
@@ -292,11 +292,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRootNote',
|
||||
objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'1cdb0b3a-0ae1-59f6-b3f0-59ff99e40525',
|
||||
'e1478d59-c947-5926-af8b-4b2f0f842c91',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'b0b1b2b3-b4b5-4000-8000-000000000001',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: 'a0f6ffdc-a570-58a6-973d-a9bc9a527de8',
|
||||
universalIdentifier: '7f891268-9376-5d00-b8ec-bc32a023154d',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRootNoteId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -312,11 +312,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRootNote',
|
||||
objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'aa05ee89-bd8a-5911-a4d5-3355926186bc',
|
||||
'34a6b8e9-0187-57e1-86c1-41bacf1b2479',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'b0b1b2b3-b4b5-4000-8000-000000000001',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '95cad10a-bff6-524d-b6d8-dbc6db7cbf18',
|
||||
universalIdentifier: 'd72bb002-9952-5651-983e-649436949113',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRootNoteId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -332,11 +332,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRootNote',
|
||||
objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'762ad37e-d624-51d8-989c-3a119eef9e7f',
|
||||
'7a62bbd6-e697-54b0-8c1c-163ac47c0305',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'b0b1b2b3-b4b5-4000-8000-000000000001',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: 'c327faff-76eb-5baf-8b35-1c31e57306fb',
|
||||
universalIdentifier: '61252ad6-2311-5054-ae51-b318dfd5fbd8',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRootNoteId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -352,11 +352,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRootNote',
|
||||
objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'd4f1b0c8-8cd9-581e-9bf9-917f8c918d76',
|
||||
'10c1af7e-e541-56bc-b5b3-d0297df3e23d',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'b0b1b2b3-b4b5-4000-8000-000000000001',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '36843050-e6e3-5759-99b6-e975426bad73',
|
||||
universalIdentifier: '2f4f3d69-b78e-5465-9e87-17059959fca8',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRootNoteId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -480,11 +480,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCardRecipient',
|
||||
objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'02f5efba-4f94-5cc3-9e17-ca7b1fe5f71b',
|
||||
'8f95b434-ee92-5c0f-95de-9d489af4d40c',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '18b6354d-8b0f-5be6-9473-d635aed670a1',
|
||||
universalIdentifier: 'a865f403-3843-5850-9a60-04e832220954',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -500,11 +500,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCardRecipient',
|
||||
objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'82497ec7-0121-53d6-9518-ae7434ceaf8b',
|
||||
'0c20df83-8cef-5cd6-9752-b98fb7780904',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: 'ba74dfd1-e7ee-502d-abad-82e14d595874',
|
||||
universalIdentifier: '501c46b7-7140-59a0-b703-9844bbcb34b9',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -520,11 +520,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCardRecipient',
|
||||
objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'6de8b57e-ed0a-5d35-bd39-bd8874445810',
|
||||
'b9642a90-2df5-555b-ab83-23cdce7a4142',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '9d5a9f4b-5431-5b90-810a-1e9d8873b302',
|
||||
universalIdentifier: '721418ff-1b53-58ef-8dd7-b58927c085c6',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -540,11 +540,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCardRecipient',
|
||||
objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'00459e7e-8ef1-5a9b-b180-26bbb54a2696',
|
||||
'd938a2d0-1279-5c48-b758-ac52f9460a76',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '85353e69-1e70-51ef-91c3-334d85013a4b',
|
||||
universalIdentifier: 'feeba797-7ebb-5a36-91cd-7279ff92a2b6',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -561,11 +561,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCard',
|
||||
objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'3f8c70b5-abb0-51ec-9109-0a94ce9c04a6',
|
||||
'9e58820c-78d7-539d-807c-d84362e1eab6',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'54b589ca-eeed-4950-a176-358418b85c05',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '1bdc52a0-0339-5360-ba17-b615250ef730',
|
||||
universalIdentifier: '47d21bb5-075f-5ed4-ba89-c0e7322a57cd',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -581,11 +581,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCard',
|
||||
objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'547c93e4-c9e7-5880-b251-46230fb59bfa',
|
||||
'3f0d38c8-a50f-51c7-bdcb-84a715f4b92d',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'54b589ca-eeed-4950-a176-358418b85c05',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: 'fecb8484-7e3c-59f8-a5ef-30882c5c1904',
|
||||
universalIdentifier: '4d3cf1bd-d87d-5325-8fbc-19f72c252050',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -601,11 +601,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCard',
|
||||
objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'47deea5c-8559-5ae7-991a-60c9f7102f86',
|
||||
'def1f10a-847b-574e-a0a2-177caf02129c',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'54b589ca-eeed-4950-a176-358418b85c05',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: 'ca687361-ac2b-5722-9ad9-cd37427d087a',
|
||||
universalIdentifier: 'bc1f9abe-e27b-574a-9290-a1c9c8255a6b',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -621,11 +621,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetPostCard',
|
||||
objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'ecdccc0d-7c31-5126-89d5-0bdf3372257c',
|
||||
'b1238c2d-bc9c-53d6-8bee-eb045b9b9086',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'54b589ca-eeed-4950-a176-358418b85c05',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '17058c96-d5a4-53b1-96ca-786f8c5fc8bd',
|
||||
universalIdentifier: 'fb610de1-db20-5e78-8e4a-7630e57148eb',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetPostCardId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -642,11 +642,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRecipient',
|
||||
objectUniversalIdentifier: '20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'51d6d2d7-2fa8-5f77-b6cf-a96dbb801fa5',
|
||||
'1118dd8a-9781-5254-b625-2d0f05eb46fe',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: 'b01f6937-4859-587b-b3df-92e0a6f65e93',
|
||||
universalIdentifier: '1ce06ca1-6e9a-53ce-9888-9152b1b2b628',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -662,11 +662,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRecipient',
|
||||
objectUniversalIdentifier: '20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'244a7841-9e45-5ed1-a830-83e73c1f13fa',
|
||||
'15714c1a-d354-5407-8a15-25c117d605f8',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '9c87e1b8-74a2-5833-9237-281d561d2a7d',
|
||||
universalIdentifier: '2081700d-7ce6-5698-a7da-a013feda0eb6',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -682,11 +682,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRecipient',
|
||||
objectUniversalIdentifier: '20202020-fff0-4b44-be82-bda313884400',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'eab71eb1-b457-555a-8a2b-f292cef1671d',
|
||||
'167f6de7-835e-51e1-b751-34055eef6aa5',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '23508618-7ed8-519e-9fee-a57b94dc98e9',
|
||||
universalIdentifier: 'a93c479c-6206-5714-94ce-affa6e9886aa',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -702,11 +702,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'targetRecipient',
|
||||
objectUniversalIdentifier: '20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'305f1890-04ba-5101-b18f-a1ca25a1d8a7',
|
||||
'd0db1844-4fde-5a5a-b037-ebeabd4467af',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
type: FieldType.MORPH_RELATION,
|
||||
universalIdentifier: '20500ff3-2410-5bfc-b2e7-804785c07770',
|
||||
universalIdentifier: '45752445-c4b6-5e80-8aa1-f5321a838b43',
|
||||
universalSettings: {
|
||||
joinColumnName: 'targetRecipientId',
|
||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
||||
@@ -749,7 +749,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: 'b5830f92-1abe-5ae7-b689-7821dda00b15',
|
||||
universalIdentifier: 'a4429f96-5745-511a-b246-67da9f920452',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -759,7 +759,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'ae2b2e5a-6c8b-5324-b639-8f17e3bc0420',
|
||||
universalIdentifier: '10452ca3-30cb-56fa-9e58-73f7b1c9fd65',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -769,7 +769,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '7c84a501-41a6-531e-bc5e-8062843f6cba',
|
||||
universalIdentifier: '777fa853-2198-5623-96f3-119ef5707ad3',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -779,7 +779,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '72c2903a-91c9-5f36-8668-b53b083ae4cf',
|
||||
universalIdentifier: 'e1715fe9-34e1-572c-b62c-f39d737a17df',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -789,7 +789,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '007f5d8e-d376-519e-bd07-aea41a53f7d9',
|
||||
universalIdentifier: 'e3b13370-7241-55f1-ab9a-346b57bf4dcf',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -802,7 +802,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '44053354-0d8b-5d66-bfa8-7e7d221de3db',
|
||||
universalIdentifier: 'd2e3e238-9bfd-5845-9fe8-3e58df09f246',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -815,7 +815,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '38e2c671-cbf5-5773-a04a-e5a2d6c323c8',
|
||||
universalIdentifier: '690003ae-f649-537e-b124-d21ef0ee86a1',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
@@ -825,7 +825,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '3b56cdea-cf85-51d4-a7a0-cd701f76141a',
|
||||
universalIdentifier: '4b6d383d-4e84-592e-b1ae-30ae27139302',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -835,7 +835,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: '7c6d88f5-a348-51dd-8d0b-74a17e626754',
|
||||
universalIdentifier: 'a92d59aa-23a1-5191-b840-933183c14846',
|
||||
},
|
||||
{
|
||||
description: 'Root notes tied to the RootNote',
|
||||
@@ -844,11 +844,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Timeline Activities',
|
||||
name: 'timelineActivities',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'a0f6ffdc-a570-58a6-973d-a9bc9a527de8',
|
||||
'7f891268-9376-5d00-b8ec-bc32a023154d',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '1cdb0b3a-0ae1-59f6-b3f0-59ff99e40525',
|
||||
universalIdentifier: 'e1478d59-c947-5926-af8b-4b2f0f842c91',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -860,11 +860,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Attachments',
|
||||
name: 'attachments',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'95cad10a-bff6-524d-b6d8-dbc6db7cbf18',
|
||||
'd72bb002-9952-5651-983e-649436949113',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: 'aa05ee89-bd8a-5911-a4d5-3355926186bc',
|
||||
universalIdentifier: '34a6b8e9-0187-57e1-86c1-41bacf1b2479',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -876,11 +876,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Note Targets',
|
||||
name: 'noteTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'c327faff-76eb-5baf-8b35-1c31e57306fb',
|
||||
'61252ad6-2311-5054-ae51-b318dfd5fbd8',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-fff0-4b44-be82-bda313884400',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '762ad37e-d624-51d8-989c-3a119eef9e7f',
|
||||
universalIdentifier: '7a62bbd6-e697-54b0-8c1c-163ac47c0305',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -892,11 +892,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Task Targets',
|
||||
name: 'taskTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'36843050-e6e3-5759-99b6-e975426bad73',
|
||||
'2f4f3d69-b78e-5465-9e87-17059959fca8',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: 'd4f1b0c8-8cd9-581e-9bf9-917f8c918d76',
|
||||
universalIdentifier: '10c1af7e-e541-56bc-b5b3-d0297df3e23d',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -931,7 +931,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '2809d745-deb3-5ac0-8328-ea6256ec22b3',
|
||||
universalIdentifier: 'fe2cec74-a43b-5910-8dd6-7a737fb869ae',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -941,7 +941,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'a6ee6cc8-b9a0-5e23-a39e-bc3fee2aa3d3',
|
||||
universalIdentifier: 'f485cd3a-4c55-5b8f-b926-a83e5e6ba651',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -951,7 +951,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '87ad23b6-b04e-5dc1-9907-c1eca54ea6d6',
|
||||
universalIdentifier: '729b0d02-9030-5440-bd4e-f34b46d5d031',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -961,7 +961,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '6d7523dc-c097-5b75-96eb-37502d09cc19',
|
||||
universalIdentifier: '42381c24-6337-5654-8c44-6649fc6b06a8',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -971,7 +971,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'bd4fe2fa-373a-5fdd-ba09-cb5435781e4c',
|
||||
universalIdentifier: '5117f25c-d3a2-533b-b3d6-39a424e88d2a',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -984,7 +984,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'c5026f20-ab7a-54e0-900e-37a2d5381aee',
|
||||
universalIdentifier: '48df9616-8671-522f-87c7-efcd3f2e2604',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -997,7 +997,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '1dd75ff5-5fd0-51df-b3a3-a12e3aff30df',
|
||||
universalIdentifier: '5c8eb640-8c91-5712-8b73-85ce5e71abb9',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
@@ -1007,7 +1007,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '6c5e91f5-e323-555b-84e0-5a1dd7f36b1b',
|
||||
universalIdentifier: '5a5800dd-743f-533d-ad5c-f983a4bb6324',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1017,7 +1017,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: '9fa18b19-41b6-5082-b3e2-4e73f7461164',
|
||||
universalIdentifier: 'c4d0beaf-9035-514b-977c-5059b544c1bb',
|
||||
},
|
||||
{
|
||||
description: 'Post Card Recipients tied to the PostCardRecipient',
|
||||
@@ -1026,11 +1026,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Timeline Activities',
|
||||
name: 'timelineActivities',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'18b6354d-8b0f-5be6-9473-d635aed670a1',
|
||||
'a865f403-3843-5850-9a60-04e832220954',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '02f5efba-4f94-5cc3-9e17-ca7b1fe5f71b',
|
||||
universalIdentifier: '8f95b434-ee92-5c0f-95de-9d489af4d40c',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1042,11 +1042,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Attachments',
|
||||
name: 'attachments',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'ba74dfd1-e7ee-502d-abad-82e14d595874',
|
||||
'501c46b7-7140-59a0-b703-9844bbcb34b9',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '82497ec7-0121-53d6-9518-ae7434ceaf8b',
|
||||
universalIdentifier: '0c20df83-8cef-5cd6-9752-b98fb7780904',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1058,11 +1058,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Note Targets',
|
||||
name: 'noteTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'9d5a9f4b-5431-5b90-810a-1e9d8873b302',
|
||||
'721418ff-1b53-58ef-8dd7-b58927c085c6',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-fff0-4b44-be82-bda313884400',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '6de8b57e-ed0a-5d35-bd39-bd8874445810',
|
||||
universalIdentifier: 'b9642a90-2df5-555b-ab83-23cdce7a4142',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1074,11 +1074,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Task Targets',
|
||||
name: 'taskTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'85353e69-1e70-51ef-91c3-334d85013a4b',
|
||||
'feeba797-7ebb-5a36-91cd-7279ff92a2b6',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '00459e7e-8ef1-5a9b-b180-26bbb54a2696',
|
||||
universalIdentifier: 'd938a2d0-1279-5c48-b758-ac52f9460a76',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1086,7 +1086,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
],
|
||||
icon: 'IconLink',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
'a6ee6cc8-b9a0-5e23-a39e-bc3fee2aa3d3',
|
||||
'f485cd3a-4c55-5b8f-b926-a83e5e6ba651',
|
||||
labelPlural: 'Post Card Recipients',
|
||||
labelSingular: 'Post Card Recipient',
|
||||
namePlural: 'postCardRecipients',
|
||||
@@ -1180,7 +1180,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '56e652a3-fac2-54fd-a379-f01b72758268',
|
||||
universalIdentifier: 'e61745a2-969c-53d1-a809-6c583e9192d5',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1190,7 +1190,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'c59a149f-5861-51a3-a924-2bd2fa31daaa',
|
||||
universalIdentifier: '3e14e30f-8271-5a13-9c1f-ce5edb932325',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -1200,7 +1200,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '236e2ec5-3636-5b97-b19d-f7799cb88d99',
|
||||
universalIdentifier: 'b60eaf02-b008-5ebc-8fe5-123f8e92cc9e',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -1210,7 +1210,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '2cf640eb-09a7-52f2-820b-419071c5ec24',
|
||||
universalIdentifier: 'd76f15e3-e877-5b62-af97-9dac7c71065b',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1220,7 +1220,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '2eb62b4a-6888-58ab-9887-a879321fff31',
|
||||
universalIdentifier: 'a5ec9b5d-90ed-587e-8a9b-cd9d1ba43116',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -1233,7 +1233,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '36e95d21-9c86-5b6f-8133-a66d09fc8952',
|
||||
universalIdentifier: '4c72ab36-ca4a-5870-b8cc-652385020c45',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -1246,7 +1246,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'dc5231a4-e128-57b0-ac9d-723678d9b239',
|
||||
universalIdentifier: '2f481c85-025c-51cf-bccd-3375d252bf26',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
@@ -1256,7 +1256,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '3ad26df7-6345-5f85-aac1-2306cf10eaed',
|
||||
universalIdentifier: 'e1178a62-d1a5-5e9c-b730-9118c640711d',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1266,7 +1266,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: 'b49b1a1c-2f6b-52a3-836b-ece89c698627',
|
||||
universalIdentifier: 'e8569449-73b0-5f19-abc0-f6ad06c40341',
|
||||
},
|
||||
{
|
||||
description: 'Post cards tied to the PostCard',
|
||||
@@ -1275,11 +1275,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Timeline Activities',
|
||||
name: 'timelineActivities',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'1bdc52a0-0339-5360-ba17-b615250ef730',
|
||||
'47d21bb5-075f-5ed4-ba89-c0e7322a57cd',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '3f8c70b5-abb0-51ec-9109-0a94ce9c04a6',
|
||||
universalIdentifier: '9e58820c-78d7-539d-807c-d84362e1eab6',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1291,11 +1291,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Attachments',
|
||||
name: 'attachments',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'fecb8484-7e3c-59f8-a5ef-30882c5c1904',
|
||||
'4d3cf1bd-d87d-5325-8fbc-19f72c252050',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '547c93e4-c9e7-5880-b251-46230fb59bfa',
|
||||
universalIdentifier: '3f0d38c8-a50f-51c7-bdcb-84a715f4b92d',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1307,11 +1307,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Note Targets',
|
||||
name: 'noteTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'ca687361-ac2b-5722-9ad9-cd37427d087a',
|
||||
'bc1f9abe-e27b-574a-9290-a1c9c8255a6b',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-fff0-4b44-be82-bda313884400',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '47deea5c-8559-5ae7-991a-60c9f7102f86',
|
||||
universalIdentifier: 'def1f10a-847b-574e-a0a2-177caf02129c',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1323,11 +1323,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Task Targets',
|
||||
name: 'taskTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'17058c96-d5a4-53b1-96ca-786f8c5fc8bd',
|
||||
'fb610de1-db20-5e78-8e4a-7630e57148eb',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: 'ecdccc0d-7c31-5126-89d5-0bdf3372257c',
|
||||
universalIdentifier: 'b1238c2d-bc9c-53d6-8bee-eb045b9b9086',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1367,7 +1367,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '71aa41d7-54d0-5447-bf98-f37435b62bc4',
|
||||
universalIdentifier: '6c8d489d-f871-598c-92b7-929b5883ae0f',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1377,7 +1377,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'ff07c707-df79-58fa-91a4-c60c31f90e6e',
|
||||
universalIdentifier: 'c400ccb8-cd91-5ae1-81e3-2df8b6f2cf53',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -1387,7 +1387,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '8dd42a57-7aca-5e60-89c1-6e0e430dad43',
|
||||
universalIdentifier: '88b7a0a5-f2d2-5dce-89a6-e0f27577dd3f',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
@@ -1397,7 +1397,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'a81e915c-f21d-5163-a547-f04029655bf0',
|
||||
universalIdentifier: '411c5a73-100a-5255-a54d-de20a5c2ae54',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1407,7 +1407,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '33ba5fa8-dbc3-5a08-b0e0-d85b18ff15e0',
|
||||
universalIdentifier: 'd3d59a1e-2873-594a-a7de-630cde6980a9',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -1420,7 +1420,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '7c377262-63b7-5a79-ba54-f7b8acd804dc',
|
||||
universalIdentifier: 'f358b82e-1254-581e-8629-4fc4302fdbdf',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
@@ -1433,7 +1433,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '8da8e149-b60e-52e2-878d-73d05c8312b2',
|
||||
universalIdentifier: '8186fca8-89da-5542-99fe-f0ddf90e1fc1',
|
||||
},
|
||||
{
|
||||
defaultValue: 0,
|
||||
@@ -1443,7 +1443,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Position',
|
||||
name: 'position',
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: '824e930c-ce9f-5267-bebd-f6db215c3c14',
|
||||
universalIdentifier: '9a81ba2c-e4b2-56f8-a610-015decebd3aa',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
@@ -1453,7 +1453,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Search vector',
|
||||
name: 'searchVector',
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: '1f5df24b-8248-59cf-8bb6-362de121d2e4',
|
||||
universalIdentifier: '41eed58f-1016-5483-b3ff-979f270dd9a6',
|
||||
},
|
||||
{
|
||||
description: 'Recipients tied to the Recipient',
|
||||
@@ -1462,11 +1462,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Timeline Activities',
|
||||
name: 'timelineActivities',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'b01f6937-4859-587b-b3df-92e0a6f65e93',
|
||||
'1ce06ca1-6e9a-53ce-9888-9152b1b2b628',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-6736-4337-b5c4-8b39fae325a5',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '51d6d2d7-2fa8-5f77-b6cf-a96dbb801fa5',
|
||||
universalIdentifier: '1118dd8a-9781-5254-b625-2d0f05eb46fe',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1478,11 +1478,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Attachments',
|
||||
name: 'attachments',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'9c87e1b8-74a2-5833-9237-281d561d2a7d',
|
||||
'2081700d-7ce6-5698-a7da-a013feda0eb6',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-bd3d-4c60-8dca-571c71d4447a',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '244a7841-9e45-5ed1-a830-83e73c1f13fa',
|
||||
universalIdentifier: '15714c1a-d354-5407-8a15-25c117d605f8',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1494,11 +1494,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Note Targets',
|
||||
name: 'noteTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'23508618-7ed8-519e-9fee-a57b94dc98e9',
|
||||
'a93c479c-6206-5714-94ce-affa6e9886aa',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-fff0-4b44-be82-bda313884400',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: 'eab71eb1-b457-555a-8a2b-f292cef1671d',
|
||||
universalIdentifier: '167f6de7-835e-51e1-b751-34055eef6aa5',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1510,11 +1510,11 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Task Targets',
|
||||
name: 'taskTargets',
|
||||
relationTargetFieldMetadataUniversalIdentifier:
|
||||
'20500ff3-2410-5bfc-b2e7-804785c07770',
|
||||
'45752445-c4b6-5e80-8aa1-f5321a838b43',
|
||||
relationTargetObjectMetadataUniversalIdentifier:
|
||||
'20202020-5a9a-44e8-95df-771cd06d0fb1',
|
||||
type: FieldType.RELATION,
|
||||
universalIdentifier: '305f1890-04ba-5101-b18f-a1ca25a1d8a7',
|
||||
universalIdentifier: 'd0db1844-4fde-5a5a-b037-ebeabd4467af',
|
||||
universalSettings: {
|
||||
relationType: RelationType.ONE_TO_MANY,
|
||||
},
|
||||
@@ -1522,7 +1522,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
],
|
||||
icon: 'IconUser',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
'ff07c707-df79-58fa-91a4-c60c31f90e6e',
|
||||
'c400ccb8-cd91-5ae1-81e3-2df8b6f2cf53',
|
||||
labelPlural: 'Recipients',
|
||||
labelSingular: 'Recipient',
|
||||
namePlural: 'recipients',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { getApplicationUniversalIdentifierOrThrow } from '@/cli/utilities/application/get-application-universal-identifier-or-throw';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
|
||||
import { appendServerVariablesToAppConfig } from '@/cli/utilities/file/append-server-variables.util';
|
||||
@@ -45,6 +46,13 @@ export class EntityAddCommand {
|
||||
|
||||
async execute(entityType?: SyncableEntity, path?: string): Promise<void> {
|
||||
try {
|
||||
// Generated files embed identifiers derived from the application
|
||||
// universal identifier, so scaffolding is blocked until it is defined.
|
||||
const applicationUniversalIdentifier =
|
||||
await getApplicationUniversalIdentifierOrThrow(
|
||||
CURRENT_EXECUTION_DIRECTORY,
|
||||
);
|
||||
|
||||
const entity = entityType ?? (await this.getEntity());
|
||||
|
||||
const entityName = this.getFolderName(entity);
|
||||
@@ -55,7 +63,10 @@ export class EntityAddCommand {
|
||||
|
||||
await ensureDir(appPath);
|
||||
|
||||
const { name, file } = await this.getEntityData(entity);
|
||||
const { name, file } = await this.getEntityData(
|
||||
entity,
|
||||
applicationUniversalIdentifier,
|
||||
);
|
||||
|
||||
const filePath = join(appPath, this.getFileName(name, entity));
|
||||
|
||||
@@ -74,7 +85,11 @@ export class EntityAddCommand {
|
||||
);
|
||||
|
||||
if (entity === SyncableEntity.Object) {
|
||||
await this.promptAndCreateObjectCompanions(name, path);
|
||||
await this.promptAndCreateObjectCompanions({
|
||||
objectName: name,
|
||||
applicationUniversalIdentifier,
|
||||
customPath: path,
|
||||
});
|
||||
}
|
||||
|
||||
if (entity === SyncableEntity.ConnectionProvider) {
|
||||
@@ -89,7 +104,10 @@ export class EntityAddCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async getEntityData(entity: SyncableEntity) {
|
||||
private async getEntityData(
|
||||
entity: SyncableEntity,
|
||||
applicationUniversalIdentifier: string,
|
||||
) {
|
||||
switch (entity) {
|
||||
case SyncableEntity.Object: {
|
||||
const entityData = await this.getObjectData();
|
||||
@@ -192,6 +210,7 @@ export class EntityAddCommand {
|
||||
|
||||
const file = getViewBaseFile({
|
||||
name,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
return { name, file };
|
||||
@@ -259,10 +278,15 @@ export class EntityAddCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async promptAndCreateObjectCompanions(
|
||||
objectName: string,
|
||||
customPath?: string,
|
||||
): Promise<void> {
|
||||
private async promptAndCreateObjectCompanions({
|
||||
objectName,
|
||||
applicationUniversalIdentifier,
|
||||
customPath,
|
||||
}: {
|
||||
objectName: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
customPath?: string;
|
||||
}): Promise<void> {
|
||||
const { createCompanions } = await inquirer.prompt<{
|
||||
createCompanions: boolean;
|
||||
}>([
|
||||
@@ -286,6 +310,7 @@ export class EntityAddCommand {
|
||||
name: `all-${kebabCase(objectName)}`,
|
||||
universalIdentifier: viewUniversalIdentifier,
|
||||
objectUniversalIdentifier: this.lastObjectUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
fields: this.lastNameFieldUniversalIdentifier
|
||||
? [
|
||||
{
|
||||
@@ -335,6 +360,7 @@ export class EntityAddCommand {
|
||||
name: `${kebabCase(objectName)}-record-page-fields`,
|
||||
universalIdentifier: fieldsWidgetViewUniversalIdentifier,
|
||||
objectUniversalIdentifier: this.lastObjectUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
type: ViewType.FIELDS_WIDGET,
|
||||
fields: recordPageFieldsViewFields,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { pathExists } from '@/cli/utilities/file/fs-utils';
|
||||
|
||||
export const APP_CONFIG_CANDIDATE_PATHS = [
|
||||
'src/application.config.ts',
|
||||
'src/application-config.ts',
|
||||
'src/applicationConfig.ts',
|
||||
];
|
||||
|
||||
export const findAppConfigPath = async (
|
||||
projectRoot: string,
|
||||
): Promise<string | null> => {
|
||||
for (const candidate of APP_CONFIG_CANDIDATE_PATHS) {
|
||||
const absolute = join(projectRoot, candidate);
|
||||
|
||||
if (await pathExists(absolute)) {
|
||||
return absolute;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
APP_CONFIG_CANDIDATE_PATHS,
|
||||
findAppConfigPath,
|
||||
} from '@/cli/utilities/application/find-app-config-path';
|
||||
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
|
||||
|
||||
type ApplicationConfigWithUniversalIdentifier = {
|
||||
universalIdentifier?: string;
|
||||
};
|
||||
|
||||
// Scaffolding derives deterministic identifiers from the application
|
||||
// universal identifier, so nothing can be generated before defineApplication
|
||||
// declares one.
|
||||
export const getApplicationUniversalIdentifierOrThrow = async (
|
||||
appPath: string,
|
||||
): Promise<string> => {
|
||||
const configPath = await findAppConfigPath(appPath);
|
||||
|
||||
if (configPath === null) {
|
||||
throw new Error(
|
||||
`Could not find the application config file (expected one of: ${APP_CONFIG_CANDIDATE_PATHS.join(', ')}). Create it with defineApplication({ universalIdentifier: ... }) before generating entities.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { config } =
|
||||
await extractManifestFromFile<ApplicationConfigWithUniversalIdentifier>({
|
||||
appPath,
|
||||
filePath: configPath,
|
||||
});
|
||||
|
||||
if (!config?.universalIdentifier) {
|
||||
throw new Error(
|
||||
`defineApplication in ${configPath} must declare a universalIdentifier before generating entities.`,
|
||||
);
|
||||
}
|
||||
|
||||
return config.universalIdentifier;
|
||||
};
|
||||
@@ -14,10 +14,10 @@ import { validateViewFilterOperands } from '@/cli/utilities/build/manifest/utils
|
||||
import { type ApplicationConfig, type LogicFunctionConfig } from '@/sdk/define';
|
||||
import { type CommandMenuItemConfig } from '@/sdk/define/command-menu-items/command-menu-item-config';
|
||||
import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config';
|
||||
import { type IndexConfig } from '@/sdk/define/indexes/index-config';
|
||||
import { type PostInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/post-install-logic-function-config';
|
||||
import { type PreInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/pre-install-logic-function-config';
|
||||
import { type ObjectConfig } from '@/sdk/define/objects/object-config';
|
||||
import { type IndexConfig } from '@/sdk/define/indexes/index-config';
|
||||
import { type PageLayoutConfig } from '@/sdk/define/page-layouts/page-layout-config';
|
||||
import { type PageLayoutTabConfig } from '@/sdk/define/page-layouts/page-layout-tab-config';
|
||||
import { type RoleConfig } from '@/sdk/define/roles/role-config';
|
||||
@@ -96,6 +96,7 @@ export const buildManifest = async (
|
||||
const warnings: string[] = [];
|
||||
|
||||
let applicationConfig: ApplicationConfig | undefined;
|
||||
const objectConfigs: ObjectConfig[] = [];
|
||||
const objects: ObjectManifest[] = [];
|
||||
const fields: FieldManifest[] = [];
|
||||
const indexes: IndexManifest[] = [];
|
||||
@@ -172,31 +173,7 @@ export const buildManifest = async (
|
||||
filePath,
|
||||
});
|
||||
|
||||
const {
|
||||
objectFields: objectFieldsWithDefaults,
|
||||
fields: reverseRelationFields,
|
||||
} = getDefaultFieldsInObjectFields(extract.config);
|
||||
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
extract.config.labelIdentifierFieldMetadataUniversalIdentifier ??
|
||||
objectFieldsWithDefaults.find((field) => field.name === 'name')
|
||||
?.universalIdentifier;
|
||||
|
||||
if (!labelIdentifierFieldMetadataUniversalIdentifier) {
|
||||
errors.push(
|
||||
`No label identifier field found for object ${extract.config.nameSingular}. Please add a field with name "name" to your object.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const objectManifest: ObjectManifest = {
|
||||
...extract.config,
|
||||
fields: objectFieldsWithDefaults.map(addMissingFieldOptionIds),
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
};
|
||||
|
||||
objects.push(objectManifest);
|
||||
fields.push(...reverseRelationFields);
|
||||
objectConfigs.push(extract.config);
|
||||
|
||||
errors.push(...extract.errors);
|
||||
warnings.push(...(extract.warnings ?? []));
|
||||
@@ -522,6 +499,39 @@ export const buildManifest = async (
|
||||
);
|
||||
}
|
||||
|
||||
if (applicationConfig) {
|
||||
for (const objectConfig of objectConfigs) {
|
||||
const {
|
||||
objectFields: objectFieldsWithDefaults,
|
||||
fields: reverseRelationFields,
|
||||
} = getDefaultFieldsInObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier: applicationConfig.universalIdentifier,
|
||||
});
|
||||
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
objectConfig.labelIdentifierFieldMetadataUniversalIdentifier ??
|
||||
objectFieldsWithDefaults.find((field) => field.name === 'name')
|
||||
?.universalIdentifier;
|
||||
|
||||
if (!labelIdentifierFieldMetadataUniversalIdentifier) {
|
||||
errors.push(
|
||||
`No label identifier field found for object ${objectConfig.nameSingular}. Please add a field with name "name" to your object.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectManifest: ObjectManifest = {
|
||||
...objectConfig,
|
||||
fields: objectFieldsWithDefaults.map(addMissingFieldOptionIds),
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
};
|
||||
|
||||
objects.push(objectManifest);
|
||||
fields.push(...reverseRelationFields);
|
||||
}
|
||||
}
|
||||
|
||||
if (postInstallLogicFunctions.length > 1) {
|
||||
errors.push(
|
||||
'Only one post install logic function is allowed per application',
|
||||
|
||||
+30
-13
@@ -14,15 +14,32 @@ const baseObjectConfig: ObjectConfig = {
|
||||
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 } =
|
||||
getDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const defaultFields = getDefaultObjectFields(baseObjectConfig);
|
||||
getMockDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const defaultFields = getMockDefaultObjectFields(baseObjectConfig);
|
||||
const {
|
||||
objectFields: defaultRelationObjectFields,
|
||||
fields: expectedReverseFields,
|
||||
} = getDefaultRelationObjectFields(baseObjectConfig);
|
||||
} = getMockDefaultRelationObjectFields(baseObjectConfig);
|
||||
|
||||
expect(objectFields).toEqual([
|
||||
...defaultFields,
|
||||
@@ -56,10 +73,10 @@ describe('getDefaultFieldsInObjectFields', () => {
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getDefaultObjectFields(objectConfig);
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getMockDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields } =
|
||||
getDefaultRelationObjectFields(objectConfig);
|
||||
getMockDefaultRelationObjectFields(objectConfig);
|
||||
|
||||
expect(objectFields[0]).toEqual(customField);
|
||||
expect(objectFields).toHaveLength(
|
||||
@@ -80,7 +97,7 @@ describe('getDefaultFieldsInObjectFields', () => {
|
||||
fields: [customIdField],
|
||||
};
|
||||
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
const idFields = objectFields.filter((f) => f.name === 'id');
|
||||
|
||||
@@ -115,10 +132,10 @@ describe('getDefaultFieldsInObjectFields', () => {
|
||||
fields: customFields,
|
||||
};
|
||||
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getDefaultObjectFields(objectConfig);
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getMockDefaultObjectFields(objectConfig);
|
||||
const { objectFields: defaultRelationObjectFields } =
|
||||
getDefaultRelationObjectFields(objectConfig);
|
||||
getMockDefaultRelationObjectFields(objectConfig);
|
||||
const overriddenCount = defaultFields.filter((df) =>
|
||||
customFields.some((cf) => cf.name === df.name),
|
||||
).length;
|
||||
@@ -148,7 +165,7 @@ describe('getDefaultFieldsInObjectFields', () => {
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
|
||||
const { objectFields } = getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(objectFields[0]).toEqual(customField);
|
||||
});
|
||||
@@ -168,13 +185,13 @@ describe('getDefaultFieldsInObjectFields', () => {
|
||||
|
||||
const originalLength = objectConfig.fields.length;
|
||||
|
||||
getDefaultFieldsInObjectFields(objectConfig);
|
||||
getMockDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(objectConfig.fields).toHaveLength(originalLength);
|
||||
});
|
||||
|
||||
it('should return reverse relation fields for each default relation', () => {
|
||||
const { fields } = getDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const { fields } = getMockDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(4);
|
||||
|
||||
|
||||
+22
-12
@@ -12,21 +12,31 @@ const mockObjectConfig: ObjectConfig = {
|
||||
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 = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('should include an id field with UUID type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const idField = fields.find((field) => field.name === 'id');
|
||||
|
||||
expect(idField).toBeDefined();
|
||||
@@ -43,7 +53,7 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should include a name field with TEXT type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const nameField = fields.find((field) => field.name === 'name');
|
||||
|
||||
expect(nameField).toBeDefined();
|
||||
@@ -60,7 +70,7 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should include createdAt, updatedAt and deletedAt fields with DATE_TIME type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
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');
|
||||
@@ -103,7 +113,7 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should include createdBy and updatedBy fields with ACTOR type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const createdByField = fields.find((field) => field.name === 'createdBy');
|
||||
const updatedByField = fields.find((field) => field.name === 'updatedBy');
|
||||
|
||||
@@ -133,7 +143,7 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should include a position field with POSITION type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const positionField = fields.find((field) => field.name === 'position');
|
||||
|
||||
expect(positionField).toBeDefined();
|
||||
@@ -150,7 +160,7 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should include a searchVector field with TS_VECTOR type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const searchVectorField = fields.find(
|
||||
(field) => field.name === 'searchVector',
|
||||
);
|
||||
@@ -169,8 +179,8 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should generate deterministic universalIdentifiers based on objectConfig', () => {
|
||||
const firstResult = getDefaultObjectFields(mockObjectConfig);
|
||||
const secondResult = getDefaultObjectFields(mockObjectConfig);
|
||||
const firstResult = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const secondResult = getMockDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
firstResult.forEach((field, index) => {
|
||||
expect(field.universalIdentifier).toBe(
|
||||
@@ -185,8 +195,8 @@ describe('getDefaultObjectFields', () => {
|
||||
universalIdentifier: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||
};
|
||||
|
||||
const fieldsA = getDefaultObjectFields(mockObjectConfig);
|
||||
const fieldsB = getDefaultObjectFields(otherObjectConfig);
|
||||
const fieldsA = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const fieldsB = getMockDefaultObjectFields(otherObjectConfig);
|
||||
|
||||
fieldsA.forEach((fieldA, index) => {
|
||||
expect(fieldA.universalIdentifier).not.toBe(
|
||||
@@ -196,7 +206,7 @@ describe('getDefaultObjectFields', () => {
|
||||
});
|
||||
|
||||
it('should return fields in the expected order', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fields = getMockDefaultObjectFields(mockObjectConfig);
|
||||
const fieldNames = fields.map((field) => field.name);
|
||||
|
||||
expect(fieldNames).toEqual([
|
||||
|
||||
+15
-5
@@ -6,12 +6,22 @@ import {
|
||||
type ObjectFieldManifest,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
export const getDefaultFieldsInObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
const defaultObjectFields = getDefaultObjectFields(objectConfig);
|
||||
export const getDefaultFieldsInObjectFields = ({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
objectConfig: ObjectConfig;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
const defaultObjectFields = getDefaultObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
const { objectFields: defaultRelationObjectFields, fields: reverseFields } =
|
||||
getDefaultRelationObjectFields(objectConfig);
|
||||
getDefaultRelationObjectFields({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name);
|
||||
|
||||
|
||||
+16
-3
@@ -3,9 +3,13 @@ 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: ObjectConfig,
|
||||
): ObjectFieldManifest[] => {
|
||||
export const getDefaultObjectFields = ({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
objectConfig: ObjectConfig;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): ObjectFieldManifest[] => {
|
||||
const objectUniversalIdentifier = objectConfig.universalIdentifier;
|
||||
|
||||
const idField: ObjectFieldManifest = {
|
||||
@@ -17,6 +21,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: 'uuid',
|
||||
type: FieldMetadataType.UUID as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'id',
|
||||
}),
|
||||
@@ -31,6 +36,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'name',
|
||||
}),
|
||||
@@ -45,6 +51,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'createdAt',
|
||||
}),
|
||||
@@ -59,6 +66,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'updatedAt',
|
||||
}),
|
||||
@@ -73,6 +81,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.DATE_TIME as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'deletedAt',
|
||||
}),
|
||||
@@ -87,6 +96,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'createdBy',
|
||||
}),
|
||||
@@ -101,6 +111,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR as const,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'updatedBy',
|
||||
}),
|
||||
@@ -115,6 +126,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: 0,
|
||||
type: FieldMetadataType.POSITION,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'position',
|
||||
}),
|
||||
@@ -129,6 +141,7 @@ export const getDefaultObjectFields = (
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName: 'searchVector',
|
||||
}),
|
||||
|
||||
+12
-6
@@ -1,5 +1,5 @@
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
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,
|
||||
@@ -119,9 +119,13 @@ const buildReverseField = ({
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultRelationObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
export const getDefaultRelationObjectFields = ({
|
||||
objectConfig,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
objectConfig: ObjectConfig;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
|
||||
const objectFields: ObjectFieldManifest[] = [];
|
||||
const fields: FieldManifest[] = [];
|
||||
|
||||
@@ -130,14 +134,16 @@ export const getDefaultRelationObjectFields = (
|
||||
|
||||
const forwardFieldUniversalIdentifier =
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectConfig.universalIdentifier,
|
||||
fieldName: config.fieldName,
|
||||
});
|
||||
|
||||
const reverseFieldUniversalIdentifier =
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
objectUniversalIdentifier: objectConfig.universalIdentifier,
|
||||
fieldName: `${config.fieldName}Inverse`,
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: standardObject.universalIdentifier,
|
||||
fieldName: config.targetFieldName(objectConfig),
|
||||
});
|
||||
|
||||
const forwardField: ObjectFieldManifest = {
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
|
||||
|
||||
const APPLICATION_UNIVERSAL_IDENTIFIER = 'app-abc-123';
|
||||
|
||||
const getTestViewBaseFile = (
|
||||
overrides: Omit<
|
||||
Parameters<typeof getViewBaseFile>[0],
|
||||
'applicationUniversalIdentifier'
|
||||
>,
|
||||
) =>
|
||||
getViewBaseFile({
|
||||
applicationUniversalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getViewBaseFile', () => {
|
||||
it('should render proper file using defineView', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'my-view',
|
||||
universalIdentifier: '71e45a58-41da-4ae4-8b73-a543c0a9d3d4',
|
||||
objectUniversalIdentifier: 'obj-abc-123',
|
||||
@@ -20,7 +33,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should use default objectUniversalIdentifier when not provided', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'default-view',
|
||||
});
|
||||
|
||||
@@ -28,7 +41,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should include commented fields, filters and sorts when no fields provided', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'empty-view',
|
||||
});
|
||||
|
||||
@@ -38,7 +51,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should render fields block when fields are provided', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-with-fields',
|
||||
fields: [
|
||||
{
|
||||
@@ -61,7 +74,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should apply default values to fields', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-defaults',
|
||||
fields: [
|
||||
{
|
||||
@@ -77,7 +90,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should generate unique UUID when not provided', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'auto-uuid-view',
|
||||
});
|
||||
|
||||
@@ -87,7 +100,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should use kebab-case for name', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'all active contacts',
|
||||
});
|
||||
|
||||
@@ -95,7 +108,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should generate UUIDs for fields when not provided', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-auto-field-uuids',
|
||||
fields: [
|
||||
{
|
||||
@@ -115,7 +128,7 @@ describe('getViewBaseFile', () => {
|
||||
});
|
||||
|
||||
it('should emit a generateDefaultFieldUniversalIdentifier call for fields using defaultFieldName', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-default-field',
|
||||
objectUniversalIdentifier: 'obj-abc-123',
|
||||
fields: [
|
||||
@@ -132,12 +145,13 @@ describe('getViewBaseFile', () => {
|
||||
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', () => {
|
||||
const result = getViewBaseFile({
|
||||
const result = getTestViewBaseFile({
|
||||
name: 'view-literal-only',
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -17,10 +17,12 @@ const renderFieldEntry = ({
|
||||
field,
|
||||
index,
|
||||
objectUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
field: ViewFieldTemplate;
|
||||
index: number;
|
||||
objectUniversalIdentifier: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}) => {
|
||||
const universalIdentifier = field.universalIdentifier ?? v4();
|
||||
const position = field.position ?? index;
|
||||
@@ -30,6 +32,7 @@ const renderFieldEntry = ({
|
||||
const fieldMetadataUniversalIdentifierLine =
|
||||
'defaultFieldName' in field
|
||||
? ` fieldMetadataUniversalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: '${applicationUniversalIdentifier}',
|
||||
objectUniversalIdentifier: '${objectUniversalIdentifier}',
|
||||
fieldName: '${field.defaultFieldName}',
|
||||
})`
|
||||
@@ -48,12 +51,14 @@ export const getViewBaseFile = ({
|
||||
name,
|
||||
universalIdentifier = v4(),
|
||||
objectUniversalIdentifier = 'fill-later',
|
||||
applicationUniversalIdentifier,
|
||||
fields = [],
|
||||
type,
|
||||
}: {
|
||||
name: string;
|
||||
universalIdentifier?: string;
|
||||
objectUniversalIdentifier?: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
fields?: ViewFieldTemplate[];
|
||||
type?: ViewType;
|
||||
}) => {
|
||||
@@ -77,7 +82,12 @@ export const getViewBaseFile = ({
|
||||
? ` fields: [
|
||||
${fields
|
||||
.map((field, index) =>
|
||||
renderFieldEntry({ field, index, objectUniversalIdentifier }),
|
||||
renderFieldEntry({
|
||||
field,
|
||||
index,
|
||||
objectUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
}),
|
||||
)
|
||||
.join(',\n')},
|
||||
],`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
import { pathExists } from '@/cli/utilities/file/fs-utils';
|
||||
import { findAppConfigPath } from '@/cli/utilities/application/find-app-config-path';
|
||||
|
||||
// One name + description pair to add to defineApplication.serverVariables.
|
||||
// `isSecret` defaults to false; set true on credentials that should be
|
||||
@@ -11,12 +11,6 @@ export type ServerVariableSpec = {
|
||||
isSecret: boolean;
|
||||
};
|
||||
|
||||
const APP_CONFIG_CANDIDATES = [
|
||||
'src/application.config.ts',
|
||||
'src/application-config.ts',
|
||||
'src/applicationConfig.ts',
|
||||
];
|
||||
|
||||
const SERVER_VARIABLES_PATTERN = /serverVariables\s*:\s*\{/;
|
||||
const DEFINE_APPLICATION_PATTERN = /defineApplication\s*\(\s*\{/;
|
||||
|
||||
@@ -102,20 +96,6 @@ export const appendServerVariablesToAppConfig = async ({
|
||||
return { status: 'created', file: configPath };
|
||||
};
|
||||
|
||||
const findAppConfigPath = async (
|
||||
projectRoot: string,
|
||||
): Promise<string | null> => {
|
||||
for (const candidate of APP_CONFIG_CANDIDATES) {
|
||||
const absolute = `${projectRoot}/${candidate}`;
|
||||
|
||||
if (await pathExists(absolute)) {
|
||||
return absolute;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderServerVariableEntries = (variables: ServerVariableSpec[]): string =>
|
||||
variables
|
||||
.map(
|
||||
|
||||
+41
-3
@@ -1,19 +1,25 @@
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/sdk/define/objects/generate-default-field-universal-identifier';
|
||||
|
||||
describe('generateDefaultFieldUniversalIdentifier', () => {
|
||||
it('should generate a unique universal identifier', () => {
|
||||
const objectUniversalIdentifier = '55b79f88-4094-4b3f-a0ac-1a91a55714f2';
|
||||
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',
|
||||
});
|
||||
@@ -21,4 +27,36 @@ describe('generateDefaultFieldUniversalIdentifier', () => {
|
||||
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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+9
-8
@@ -1,15 +1,16 @@
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
const UNIVERSAL_IDENTIFIER_NAMESPACE = '142046f0-4d80-48b5-ad56-26ad410e895c';
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
|
||||
export const generateDefaultFieldUniversalIdentifier = ({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
fieldName,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
objectUniversalIdentifier: string;
|
||||
fieldName: string;
|
||||
}) => {
|
||||
const seed = `${objectUniversalIdentifier}-${fieldName}`;
|
||||
|
||||
return v5(seed, UNIVERSAL_IDENTIFIER_NAMESPACE);
|
||||
};
|
||||
}) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: fieldName,
|
||||
});
|
||||
|
||||
+6
@@ -4,13 +4,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { BackfillWorkspaceCustomApplicationRegistrationCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-workspace-command-1782853718000-backfill-workspace-custom-application-registration.command';
|
||||
import { BackfillSystemUniqueIndexUniversalIdentifierCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-workspace-command-1783093620000-backfill-system-unique-index-universal-identifier.command';
|
||||
import { BackfillDeterministicFieldUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/2-19/2-19-workspace-command-1783100000000-backfill-deterministic-field-universal-identifiers.command';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -18,16 +21,19 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
TypeOrmModule.forFeature([
|
||||
WorkspaceEntity,
|
||||
ApplicationEntity,
|
||||
FieldMetadataEntity,
|
||||
IndexMetadataEntity,
|
||||
]),
|
||||
WorkspaceIteratorModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
],
|
||||
providers: [
|
||||
BackfillWorkspaceCustomApplicationRegistrationCommand,
|
||||
BackfillSystemUniqueIndexUniversalIdentifierCommand,
|
||||
BackfillDeterministicFieldUniversalIdentifiersCommand,
|
||||
],
|
||||
})
|
||||
export class V2_19_UpgradeVersionCommandModule {}
|
||||
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
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 { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
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 { 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 { 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 { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service';
|
||||
|
||||
// Server-owned system field names: their universal identifiers are taken
|
||||
// over for every application, whatever value they currently hold, since
|
||||
// validateObjectMetadataSystemFieldsIntegrity now rejects any non-derived
|
||||
// value at sync time. Fields are picked explicitly so system fields added to
|
||||
// PARTIAL_SYSTEM_FLAT_FIELD_METADATAS later never alter this shipped
|
||||
// migration.
|
||||
const SYSTEM_FIELD_NAMES = new Set<string>([
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.id.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.createdAt.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.updatedAt.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.deletedAt.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.createdBy.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.updatedBy.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.position.name,
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS.searchVector.name,
|
||||
]);
|
||||
|
||||
// The name field is a default field, not a system field: it is only taken
|
||||
// over where it is guaranteed to be auto-provisioned (workspace-custom
|
||||
// objects). For installed applications authors may define it themselves, so
|
||||
// only legacy SDK-derived values are converged; for the standard application
|
||||
// it is author-provided in STANDARD_OBJECTS and keeps its hardcoded value.
|
||||
const NAME_FIELD_NAME = 'name';
|
||||
|
||||
const DEFAULT_RELATION_FORWARD_FIELD_NAMES = new Set([
|
||||
'timelineActivities',
|
||||
'attachments',
|
||||
'noteTargets',
|
||||
'taskTargets',
|
||||
]);
|
||||
|
||||
const DEFAULT_RELATION_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS = new Set<string>([
|
||||
STANDARD_OBJECTS.timelineActivity.universalIdentifier,
|
||||
STANDARD_OBJECTS.attachment.universalIdentifier,
|
||||
STANDARD_OBJECTS.noteTarget.universalIdentifier,
|
||||
STANDARD_OBJECTS.taskTarget.universalIdentifier,
|
||||
]);
|
||||
|
||||
// Namespace the SDK used before default field universal identifiers were
|
||||
// aligned with getFieldUniversalIdentifier; only rows still carrying a
|
||||
// legacy-derived value are backfilled for installed applications, so
|
||||
// author-provided identifiers are left untouched.
|
||||
const LEGACY_SDK_UNIVERSAL_IDENTIFIER_NAMESPACE =
|
||||
'142046f0-4d80-48b5-ad56-26ad410e895c';
|
||||
|
||||
const computeLegacySdkDefaultFieldUniversalIdentifier = ({
|
||||
objectUniversalIdentifier,
|
||||
fieldName,
|
||||
}: {
|
||||
objectUniversalIdentifier: string;
|
||||
fieldName: string;
|
||||
}) =>
|
||||
v5(
|
||||
`${objectUniversalIdentifier}-${fieldName}`,
|
||||
LEGACY_SDK_UNIVERSAL_IDENTIFIER_NAMESPACE,
|
||||
);
|
||||
|
||||
const isMorphOrRelationFieldMetadataType = (type: FieldMetadataType) =>
|
||||
type === FieldMetadataType.RELATION ||
|
||||
type === FieldMetadataType.MORPH_RELATION;
|
||||
|
||||
@RegisteredWorkspaceCommand('2.19.0', 1783100000000)
|
||||
@Command({
|
||||
name: 'upgrade:2-19:backfill-deterministic-field-universal-identifiers',
|
||||
description:
|
||||
'Recompute the universal identifier of auto-provisioned field metadata (system fields and default relation fields) to the deterministic getFieldUniversalIdentifier derivation.',
|
||||
})
|
||||
export class BackfillDeterministicFieldUniversalIdentifiersCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
|
||||
constructor(
|
||||
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(FieldMetadataEntity)
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {
|
||||
super(workspaceIteratorService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const { twentyStandardFlatApplication, workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const applications = await this.applicationRepository.find({
|
||||
select: ['id', 'universalIdentifier'],
|
||||
where: { workspaceId },
|
||||
withDeleted: true,
|
||||
});
|
||||
const applicationUniversalIdentifierById = new Map(
|
||||
applications.map((application) => [
|
||||
application.id,
|
||||
application.universalIdentifier,
|
||||
]),
|
||||
);
|
||||
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const updates: { id: string; newUniversalIdentifier: string }[] = [];
|
||||
|
||||
for (const flatFieldMetadata of Object.values(
|
||||
flatFieldMetadataMaps.byUniversalIdentifier,
|
||||
)) {
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flatObjectMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: flatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
const applicationUniversalIdentifier =
|
||||
applicationUniversalIdentifierById.get(flatFieldMetadata.applicationId);
|
||||
|
||||
if (
|
||||
!isDefined(flatObjectMetadata) ||
|
||||
!isDefined(applicationUniversalIdentifier)
|
||||
) {
|
||||
this.logger.warn(
|
||||
`Missing object or application for field ${flatFieldMetadata.name} (${flatFieldMetadata.id}) in workspace ${workspaceId}, skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const shouldBackfill = this.shouldBackfillFieldMetadata({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
});
|
||||
|
||||
if (!shouldBackfill) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
name: flatFieldMetadata.name,
|
||||
});
|
||||
|
||||
if (newUniversalIdentifier === flatFieldMetadata.universalIdentifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
updates.push({ id: flatFieldMetadata.id, newUniversalIdentifier });
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
this.logger.log(
|
||||
`No field universal identifiers to backfill for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Backfilling ${updates.length} field universal identifier(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (isDryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fieldMetadataRepository.manager.transaction(
|
||||
async (entityManager) => {
|
||||
const transactionalFieldMetadataRepository =
|
||||
entityManager.getRepository(FieldMetadataEntity);
|
||||
|
||||
for (const { id, newUniversalIdentifier } of updates) {
|
||||
await transactionalFieldMetadataRepository.update(
|
||||
{ id, workspaceId },
|
||||
{ universalIdentifier: newUniversalIdentifier },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const fieldMetadataRelatedNames = [
|
||||
'fieldMetadata',
|
||||
...getMetadataRelatedMetadataNames('fieldMetadata'),
|
||||
...getMetadataSerializedRelationNames('fieldMetadata'),
|
||||
'index',
|
||||
] as const;
|
||||
const allFlatEntityMapsKeys = [
|
||||
...new Set(fieldMetadataRelatedNames.map(getMetadataFlatEntityMapsKey)),
|
||||
];
|
||||
|
||||
await this.workspaceMigrationRunnerService.invalidateCache({
|
||||
allFlatEntityMapsKeys,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Backfilled ${updates.length} field universal identifier(s) for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
private shouldBackfillFieldMetadata({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
flatObjectMetadataMaps,
|
||||
flatFieldMetadataMaps,
|
||||
twentyStandardApplicationId,
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
flatFieldMetadata: FlatFieldMetadata;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
twentyStandardApplicationId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
}): boolean {
|
||||
// System field universal identifiers are server-owned: they are taken
|
||||
// over for every application. Sync now rejects non-derived values
|
||||
// (validateObjectMetadataSystemFieldsIntegrity), so converging every row
|
||||
// here is both safe and required.
|
||||
if (SYSTEM_FIELD_NAMES.has(flatFieldMetadata.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standard application: every non-system standard field (including name)
|
||||
// is author-provided in STANDARD_OBJECTS and keeps its hardcoded value.
|
||||
if (flatFieldMetadata.applicationId === twentyStandardApplicationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const relationTargetFlatObjectMetadata = isDefined(
|
||||
flatFieldMetadata.relationTargetObjectMetadataId,
|
||||
)
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: flatFieldMetadata.relationTargetObjectMetadataId,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (flatFieldMetadata.applicationId === workspaceCustomApplicationId) {
|
||||
if (flatFieldMetadata.name === NAME_FIELD_NAME) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isMorphOrRelationFieldMetadataType(flatFieldMetadata.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Forward default relation fields on custom objects (attachments,
|
||||
// noteTargets, taskTargets, timelineActivities).
|
||||
if (
|
||||
DEFAULT_RELATION_FORWARD_FIELD_NAMES.has(flatFieldMetadata.name) &&
|
||||
isDefined(relationTargetFlatObjectMetadata) &&
|
||||
DEFAULT_RELATION_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.has(
|
||||
relationTargetFlatObjectMetadata.universalIdentifier,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reverse default relation fields living on the standard relation
|
||||
// objects and pointing back at the custom object.
|
||||
if (
|
||||
DEFAULT_RELATION_STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.has(
|
||||
flatObjectMetadata.universalIdentifier,
|
||||
) &&
|
||||
isDefined(relationTargetFlatObjectMetadata) &&
|
||||
(flatFieldMetadata.name ===
|
||||
`target${capitalize(relationTargetFlatObjectMetadata.nameSingular)}` ||
|
||||
flatFieldMetadata.name ===
|
||||
relationTargetFlatObjectMetadata.nameSingular)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Installed applications, remaining auto-provisioned fields (injected
|
||||
// name field and default relation fields): only rows still carrying an
|
||||
// SDK-auto-generated (legacy derivation) identifier are backfilled;
|
||||
// author-provided identifiers must keep matching the application source
|
||||
// code.
|
||||
const legacyDefaultUniversalIdentifier =
|
||||
computeLegacySdkDefaultFieldUniversalIdentifier({
|
||||
objectUniversalIdentifier: flatObjectMetadata.universalIdentifier,
|
||||
fieldName: flatFieldMetadata.name,
|
||||
});
|
||||
|
||||
if (
|
||||
flatFieldMetadata.universalIdentifier === legacyDefaultUniversalIdentifier
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Legacy reverse default relation fields were derived from the custom
|
||||
// object universal identifier and the forward field name suffixed with
|
||||
// "Inverse".
|
||||
const relationTargetFlatFieldMetadata = isDefined(
|
||||
flatFieldMetadata.relationTargetFieldMetadataId,
|
||||
)
|
||||
? findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
flatEntityId: flatFieldMetadata.relationTargetFieldMetadataId,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
isDefined(relationTargetFlatObjectMetadata) &&
|
||||
isDefined(relationTargetFlatFieldMetadata)
|
||||
) {
|
||||
const legacyReverseUniversalIdentifier =
|
||||
computeLegacySdkDefaultFieldUniversalIdentifier({
|
||||
objectUniversalIdentifier:
|
||||
relationTargetFlatObjectMetadata.universalIdentifier,
|
||||
fieldName: `${relationTargetFlatFieldMetadata.name}Inverse`,
|
||||
});
|
||||
|
||||
if (
|
||||
flatFieldMetadata.universalIdentifier ===
|
||||
legacyReverseUniversalIdentifier
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+1
@@ -33,6 +33,7 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/se
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
|
||||
import { getWorkspaceScopedRepositoryToken } from 'src/engine/twenty-orm/workspace-scoped-repository/get-workspace-scoped-repository-token.util';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'mocked-file-id'),
|
||||
}));
|
||||
|
||||
|
||||
+8
-2
@@ -62,6 +62,8 @@ type GenerateMorphOrRelationFlatFieldMetadataPairArgs = {
|
||||
flatApplication: FlatApplication;
|
||||
targetFieldName?: string;
|
||||
junctionTargetFlatFieldMetadata?: FlatFieldMetadata;
|
||||
sourceFieldUniversalIdentifier?: string;
|
||||
targetFieldUniversalIdentifier?: string;
|
||||
};
|
||||
|
||||
export type SourceTargetMorphOrRelationFlatFieldAndFlatIndex = {
|
||||
@@ -79,6 +81,8 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
morphId = null,
|
||||
targetFieldName,
|
||||
junctionTargetFlatFieldMetadata,
|
||||
sourceFieldUniversalIdentifier: providedSourceFieldUniversalIdentifier,
|
||||
targetFieldUniversalIdentifier: providedTargetFieldUniversalIdentifier,
|
||||
}: GenerateMorphOrRelationFlatFieldMetadataPairArgs): SourceTargetMorphOrRelationFlatFieldAndFlatIndex => {
|
||||
const sourceFlatFieldMetadataType = createFieldInput.type;
|
||||
|
||||
@@ -91,8 +95,10 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({
|
||||
junctionTargetFieldUniversalIdentifier:
|
||||
junctionTargetFlatFieldMetadata?.universalIdentifier,
|
||||
});
|
||||
const sourceFieldUniversalIdentifier = v4();
|
||||
const targetFieldUniversalIdentifier = v4();
|
||||
const sourceFieldUniversalIdentifier =
|
||||
providedSourceFieldUniversalIdentifier ?? v4();
|
||||
const targetFieldUniversalIdentifier =
|
||||
providedTargetFieldUniversalIdentifier ?? v4();
|
||||
|
||||
const defaultDescriptionFromField =
|
||||
buildDescriptionForRelationFieldMetadataOnFromField({
|
||||
|
||||
+25
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -66,6 +67,30 @@ export const validateObjectMetadataSystemFieldsIntegrity = ({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-10
@@ -1,5 +1,5 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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';
|
||||
@@ -39,10 +39,17 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
updatedBy,
|
||||
} = PARTIAL_SYSTEM_FLAT_FIELD_METADATAS;
|
||||
|
||||
const computeFieldUniversalIdentifier = (name: string) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name,
|
||||
});
|
||||
|
||||
return {
|
||||
id: {
|
||||
...id,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(id.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -50,7 +57,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
createdAt: {
|
||||
...createdAt,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(createdAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -58,7 +65,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
createdBy: {
|
||||
...createdBy,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(createdBy.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -66,7 +73,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
deletedAt: {
|
||||
...deletedAt,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(deletedAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -74,7 +81,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
position: {
|
||||
...position,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(position.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -82,7 +89,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
searchVector: {
|
||||
...searchVector,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(searchVector.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -91,7 +98,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
updatedAt: {
|
||||
...updatedAt,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(updatedAt.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -99,7 +106,7 @@ const buildObjectSystemFlatFieldMetadatas = ({
|
||||
},
|
||||
updatedBy: {
|
||||
...updatedBy,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: computeFieldUniversalIdentifier(updatedBy.name),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
@@ -125,7 +132,11 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.TEXT,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
universalIdentifier: getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: objectMetadataUniversalIdentifier,
|
||||
name: 'name',
|
||||
}),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
icon: 'IconAbc',
|
||||
|
||||
+19
@@ -1,3 +1,4 @@
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import {
|
||||
STANDARD_OBJECTS,
|
||||
DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS,
|
||||
@@ -117,6 +118,22 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
const morphId =
|
||||
morphIdByRelationObjectNameSingular[objectMetadataNameSingular];
|
||||
|
||||
// Default relation fields get deterministic universal identifiers so
|
||||
// that provisioning the same custom object always yields the same
|
||||
// identifiers (matching the SDK manifest derivation).
|
||||
const sourceFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
objectUniversalIdentifier:
|
||||
sourceFlatObjectMetadata.universalIdentifier,
|
||||
name: targetFlatObjectMetadata.namePlural,
|
||||
});
|
||||
const targetFieldUniversalIdentifier = getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier: flatApplication.universalIdentifier,
|
||||
objectUniversalIdentifier:
|
||||
targetFlatObjectMetadata.universalIdentifier,
|
||||
name: fieldName,
|
||||
});
|
||||
|
||||
const { flatFieldMetadatas, indexMetadatas } =
|
||||
generateMorphOrRelationFlatFieldMetadataPair({
|
||||
sourceFlatObjectMetadata,
|
||||
@@ -128,6 +145,8 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
sourceFlatObjectMetadataJoinColumnName: joinColumnName,
|
||||
morphId,
|
||||
targetFieldName: fieldName,
|
||||
sourceFieldUniversalIdentifier,
|
||||
targetFieldUniversalIdentifier,
|
||||
createFieldInput: {
|
||||
icon: standardFieldProperties.icon,
|
||||
type: FieldMetadataType.RELATION,
|
||||
|
||||
+420
-420
File diff suppressed because it is too large
Load Diff
+1
@@ -3,6 +3,7 @@ import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-
|
||||
let uuidCounter = 0;
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(
|
||||
() => `00000000-0000-0000-0000-${String(++uuidCounter).padStart(12, '0')}`,
|
||||
),
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { v4 } from 'uuid';
|
||||
import { getStandardPageLayoutMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-page-layout-metadata-related-entity-ids.util';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-impo
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
...jest.requireActual('uuid'),
|
||||
v4: jest.fn(() => 'mocked-uuid'),
|
||||
}));
|
||||
|
||||
|
||||
+49
@@ -174,6 +174,43 @@ exports[`Sync application should fail due to object system fields integrity when
|
||||
}
|
||||
`;
|
||||
|
||||
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": {
|
||||
@@ -188,6 +225,12 @@ exports[`Sync application should fail due to object system fields integrity when
|
||||
"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",
|
||||
@@ -303,6 +346,12 @@ exports[`Sync application should fail due to object system fields integrity when
|
||||
"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",
|
||||
|
||||
+2
@@ -42,6 +42,7 @@ describe('Manifest sync - dry run', () => {
|
||||
|
||||
it('returns the planned actions without applying them', async () => {
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'dryRunTicket',
|
||||
namePlural: 'dryRunTickets',
|
||||
labelSingular: 'Dry Run Ticket',
|
||||
@@ -55,6 +56,7 @@ describe('Manifest sync - dry run', () => {
|
||||
});
|
||||
|
||||
const invoiceObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'dryRunInvoice',
|
||||
namePlural: 'dryRunInvoices',
|
||||
labelSingular: 'Dry Run Invoice',
|
||||
|
||||
+3
-1
@@ -47,7 +47,9 @@ describe('Sync application should fail when creating permissions on a standard r
|
||||
label: 'Stolen Admin Role',
|
||||
description:
|
||||
'Attempts to add permissions to the standard admin role',
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKSPACE],
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.WORKSPACE,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+6
-2
@@ -44,7 +44,9 @@ describe('Sync application should fail when retargeting existing permissions to
|
||||
universalIdentifier: TEST_ROLE_ID,
|
||||
label: 'App Default Role',
|
||||
description: 'Default role for the test app',
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKSPACE],
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.WORKSPACE,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -69,7 +71,9 @@ describe('Sync application should fail when retargeting existing permissions to
|
||||
universalIdentifier: STANDARD_ROLE.admin.universalIdentifier,
|
||||
label: 'Admin',
|
||||
description: 'Attempts to retarget permission flag to admin role',
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.WORKSPACE],
|
||||
permissionFlagUniversalIdentifiers: [
|
||||
SystemPermissionFlag.WORKSPACE,
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+3
@@ -32,6 +32,7 @@ describe('Sync application should surface a human error on flat-entity map confl
|
||||
|
||||
it('should fail with an installation error naming the object when two objects share a universalIdentifier', async () => {
|
||||
const firstObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
@@ -40,6 +41,7 @@ describe('Sync application should surface a human error on flat-entity map confl
|
||||
});
|
||||
|
||||
const conflictingObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'invoiceDuplicate',
|
||||
namePlural: 'invoiceDuplicates',
|
||||
labelSingular: 'Invoice Duplicate',
|
||||
@@ -65,6 +67,7 @@ describe('Sync application should surface a human error on flat-entity map confl
|
||||
|
||||
it('should fail with an installation error naming the field when two fields of an object share a universalIdentifier', async () => {
|
||||
const objectWithConflictingFields = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
|
||||
+65
-8
@@ -10,10 +10,16 @@ import {
|
||||
eachTestingContextFilter,
|
||||
} from 'twenty-shared/testing';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
|
||||
|
||||
const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
// 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;
|
||||
@@ -43,8 +49,8 @@ const buildObjectWithLabelField = ({
|
||||
description: string;
|
||||
additionalFields?: ObjectManifest['fields'];
|
||||
}): Pick<Manifest, 'objects' | 'fields'> => {
|
||||
const objectId = uuidv4();
|
||||
const labelFieldId = uuidv4();
|
||||
const objectId = computeDeterministicTestUuid(nameSingular);
|
||||
const labelFieldId = computeDeterministicTestUuid(`${nameSingular}-title`);
|
||||
|
||||
return {
|
||||
objects: [
|
||||
@@ -90,6 +96,8 @@ const buildDefaultObjectWithModifiedSearchVector = ({
|
||||
searchVectorOverrides: Partial<ObjectManifest['fields'][number]>;
|
||||
}): Pick<Manifest, 'objects' | 'fields'> => {
|
||||
const defaultObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
universalIdentifier: computeDeterministicTestUuid(nameSingular),
|
||||
nameSingular,
|
||||
namePlural,
|
||||
labelSingular,
|
||||
@@ -144,7 +152,8 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
description: 'Object with wrong id field type',
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier:
|
||||
computeDeterministicTestUuid('wrongIdTypeObject-id'),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
@@ -183,7 +192,9 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
description: 'Object with wrong position field type',
|
||||
additionalFields: [
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeDeterministicTestUuid(
|
||||
'wrongPositionObject-position',
|
||||
),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
@@ -231,16 +242,60 @@ const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
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 = uuidv4();
|
||||
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',
|
||||
@@ -300,6 +355,7 @@ describe('Sync application should fail due to object system fields integrity', (
|
||||
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',
|
||||
@@ -350,6 +406,7 @@ describe('Sync application should fail due to object system fields integrity', (
|
||||
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',
|
||||
|
||||
+1
@@ -47,6 +47,7 @@ const buildCollidingManifest = (): Manifest => {
|
||||
const uniqueFieldUniversalIdentifier = uuidv4();
|
||||
|
||||
const object = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: 'collidingIndexObjects',
|
||||
labelSingular: 'Colliding Index Object',
|
||||
|
||||
+4
-5
@@ -85,8 +85,7 @@ const FAILING_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath is a folder path without extension (bare UUID)',
|
||||
title: 'when filePath is a folder path without extension (bare UUID)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'BuiltFrontComponent',
|
||||
@@ -94,8 +93,7 @@ const FAILING_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'when filePath is a nested folder path without extension',
|
||||
title: 'when filePath is a nested folder path without extension',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'Source',
|
||||
@@ -121,7 +119,8 @@ const FAILING_TEST_CASES: EachTestingContext<TestContext>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when filePath has an invalid extension for Source (.js instead of .ts)',
|
||||
title:
|
||||
'when filePath has an invalid extension for Source (.js instead of .ts)',
|
||||
context: {
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
fileFolder: 'Source',
|
||||
|
||||
+1
-4
@@ -64,10 +64,7 @@ const buildManifest = ({
|
||||
});
|
||||
};
|
||||
|
||||
const timeSync = async (
|
||||
label: string,
|
||||
manifest: Manifest,
|
||||
): Promise<number> => {
|
||||
const timeSync = async (label: string, manifest: Manifest): Promise<number> => {
|
||||
const start = performance.now();
|
||||
|
||||
await syncApplication({ manifest, expectToFail: false });
|
||||
|
||||
+4
-4
@@ -249,10 +249,10 @@ describe('Marketplace Catalog Sync (integration)', () => {
|
||||
await fs.writeFile(join(sourceDir, 'manifest.json'), manifest);
|
||||
await fs.writeFile(join(sourceDir, 'package.json'), packageJson);
|
||||
|
||||
await tar.create(
|
||||
{ file: tarballPath, gzip: true, cwd: sourceDir },
|
||||
['manifest.json', 'package.json'],
|
||||
);
|
||||
await tar.create({ file: tarballPath, gzip: true, cwd: sourceDir }, [
|
||||
'manifest.json',
|
||||
'package.json',
|
||||
]);
|
||||
|
||||
const tarballBuffer = await fs.readFile(tarballPath);
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ const TEST_SECOND_FIELD_ID = uuidv4();
|
||||
const TEST_NUMBER_FIELD_ID = uuidv4();
|
||||
|
||||
const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
|
||||
+5
@@ -48,6 +48,7 @@ describe('Manifest update - objects', () => {
|
||||
|
||||
it('should create a new object when added to manifest on second sync', async () => {
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
@@ -75,6 +76,7 @@ describe('Manifest update - objects', () => {
|
||||
});
|
||||
|
||||
const invoiceObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
@@ -113,6 +115,7 @@ describe('Manifest update - objects', () => {
|
||||
it('should update object properties when changed in manifest on second sync', async () => {
|
||||
const universalIdentifier = uuidv4();
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
@@ -173,6 +176,7 @@ describe('Manifest update - objects', () => {
|
||||
|
||||
it('should delete an object when removed from manifest on second sync', async () => {
|
||||
const ticketObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
@@ -182,6 +186,7 @@ describe('Manifest update - objects', () => {
|
||||
});
|
||||
|
||||
const invoiceObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'invoice',
|
||||
namePlural: 'invoices',
|
||||
labelSingular: 'Invoice',
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ const INITIAL_ANY_FIELD_FILTER_VALUE = 'arm';
|
||||
const UPDATED_ANY_FIELD_FILTER_VALUE = 'tail';
|
||||
|
||||
const HUMAN_OBJECT = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'human',
|
||||
namePlural: 'humans',
|
||||
labelSingular: 'Human',
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ const TEST_APP_ID = uuidv4();
|
||||
const TEST_ROLE_ID = uuidv4();
|
||||
|
||||
const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'uniqueRenameObject',
|
||||
namePlural: 'uniqueRenameObjects',
|
||||
labelSingular: 'Unique Rename Object',
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ const TEST_ROLE_ID = uuidv4();
|
||||
const REUSABLE_UID = uuidv4();
|
||||
|
||||
const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
universalIdentifier: REUSABLE_UID,
|
||||
nameSingular: 'ephemeral',
|
||||
namePlural: 'ephemerals',
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ const TEST_FIELD_ID = uuidv4();
|
||||
const TEST_SKILL_ID = uuidv4();
|
||||
|
||||
const TEST_OBJECT = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
@@ -172,6 +173,7 @@ describe('syncApplication', () => {
|
||||
const updatedFieldId = uuidv4();
|
||||
|
||||
const testObject = buildDefaultObjectManifest({
|
||||
applicationUniversalIdentifier: TEST_APP_ID,
|
||||
nameSingular: 'ticket',
|
||||
namePlural: 'tickets',
|
||||
labelSingular: 'Ticket',
|
||||
|
||||
+28
-10
@@ -1,7 +1,12 @@
|
||||
import { type ObjectManifest } from 'twenty-shared/application';
|
||||
import {
|
||||
getFieldUniversalIdentifier,
|
||||
type ObjectManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// System field universal identifiers are server-owned and validated against
|
||||
// the deterministic derivation, so the manifest must carry the derived values.
|
||||
export const buildDefaultObjectManifest = ({
|
||||
nameSingular,
|
||||
namePlural,
|
||||
@@ -12,6 +17,7 @@ export const buildDefaultObjectManifest = ({
|
||||
additionalFields = [],
|
||||
universalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
@@ -22,11 +28,22 @@ export const buildDefaultObjectManifest = ({
|
||||
additionalFields?: ObjectManifest['fields'];
|
||||
universalIdentifier?: string;
|
||||
labelIdentifierFieldMetadataUniversalIdentifier?: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): ObjectManifest => {
|
||||
const idFieldUniversalIdentifier = uuidv4();
|
||||
const objectUniversalIdentifier = universalIdentifier ?? uuidv4();
|
||||
|
||||
const computeSystemFieldUniversalIdentifier = (fieldName: string) =>
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: fieldName,
|
||||
});
|
||||
|
||||
const idFieldUniversalIdentifier =
|
||||
computeSystemFieldUniversalIdentifier('id');
|
||||
|
||||
return {
|
||||
universalIdentifier: universalIdentifier ?? uuidv4(),
|
||||
universalIdentifier: objectUniversalIdentifier,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
labelIdentifierFieldMetadataUniversalIdentifier ??
|
||||
idFieldUniversalIdentifier,
|
||||
@@ -44,43 +61,44 @@ export const buildDefaultObjectManifest = ({
|
||||
label: 'Id',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeSystemFieldUniversalIdentifier('createdAt'),
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeSystemFieldUniversalIdentifier('updatedAt'),
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeSystemFieldUniversalIdentifier('deletedAt'),
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeSystemFieldUniversalIdentifier('createdBy'),
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeSystemFieldUniversalIdentifier('updatedBy'),
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier: computeSystemFieldUniversalIdentifier('position'),
|
||||
type: FieldMetadataType.POSITION,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
},
|
||||
{
|
||||
universalIdentifier: uuidv4(),
|
||||
universalIdentifier:
|
||||
computeSystemFieldUniversalIdentifier('searchVector'),
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
|
||||
import { findManyFieldsMetadata } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
|
||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const SYSTEM_FIELD_NAMES = [
|
||||
'id',
|
||||
'name',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
'searchVector',
|
||||
];
|
||||
|
||||
const DEFAULT_RELATION_FORWARD_FIELD_NAMES = [
|
||||
'timelineActivities',
|
||||
'attachments',
|
||||
'noteTargets',
|
||||
'taskTargets',
|
||||
];
|
||||
|
||||
const OBJECT_NAME_SINGULAR = 'rocketForUniversalIdentifier';
|
||||
|
||||
type FetchedField = {
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
relation: {
|
||||
targetFieldMetadata: {
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
targetObjectMetadata: {
|
||||
id: string;
|
||||
nameSingular: string;
|
||||
universalIdentifier: string;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
describe('Custom object creation deterministic field universal identifiers', () => {
|
||||
let createdObjectMetadataId: string;
|
||||
let objectUniversalIdentifier: string;
|
||||
let applicationUniversalIdentifier: string;
|
||||
let fetchedFields: FetchedField[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data: createObjectData } = await createOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
nameSingular: OBJECT_NAME_SINGULAR,
|
||||
namePlural: `${OBJECT_NAME_SINGULAR}s`,
|
||||
labelSingular: 'Rocket For Universal Identifier',
|
||||
labelPlural: 'Rockets For Universal Identifier',
|
||||
icon: 'IconRocket',
|
||||
},
|
||||
gqlFields: 'id universalIdentifier applicationId',
|
||||
});
|
||||
|
||||
createdObjectMetadataId = createObjectData.createOneObject.id;
|
||||
objectUniversalIdentifier =
|
||||
createObjectData.createOneObject.universalIdentifier;
|
||||
|
||||
const { data: findManyApplicationsData } = await findManyApplications({
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
const workspaceCustomApplication =
|
||||
findManyApplicationsData.findManyApplications.find(
|
||||
(application) =>
|
||||
application.id === createObjectData.createOneObject.applicationId,
|
||||
);
|
||||
|
||||
if (!isDefined(workspaceCustomApplication)) {
|
||||
throw new Error(
|
||||
'Could not resolve the application of the created custom object',
|
||||
);
|
||||
}
|
||||
|
||||
applicationUniversalIdentifier =
|
||||
workspaceCustomApplication.universalIdentifier;
|
||||
|
||||
const { fields } = await findManyFieldsMetadata({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: { objectMetadataId: { eq: createdObjectMetadataId } },
|
||||
paging: { first: 100 },
|
||||
},
|
||||
gqlFields: `
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
relation {
|
||||
targetFieldMetadata { id name universalIdentifier }
|
||||
targetObjectMetadata { id nameSingular universalIdentifier }
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
fetchedFields = fields.map((edge: { node: FetchedField }) => edge.node);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteOneObjectMetadata({
|
||||
expectToFail: false,
|
||||
input: { idToDelete: createdObjectMetadataId },
|
||||
});
|
||||
});
|
||||
|
||||
it.each(SYSTEM_FIELD_NAMES)(
|
||||
'should derive the %s system field universal identifier deterministically',
|
||||
(systemFieldName) => {
|
||||
const systemField = fetchedFields.find(
|
||||
(field) => field.name === systemFieldName,
|
||||
);
|
||||
|
||||
expect(systemField).toBeDefined();
|
||||
expect(systemField?.universalIdentifier).toBe(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: systemFieldName,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(DEFAULT_RELATION_FORWARD_FIELD_NAMES)(
|
||||
'should derive the %s default relation field universal identifiers deterministically',
|
||||
async (forwardFieldName) => {
|
||||
const forwardField = fetchedFields.find(
|
||||
(field) => field.name === forwardFieldName,
|
||||
);
|
||||
|
||||
expect(forwardField).toBeDefined();
|
||||
expect(forwardField?.universalIdentifier).toBe(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier,
|
||||
name: forwardFieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
const standardRelationObject =
|
||||
forwardField?.relation?.targetObjectMetadata;
|
||||
|
||||
expect(standardRelationObject).toBeDefined();
|
||||
|
||||
if (!isDefined(standardRelationObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fields: standardObjectFieldEdges } = await findManyFieldsMetadata(
|
||||
{
|
||||
expectToFail: false,
|
||||
input: {
|
||||
filter: { objectMetadataId: { eq: standardRelationObject.id } },
|
||||
paging: { first: 1000 },
|
||||
},
|
||||
gqlFields: 'id name universalIdentifier',
|
||||
},
|
||||
);
|
||||
|
||||
const storedReverseFieldName = `target${capitalize(OBJECT_NAME_SINGULAR)}`;
|
||||
const reverseField = standardObjectFieldEdges
|
||||
.map((edge: { node: FetchedField }) => edge.node)
|
||||
.find((field: FetchedField) => field.name === storedReverseFieldName);
|
||||
|
||||
expect(reverseField).toBeDefined();
|
||||
expect(reverseField?.universalIdentifier).toBe(
|
||||
getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier,
|
||||
objectUniversalIdentifier: standardRelationObject.universalIdentifier,
|
||||
name: storedReverseFieldName,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
+3212
File diff suppressed because it is too large
Load Diff
+10
@@ -0,0 +1,10 @@
|
||||
import { STANDARD_OBJECTS } from '@/metadata/constants/standard-object.constant';
|
||||
|
||||
// If this test fails, a universal identifier changed: make sure the
|
||||
// change is intentional and ships with a coordinated backfill of existing
|
||||
// workspaces before updating the snapshot.
|
||||
describe('STANDARD_OBJECTS universal identifiers', () => {
|
||||
it('should never change without an explicit snapshot update', () => {
|
||||
expect(STANDARD_OBJECTS).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
+38
@@ -0,0 +1,38 @@
|
||||
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from '@/application/constants/TwentyStandardApplicationUniversalIdentifier';
|
||||
import { getFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-field-universal-identifier.util';
|
||||
|
||||
export const STANDARD_OBJECT_SYSTEM_FIELD_NAMES = [
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'position',
|
||||
'searchVector',
|
||||
] as const;
|
||||
|
||||
type StandardObjectSystemFieldName =
|
||||
(typeof STANDARD_OBJECT_SYSTEM_FIELD_NAMES)[number];
|
||||
|
||||
// System field universal identifiers are deterministically derived from the
|
||||
// standard application universal identifier, the object universal identifier
|
||||
// and the field name. Renaming a system field therefore changes its
|
||||
// universal identifier and must never happen without a coordinated backfill
|
||||
// of existing workspaces.
|
||||
export const buildStandardObjectSystemFields = (
|
||||
objectUniversalIdentifier: string,
|
||||
): Record<StandardObjectSystemFieldName, { universalIdentifier: string }> =>
|
||||
Object.fromEntries(
|
||||
STANDARD_OBJECT_SYSTEM_FIELD_NAMES.map((fieldName) => [
|
||||
fieldName,
|
||||
{
|
||||
universalIdentifier: getFieldUniversalIdentifier({
|
||||
applicationUniversalIdentifier:
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
objectUniversalIdentifier,
|
||||
name: fieldName,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
) as Record<StandardObjectSystemFieldName, { universalIdentifier: string }>;
|
||||
Reference in New Issue
Block a user