From 47b60bd49f65f4b5dc15106b6540ede39edc9846 Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:30:18 +0200 Subject: [PATCH] Deprecate FieldMetadataInterface (#13264) # Introduction From the moment replaced the FieldMetadataInterface definition to: ```ts import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; export type FieldMetadataInterface< T extends FieldMetadataType = FieldMetadataType, > = FieldMetadataEntity; ``` After this PR merge will create a new one removing the type and replacing it to `FieldMetadataEntity`. Did not renamed it here to avoid conflicts on naming + type issues fixs within the same PR ## Field metadata entity RELATION or MORPH Relations fields cannot be null for those field metadata entity instance anymore, but are never for the others see `packages/twenty-server/src/engine/metadata-modules/field-metadata/types/field-metadata-entity-test.type.ts` ( introduced TypeScript tests ) ## Concerns - TS_VECTOR is the most at risk with the `generatedType` and `asExpression` removal from interface ## What's next - `FielMetadataInterface` removal and rename ( see introduction ) - Depcrecating `ObjectMetadataInterface` - Refactor `FieldMetadataEntity` optional fiels to be nullable only - TO DIG `never` occurences on settings, defaultValue etc - Some interfaces will be replaced by the `FlatFieldMetadata` when deprecating the current sync and comparators tools --- ...standard-select-fields-position.command.ts | 2 +- ...enqueued-status-to-workflow-run.command.ts | 4 +- .../__mocks__/object-metadata-item.mock.ts | 350 ++++++++++++----- .../__mocks__/mockPersonObjectMetadata.ts | 48 +-- .../factories/enum-type-definition.factory.ts | 11 +- ...n-connect-input-type-definition.factory.ts | 2 +- .../utils/generate-fields.utils.ts | 4 +- .../core/interfaces/rest-api-base.handler.ts | 3 +- .../__tests__/get-field-type.utils.spec.ts | 13 +- ...ld-metadata-to-graphql-query.utils.spec.ts | 70 ++-- .../mocks/opportunity-field-maps.mock.ts | 254 +++++++------ .../check-filter-enum-values.spec.ts | 13 +- .../__tests__/parse-filter.utils.spec.ts | 52 +-- .../__tests__/filter-input.factory.spec.ts | 21 +- .../compute-cursor-arg-filter.utils.spec.ts | 29 +- .../mockObjectMetadataItemsWithFieldMaps.ts | 60 +-- .../utils/__tests__/components.utils.spec.ts | 78 +++- .../utils/generate-random-field-value.util.ts | 12 +- .../engine/dataloaders/dataloader.service.ts | 66 ++-- .../field-metadata/dtos/field-metadata.dto.ts | 3 +- .../field-metadata/field-metadata.entity.ts | 54 ++- .../field-metadata/field-metadata.resolver.ts | 19 +- .../before-update-one-field.hook.spec.ts | 76 +++- .../hooks/before-update-one-field.hook.ts | 6 +- .../interfaces/field-metadata.interface.ts | 38 +- .../field-metadata-related-records.service.ts | 27 +- .../field-metadata-relation.service.ts | 1 + .../services/field-metadata.service.ts | 5 +- .../types/field-metadata-entity-test.type.ts | 90 +++++ ...fault-value-for-non-nullable-field.util.ts | 2 +- ...tadata-entity-to-fieldMetadata-dto.util.ts | 31 ++ ...ect-or-multi-select-field-metadata.util.ts | 3 +- .../object-metadata.service.ts | 14 +- .../field-permissions.service.spec.ts | 16 +- .../search-vector/search-vector.service.ts | 27 +- .../composite-column-action.factory.ts | 2 +- .../ts-vector-column-action.factory.ts | 13 +- .../workspace-migration.factory.ts | 15 +- .../workspace-entity-manager.spec.ts | 3 +- .../factories/entity-schema-column.factory.ts | 4 +- .../workspace-delete-query-builder.ts | 2 +- ...ject-metadata-to-schema-properties.util.ts | 10 +- .../services/field-metadata-health.service.ts | 27 +- ...space-migration-index.factory.util.spec.ts | 44 ++- .../__tests__/get-flat-field-metadata.mock.ts | 14 +- .../types/flat-field-metadata.ts | 4 +- .../workspace-migration-builder.spec.ts.snap | 354 +++++++++++++++--- ...space-migration-builder-field-test-case.ts | 31 +- .../workspace-field-relation.comparator.ts | 10 +- .../factories/standard-field.factory.ts | 14 +- .../partial-field-metadata.interface.ts | 14 +- .../utils/compute-standard-fields.util.ts | 6 + .../timeline-activity.repository.ts | 70 ++-- .../services/timeline-activity.service.ts | 33 +- .../generate-fake-form-response.spec.ts | 106 ++++-- .../should-generate-field-fake-value.spec.ts | 53 ++- .../utils/generate-object-record-fields.ts | 4 +- .../utils/should-generate-field-fake-value.ts | 11 +- .../get-field-metadata-entity.mock.ts | 47 +++ ...ne-field-metadata-enum.integration-spec.ts | 2 +- ...ne-enum-field-metadata.integration-spec.ts | 11 +- .../utils/create-one-field-metadata.util.ts | 4 +- ...metadata-with-relation.integration-spec.ts | 44 +-- .../create-relation-between-objects.util.ts | 28 +- .../src/testing/EachTestingContextFilter.ts | 1 + packages/twenty-shared/src/testing/index.ts | 5 + .../src/testing/types/TestingGenerics.type.ts | 59 +++ 67 files changed, 1780 insertions(+), 769 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/types/field-metadata-entity-test.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util.ts create mode 100644 packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts create mode 100644 packages/twenty-shared/src/testing/types/TestingGenerics.type.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command.ts index 770e10ff0..80b63fd79 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-54/0-54-fix-standard-select-fields-position.command.ts @@ -71,7 +71,7 @@ export class FixStandardSelectFieldsPositionCommand extends ActiveOrSuspendedWor let biggestPosition = -1; // Sort options by position for consistent processing - const sortedOptions = [...taskStatusFieldMetadata.options].sort( + const sortedOptions = (taskStatusFieldMetadata.options ?? []).sort( (a, b) => a.position - b.position, ); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command.ts index 9c9d2ec9f..b59229566 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-1/1-1-add-enqueued-status-to-workflow-run.command.ts @@ -58,7 +58,7 @@ export class AddEnqueuedStatusToWorkflowRunCommand extends ActiveOrSuspendedWork // check if enqueued status is already in the field metadata options if ( - workflowRunStatusFieldMetadataOptions.some( + workflowRunStatusFieldMetadataOptions?.some( (option) => option.value === WorkflowRunStatus.ENQUEUED, ) ) { @@ -72,7 +72,7 @@ export class AddEnqueuedStatusToWorkflowRunCommand extends ActiveOrSuspendedWork `Would add enqueued status to workflow run status field metadata for workspace ${workspaceId}`, ); } else { - workflowRunStatusFieldMetadataOptions.push({ + workflowRunStatusFieldMetadataOptions?.push({ value: WorkflowRunStatus.ENQUEUED, label: 'Enqueued', position: 4, diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts index 47229427c..4fe261537 100644 --- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts +++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts @@ -1,10 +1,15 @@ import { FieldMetadataType } from 'twenty-shared/types'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; export const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; export const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency'; @@ -13,194 +18,292 @@ export const FIELD_ACTOR_MOCK_NAME = 'fieldActor'; export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName'; export const FIELD_PHONES_MOCK_NAME = 'fieldPhones'; -export const fieldNumberMock = { +const workspaceId = '20202020-0000-0000-0000-000000000000'; +const objectMetadataId = '20202020-0000-0000-0000-000000000001'; + +export const fieldNumberMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldNumberId', name: 'fieldNumber', type: FieldMetadataType.NUMBER, + label: 'Field Number', isNullable: false, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -export const fieldTextMock = { +export const fieldTextMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldTextId', name: 'fieldText', type: FieldMetadataType.TEXT, + label: 'Field Text', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -export const fieldCurrencyMock = { +export const fieldCurrencyMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldCurrencyId', name: FIELD_CURRENCY_MOCK_NAME, type: FieldMetadataType.CURRENCY, + label: 'Field Currency', isNullable: true, defaultValue: { amountMicros: null, currencyCode: "''" }, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -export const fieldSelectMock = { +export const fieldSelectMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldSelectId', name: 'fieldSelect', type: FieldMetadataType.SELECT, + label: 'Field Select', isNullable: true, defaultValue: 'OPTION_1', options: [ { id: '9a519a86-422b-4598-88ae-78751353f683', - color: 'red', label: 'Opt 1', value: 'OPTION_1', position: 0, + color: 'red', }, { id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', - color: 'purple', label: 'Opt 2', value: 'OPTION_2', position: 1, + color: 'purple', }, ], -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldMultiSelectMock = { +export const fieldMultiSelectMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldMultiSelectId', name: 'fieldMultiSelect', type: FieldMetadataType.MULTI_SELECT, + label: 'Field Multi Select', isNullable: true, - defaultValue: "{'OPTION_1'}", + defaultValue: ['OPTION_1'], options: [ { id: '9a519a86-422b-4598-88ae-78751353f683', - color: 'red', label: 'Opt 1', value: 'OPTION_1', position: 0, + color: 'red', }, { id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', - color: 'purple', label: 'Opt 2', value: 'OPTION_2', position: 1, + color: 'purple', }, ], -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -export const fieldRelationMock = { +export const fieldRelationMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldRelationId', name: 'fieldRelation', type: FieldMetadataType.RELATION, + label: 'Field Relation', + isNullable: true, + defaultValue: null, settings: { relationType: RelationType.MANY_TO_ONE, joinColumnName: 'fieldRelationId', - onDelete: 'CASCADE', + onDelete: RelationOnDeleteAction.CASCADE, }, relationTargetObjectMetadata: { id: 'relationTargetObjectId', nameSingular: 'relationTargetObject', namePlural: 'relationTargetObjects', - }, + } as ObjectMetadataEntity, relationTargetFieldMetadata: { id: 'relationTargetFieldId', name: 'relationTargetField', - }, - isNullable: true, - defaultValue: null, -}; + } as FieldMetadataEntity, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldLinksMock = { +export const fieldLinksMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldLinksId', name: FIELD_LINKS_MOCK_NAME, type: FieldMetadataType.LINKS, + label: 'Field Links', isNullable: false, - defaultValue: [ - { primaryLinkLabel: '', primaryLinkUrl: '', secondaryLinks: [] }, - ], -}; + defaultValue: { + primaryLinkLabel: '', + primaryLinkUrl: '', + secondaryLinks: [], + }, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldUuidMock = { +export const fieldUuidMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldUuidId', name: 'fieldUuid', type: FieldMetadataType.UUID, + label: 'Field UUID', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldDateTimeMock = { +export const fieldDateTimeMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldDateTimeId', name: 'fieldDateTime', type: FieldMetadataType.DATE_TIME, + label: 'Field Date Time', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldDateMock = { +export const fieldDateMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldDateId', name: 'fieldDate', type: FieldMetadataType.DATE, + label: 'Field Date', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldBooleanMock = { +export const fieldBooleanMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldBooleanId', name: 'fieldBoolean', type: FieldMetadataType.BOOLEAN, + label: 'Field Boolean', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldNumericMock = { +export const fieldNumericMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldNumericId', name: 'fieldNumeric', type: FieldMetadataType.NUMERIC, + label: 'Field Numeric', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldFullNameMock = { +export const fieldFullNameMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldFullNameId', name: FIELD_FULL_NAME_MOCK_NAME, type: FieldMetadataType.FULL_NAME, + label: 'Field Full Name', isNullable: true, defaultValue: { firstName: '', lastName: '' }, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldRatingMock = { +export const fieldRatingMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldRatingId', name: 'fieldRating', type: FieldMetadataType.RATING, + label: 'Field Rating', isNullable: true, defaultValue: 'RATING_1', options: [ { id: '9a519a86-422b-4598-88ae-78751353f683', - color: 'red', label: 'Opt 1', value: 'RATING_1', position: 0, + color: 'red', }, { id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', - color: 'purple', label: 'Opt 2', value: 'RATING_2', position: 1, + color: 'purple', }, - ], -}; + ] as FieldMetadataComplexOption[], + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldPositionMock = { +export const fieldPositionMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldPositionId', name: 'fieldPosition', type: FieldMetadataType.POSITION, + label: 'Field Position', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldAddressMock = { +export const fieldAddressMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldAddressId', name: FIELD_ADDRESS_MOCK_NAME, type: FieldMetadataType.ADDRESS, + label: 'Field Address', isNullable: true, defaultValue: { addressStreet1: '', @@ -212,65 +315,105 @@ const fieldAddressMock = { addressLat: null, addressLng: null, }, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldRawJsonMock = { +export const fieldRawJsonMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldRawJsonId', name: 'fieldRawJson', type: FieldMetadataType.RAW_JSON, + label: 'Field Raw JSON', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldRichTextMock = { +export const fieldRichTextMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldRichTextId', name: 'fieldRichText', type: FieldMetadataType.RICH_TEXT, + label: 'Field Rich Text', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldActorMock = { +export const fieldActorMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldActorId', name: FIELD_ACTOR_MOCK_NAME, type: FieldMetadataType.ACTOR, + label: 'Field Actor', isNullable: true, defaultValue: { source: FieldActorSource.MANUAL, name: '', }, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldEmailsMock = { +export const fieldEmailsMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldEmailsId', name: 'fieldEmails', type: FieldMetadataType.EMAILS, + label: 'Field Emails', isNullable: false, - defaultValue: [{ primaryEmail: '', additionalEmails: {} }], -}; + defaultValue: { + primaryEmail: '', + additionalEmails: {}, + }, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldArrayMock = { +export const fieldArrayMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldArrayId', name: 'fieldArray', type: FieldMetadataType.ARRAY, + label: 'Field Array', isNullable: true, defaultValue: null, -}; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); -const fieldPhonesMock = { +export const fieldPhonesMock = getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId, id: 'fieldPhonesId', name: FIELD_PHONES_MOCK_NAME, type: FieldMetadataType.PHONES, + label: 'Field Phones', isNullable: false, - defaultValue: [ - { - primaryPhoneNumber: '', - primaryPhoneCountryCode: '', - primaryPhoneCallingCode: '', - additionalPhones: {}, - }, - ], -}; + defaultValue: { + primaryPhoneNumber: '', + primaryPhoneCountryCode: '', + primaryPhoneCallingCode: '', + additionalPhones: {}, + }, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), +}); export const fields = [ fieldUuidMock, @@ -297,46 +440,47 @@ export const fields = [ fieldArrayMock, ]; -export const objectMetadataItemMock = { - targetTableName: 'testingObject', - id: 'mockObjectId', +export const objectMetadataItemMock: ObjectMetadataEntity = { + id: objectMetadataId, + workspaceId, nameSingular: 'objectName', - namePlural: 'objectsName', + namePlural: 'objectNames', + labelSingular: 'Object Name', + labelPlural: 'Object Names', + description: 'Object description', + icon: 'Icon123', + targetTableName: 'DEPRECATED', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + isAuditLogged: true, + isSearchable: true, fields, + createdAt: new Date(), + updatedAt: new Date(), } as ObjectMetadataEntity; -export const objectMetadataMapItemMock = { - id: 'mockObjectId', - icon: 'Icon123', - nameSingular: 'objectName', - namePlural: 'objectsName', - fieldsById: fields.reduce((acc, field) => { - // @ts-expect-error legacy noImplicitAny - acc[field.id] = field; - - return acc; - }, {}), - fieldIdByName: fields.reduce((acc, field) => { - // @ts-expect-error legacy noImplicitAny - acc[field.name] = field; - - return acc; - }, {}), +export const objectMetadataMapItemMock: ObjectMetadataItemWithFieldMaps = { + ...objectMetadataItemMock, + fieldsById: fields.reduce( + (acc, field) => ({ + ...acc, + [field.id]: field, + }), + {}, + ), + fieldIdByName: fields.reduce( + (acc, field) => ({ + ...acc, + [field.name]: field.id, + }), + {}, + ), fieldIdByJoinColumnName: {}, - labelSingular: 'Object', - labelPlural: 'Objects', - workspaceId: 'mockWorkspaceId', - isCustom: false, - isSystem: false, - targetTableName: '', indexMetadatas: [], - isActive: true, - isRemote: false, - isAuditLogged: false, - isSearchable: false, -} satisfies ObjectMetadataItemWithFieldMaps; - -export const objectMetadataMapsMock = { +}; +export const objectMetadataMapsMock: ObjectMetadataMaps = { byId: { [objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItemMock, }, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts index 8501a583f..6fc1faf0f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/__mocks__/mockPersonObjectMetadata.ts @@ -1,7 +1,11 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; + +const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; export const mockPersonObjectMetadataWithFieldMaps = ( duplicateCriteria: WorkspaceEntityDuplicateCriteria[], @@ -24,7 +28,7 @@ export const mockPersonObjectMetadataWithFieldMaps = ( duplicateCriteria: duplicateCriteria, labelIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '', - workspaceId: '', + workspaceId, indexMetadatas: [], fieldIdByName: { name: 'name-id', @@ -34,9 +38,10 @@ export const mockPersonObjectMetadataWithFieldMaps = ( }, fieldIdByJoinColumnName: {}, fieldsById: { - 'name-id': { - id: '', + 'name-id': getMockFieldMetadataEntity({ + workspaceId, objectMetadataId: '', + id: 'name-id', type: FieldMetadataType.FULL_NAME, name: 'name', label: 'Name', @@ -44,18 +49,18 @@ export const mockPersonObjectMetadataWithFieldMaps = ( lastName: "''", firstName: "''", }, - description: 'Contact’s name', + description: "Contact's name", isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, - 'emails-id': { - id: '', + }) as FieldMetadataEntity, + 'emails-id': getMockFieldMetadataEntity({ + workspaceId, objectMetadataId: '', + id: 'emails-id', type: FieldMetadataType.EMAILS, name: 'emails', label: 'Emails', @@ -63,49 +68,48 @@ export const mockPersonObjectMetadataWithFieldMaps = ( primaryEmail: "''", additionalEmails: null, }, - description: 'Contact’s Emails', + description: "Contact's Emails", isCustom: false, isNullable: true, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, - 'linkedinLink-id': { - id: '', + }) as FieldMetadataEntity, + 'linkedinLink-id': getMockFieldMetadataEntity({ + workspaceId, objectMetadataId: '', + id: 'linkedinLink-id', type: FieldMetadataType.LINKS, name: 'linkedinLink', label: 'Linkedin', defaultValue: { primaryLinkUrl: "''", - secondaryLinks: "'[]'", + secondaryLinks: [], primaryLinkLabel: "''", }, - description: 'Contact’s Linkedin account', + description: "Contact's Linkedin account", isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, - 'jobTitle-id': { - id: '', + }) as FieldMetadataEntity, + 'jobTitle-id': getMockFieldMetadataEntity({ + workspaceId, objectMetadataId: '', + id: 'jobTitle-id', type: FieldMetadataType.TEXT, name: 'jobTitle', label: 'Job Title', defaultValue: "''", - description: 'Contact’s job title', + description: "Contact's job title", isCustom: false, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, + }) as FieldMetadataEntity, }, }); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory.ts index aefd82efc..408de675f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { GraphQLEnumType } from 'graphql'; +import { isDefined } from 'twenty-shared/utils'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; @@ -54,11 +55,13 @@ export class EnumTypeDefinitionFactory { ): GraphQLEnumType { // FixMe: It's a hack until Typescript get fixed on union types for reduce function // https://github.com/microsoft/TypeScript/issues/36390 - const enumOptions = transformEnumValue(fieldMetadata.options) as Array< - FieldMetadataDefaultOption | FieldMetadataComplexOption - >; + const enumOptions = transformEnumValue( + fieldMetadata.options ?? undefined, + ) as + | Array + | undefined; - if (!enumOptions) { + if (!isDefined(enumOptions)) { this.logger.error( `Enum options are not defined for ${fieldMetadata.name}`, { diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts index dd1e7d45c..d5a4c809d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/relation-connect-input-type-definition.factory.ts @@ -117,7 +117,7 @@ export class RelationConnectInputTypeDefinitionFactory { } else { const scalarType = this.typeMapperService.mapToScalarType( field.type, - field.settings, + field.settings ?? undefined, field.name === 'id', ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts index 2356e9747..f3f03a24f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts @@ -108,7 +108,7 @@ const getTypeFactoryOptions = ( ) => { return isInputTypeDefinitionKind(kind) ? { - nullable: fieldMetadata.isNullable, + nullable: fieldMetadata.isNullable ?? undefined, defaultValue: fieldMetadata.defaultValue, isArray: kind !== InputTypeDefinitionKind.Filter && @@ -117,7 +117,7 @@ const getTypeFactoryOptions = ( isIdField: fieldMetadata.name === 'id', } : { - nullable: fieldMetadata.isNullable, + nullable: fieldMetadata.isNullable ?? undefined, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, settings: fieldMetadata.settings, // Scalar type is already defined in the entity itself. diff --git a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts index ae050362f..93192e0b2 100644 --- a/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/interfaces/rest-api-base.handler.ts @@ -34,6 +34,7 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; +import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; export interface PageInfo { hasNextPage?: boolean; @@ -159,7 +160,7 @@ export abstract class RestApiBaseHandler { Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach( (field) => { - if (field.type === FieldMetadataType.RELATION) { + if (isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION)) { if ( depth === MAX_DEPTH && isDefined(field.relationTargetObjectMetadataId) diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts index 9ba6edb74..4f07077c9 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/get-field-type.utils.spec.ts @@ -1,30 +1,31 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { fieldNumberMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('getFieldType', () => { - const completeFieldNumberMock: FieldMetadataInterface = { + const completeFieldNumberMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', id: 'field-number-id', type: fieldNumberMock.type, name: fieldNumberMock.name, label: 'Field Number', - objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); const fieldsById: FieldMetadataMap = { - 'field-number-id': completeFieldNumberMock, + 'field-number-id': completeFieldNumberMock as FieldMetadataEntity, }; const mockObjectMetadataWithFieldMaps = { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts index 90057c1f2..813e65bf9 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts @@ -1,7 +1,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { @@ -11,54 +10,59 @@ import { objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('mapFieldMetadataToGraphqlQuery', () => { - const typedFieldNumberMock: FieldMetadataInterface = { - id: 'field-number-id', + const typedFieldNumberMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000002', name: fieldNumberMock.name, type: fieldNumberMock.type, label: 'Field Number', - objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); - const typedFieldTextMock: FieldMetadataInterface = { - id: 'field-text-id', + const typedFieldTextMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000003', name: fieldTextMock.name, type: fieldTextMock.type, label: 'Field Text', - objectMetadataId: 'object-metadata-id', isNullable: fieldTextMock.isNullable, defaultValue: fieldTextMock.defaultValue, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); - const typedFieldCurrencyMock: FieldMetadataInterface = { - id: 'field-currency-id', + const typedFieldCurrencyMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000004', name: fieldCurrencyMock.name, type: fieldCurrencyMock.type, label: 'Field Currency', - objectMetadataId: 'object-metadata-id', isNullable: fieldCurrencyMock.isNullable, defaultValue: fieldCurrencyMock.defaultValue, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); const fieldsById: FieldMetadataMap = { - 'field-number-id': typedFieldNumberMock, - 'field-text-id': typedFieldTextMock, - 'field-currency-id': typedFieldCurrencyMock, + 'field-number-id': typedFieldNumberMock as FieldMetadataEntity, + 'field-text-id': typedFieldTextMock as FieldMetadataEntity, + 'field-currency-id': typedFieldCurrencyMock as FieldMetadataEntity, }; const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = { @@ -86,19 +90,19 @@ describe('mapFieldMetadataToGraphqlQuery', () => { expect( mapFieldMetadataToGraphqlQuery( objectMetadataMapsMock, - typedFieldNumberMock, + typedFieldNumberMock as FieldMetadataEntity, ), ).toEqual('fieldNumber'); expect( mapFieldMetadataToGraphqlQuery( objectMetadataMapsMock, - typedFieldTextMock, + typedFieldTextMock as FieldMetadataEntity, ), ).toEqual('fieldText'); expect( mapFieldMetadataToGraphqlQuery( objectMetadataMapsMock, - typedFieldCurrencyMock, + typedFieldCurrencyMock as FieldMetadataEntity, ), ).toEqual(` fieldCurrency @@ -112,29 +116,25 @@ describe('mapFieldMetadataToGraphqlQuery', () => { describe('should handle all field metadata types', () => { Object.values(FieldMetadataType).forEach((fieldMetadataType) => { it(`with field type ${fieldMetadataType}`, () => { - const field: FieldMetadataInterface = { - id: 'test-field-id', + const field = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000005', type: fieldMetadataType, name: 'toObjectMetadataName', label: 'Test Field', - objectMetadataId: 'object-metadata-id', isNullable: true, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; - - if (fieldMetadataType === FieldMetadataType.RELATION) { - field.settings = { - relationType: RelationType.MANY_TO_ONE, - } as FieldMetadataDefaultSettings; - } - - if (fieldMetadataType === FieldMetadataType.MORPH_RELATION) { - field.settings = { - relationType: RelationType.MANY_TO_ONE, - } as FieldMetadataDefaultSettings; - } + settings: + fieldMetadataType === FieldMetadataType.RELATION || + fieldMetadataType === FieldMetadataType.MORPH_RELATION + ? ({ + relationType: RelationType.MANY_TO_ONE, + } as FieldMetadataDefaultSettings) + : undefined, + }); expect( mapFieldMetadataToGraphqlQuery(objectMetadataMapsMock, field), diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts index 9f10e86bf..894f1f811 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/mocks/opportunity-field-maps.mock.ts @@ -1,11 +1,18 @@ import { FieldMetadataType } from 'twenty-shared/types'; +import { DateDisplayFormat } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; +import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; + +const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419'; +const objectMetadataId = '20202020-6e2c-42f6-a83c-cc58d776af88'; export const OPPORTUNITY_WITH_FIELDS_MAPS = { - id: '20202020-6e2c-42f6-a83c-cc58d776af88', + id: objectMetadataId, nameSingular: 'opportunity', namePlural: 'opportunities', labelSingular: 'Opportunity', @@ -21,12 +28,13 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSearchable: true, labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6', imageIdentifierFieldMetadataId: null, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - indexMetadatas: [], // unused + workspaceId, + indexMetadatas: [], fieldsById: { - '20202020-c2f1-4435-adca-22931f8b41b6': { + '20202020-c2f1-4435-adca-22931f8b41b6': getMockFieldMetadataEntity({ id: '20202020-c2f1-4435-adca-22931f8b41b6', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.TEXT, name: 'name', label: 'Name', @@ -38,14 +46,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: false, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-5eef-417a-b517-ebeedaa8e10b': { + }) as FieldMetadataEntity, + '20202020-5eef-417a-b517-ebeedaa8e10b': getMockFieldMetadataEntity({ id: '20202020-5eef-417a-b517-ebeedaa8e10b', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.CURRENCY, name: 'amount', label: 'Amount', @@ -57,14 +65,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-597c-44d3-98ec-ea71aea5256b': { + }) as FieldMetadataEntity, + '20202020-597c-44d3-98ec-ea71aea5256b': getMockFieldMetadataEntity({ id: '20202020-597c-44d3-98ec-ea71aea5256b', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.DATE_TIME, name: 'closeDate', label: 'Close date', @@ -76,14 +84,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-9b94-454a-94ca-8afb09c68faf': { + }) as FieldMetadataEntity, + '20202020-9b94-454a-94ca-8afb09c68faf': getMockFieldMetadataEntity({ id: '20202020-9b94-454a-94ca-8afb09c68faf', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.SELECT, name: 'stage', label: 'Stage', @@ -132,14 +140,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: false, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-30a5-4d8e-9b93-12d31ece0aaa': { + }) as FieldMetadataEntity, + '20202020-30a5-4d8e-9b93-12d31ece0aaa': getMockFieldMetadataEntity({ id: '20202020-30a5-4d8e-9b93-12d31ece0aaa', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.POSITION, name: 'position', label: 'Position', @@ -151,18 +159,18 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: true, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-f95f-424f-ab32-65961e8e9635': { + }) as FieldMetadataEntity, + '20202020-f95f-424f-ab32-65961e8e9635': getMockFieldMetadataEntity({ id: '20202020-f95f-424f-ab32-65961e8e9635', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.ACTOR, name: 'createdBy', label: 'Created by', - defaultValue: { name: "'System'", source: "'MANUAL'", context: {} }, + defaultValue: { name: "'System'", source: "'MANUAL'" }, description: 'The creator of the record', icon: 'IconCreativeCommonsSa', isCustom: false, @@ -170,14 +178,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: false, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-5e10-4780-babb-38a465ac546c': { + }) as FieldMetadataEntity, + '20202020-5e10-4780-babb-38a465ac546c': getMockFieldMetadataEntity({ id: '20202020-5e10-4780-babb-38a465ac546c', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.TEXT, name: 'searchVector', label: 'Search vector', @@ -189,14 +197,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: true, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-8f4a-4f8d-822e-90fe72f75b79': { + }) as FieldMetadataEntity, + '20202020-8f4a-4f8d-822e-90fe72f75b79': getMockFieldMetadataEntity({ id: '20202020-8f4a-4f8d-822e-90fe72f75b79', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.UUID, name: 'id', label: 'Id', @@ -208,233 +216,233 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = { isSystem: true, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-f120-4b59-b239-f7f1d8eb243e': { + }) as FieldMetadataEntity, + '20202020-f120-4b59-b239-f7f1d8eb243e': getMockFieldMetadataEntity({ id: '20202020-f120-4b59-b239-f7f1d8eb243e', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.DATE_TIME, name: 'createdAt', label: 'Creation date', defaultValue: 'now', description: 'Creation date', icon: 'IconCalendar', - settings: { displayFormat: 'RELATIVE' } as any, + settings: { displayFormat: DateDisplayFormat.RELATIVE }, isCustom: false, isActive: true, isSystem: false, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: false, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-dcc8-4318-9756-b87377692561': { + }) as FieldMetadataEntity, + '20202020-dcc8-4318-9756-b87377692561': getMockFieldMetadataEntity({ id: '20202020-dcc8-4318-9756-b87377692561', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.DATE_TIME, name: 'updatedAt', label: 'Last update', defaultValue: 'now', description: 'Last time the record was changed', icon: 'IconCalendarClock', - settings: { displayFormat: 'RELATIVE' } as any, + settings: { displayFormat: DateDisplayFormat.RELATIVE }, isCustom: false, isActive: true, isSystem: false, isNullable: false, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: false, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-1694-4f8b-8760-61a5ff330022': { + }) as FieldMetadataEntity, + '20202020-1694-4f8b-8760-61a5ff330022': getMockFieldMetadataEntity({ id: '20202020-1694-4f8b-8760-61a5ff330022', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.DATE_TIME, name: 'deletedAt', - label: 'Deleted at', + label: 'Deletion date', defaultValue: null, - description: 'Date when the record was deleted', - icon: 'IconCalendarMinus', - settings: { displayFormat: 'RELATIVE' } as any, + description: 'Record deletion date', + icon: 'IconCalendarOff', + settings: { displayFormat: DateDisplayFormat.RELATIVE }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: true, + isLabelSyncedWithName: false, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-4f52-4dea-a116-723f9bf7f082': { + }) as FieldMetadataEntity, + '20202020-4f52-4dea-a116-723f9bf7f082': getMockFieldMetadataEntity({ id: '20202020-4f52-4dea-a116-723f9bf7f082', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'pointOfContact', label: 'Point of Contact', defaultValue: null, - description: 'Opportunity point of contact', + description: 'The point of contact for this opportunity', icon: 'IconUser', settings: { - onDelete: 'SET_NULL', - relationType: 'MANY_TO_ONE', + relationType: RelationType.MANY_TO_ONE, joinColumnName: 'pointOfContactId', - } as any, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '20202020-a36b-4889-97d4-63a578423688', - relationTargetObjectMetadataId: '20202020-6799-4a38-92d3-8e844a7ea8ab', createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-fc02-4be2-be1a-e121daf5400d': { + }) as FieldMetadataEntity, + '20202020-fc02-4be2-be1a-e121daf5400d': getMockFieldMetadataEntity({ id: '20202020-fc02-4be2-be1a-e121daf5400d', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'company', label: 'Company', defaultValue: null, - description: 'Opportunity company', + description: 'The company this opportunity is associated with', icon: 'IconBuildingSkyscraper', settings: { - onDelete: 'SET_NULL', - relationType: 'MANY_TO_ONE', + relationType: RelationType.MANY_TO_ONE, joinColumnName: 'companyId', - } as any, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '20202020-bd16-4f63-8165-0a7f5d78170d', - relationTargetObjectMetadataId: '20202020-0be8-4764-8e0d-7a2e1c66f78c', createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-fd9f-48f0-bd5f-5b0fec6a5de4': { + }) as FieldMetadataEntity, + '20202020-fd9f-48f0-bd5f-5b0fec6a5de4': getMockFieldMetadataEntity({ id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'favorites', label: 'Favorites', defaultValue: null, - description: 'Favorites linked to the opportunity', - icon: 'IconHeart', - settings: { relationType: RelationType.ONE_TO_MANY } as any, + description: 'Users who favorited this opportunity', + icon: 'IconStar', + settings: { + relationType: RelationType.ONE_TO_MANY, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, - isSystem: true, + isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '20202020-7c3c-4149-b400-5a958910c7a2', - relationTargetObjectMetadataId: '20202020-df10-42dc-baed-ea77db7ad96c', createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-88ab-4138-98ce-80533bb423e3': { + }) as FieldMetadataEntity, + '20202020-88ab-4138-98ce-80533bb423e3': getMockFieldMetadataEntity({ id: '20202020-88ab-4138-98ce-80533bb423e3', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'taskTargets', - label: 'Tasks', + label: 'Task Targets', defaultValue: null, - description: 'Tasks tied to the opportunity', + description: 'Tasks targeting this opportunity', icon: 'IconCheckbox', - settings: { relationType: RelationType.ONE_TO_MANY } as any, + settings: { + relationType: RelationType.ONE_TO_MANY, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: false, - relationTargetFieldMetadataId: '20202020-eb77-4b1c-b6a6-d5dcd13b1634', - relationTargetObjectMetadataId: '20202020-3af1-4c4f-90f4-cd43c53f7f41', + isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-4258-422b-b35b-db3f090af8da': { + }) as FieldMetadataEntity, + '20202020-4258-422b-b35b-db3f090af8da': getMockFieldMetadataEntity({ id: '20202020-4258-422b-b35b-db3f090af8da', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'noteTargets', - label: 'Notes', + label: 'Note Targets', defaultValue: null, - description: 'Notes tied to the opportunity', + description: 'Notes targeting this opportunity', icon: 'IconNotes', - settings: { relationType: RelationType.ONE_TO_MANY } as any, + settings: { + relationType: RelationType.ONE_TO_MANY, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', - isLabelSyncedWithName: false, - relationTargetFieldMetadataId: '20202020-d927-4c91-9893-28cea5aff979', - relationTargetObjectMetadataId: '20202020-8a5e-4dea-868b-71611e718a73', + isLabelSyncedWithName: true, createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-16ca-40a7-a1ba-712975c916cd': { + }) as FieldMetadataEntity, + '20202020-16ca-40a7-a1ba-712975c916cd': getMockFieldMetadataEntity({ id: '20202020-16ca-40a7-a1ba-712975c916cd', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'attachments', label: 'Attachments', defaultValue: null, - description: 'Attachments linked to the opportunity', - icon: 'IconFileImport', - settings: { relationType: RelationType.ONE_TO_MANY } as any, + description: 'Attachments for this opportunity', + icon: 'IconPaperclip', + settings: { + relationType: RelationType.ONE_TO_MANY, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '20202020-9236-427a-8a8a-a2296b93b542', - relationTargetObjectMetadataId: '20202020-3005-4c93-a04c-2941f7424f54', createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, - '20202020-92a5-47bf-a38d-c1c72b2c3e4d': { + }) as FieldMetadataEntity, + '20202020-92a5-47bf-a38d-c1c72b2c3e4d': getMockFieldMetadataEntity({ id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d', - objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', + workspaceId, + objectMetadataId, type: FieldMetadataType.RELATION, name: 'timelineActivities', label: 'Timeline Activities', defaultValue: null, - description: 'Timeline Activities linked to the opportunity.', - icon: 'IconTimelineEvent', - settings: { relationType: RelationType.ONE_TO_MANY } as any, + description: 'Timeline activities for this opportunity', + icon: 'IconTimeline', + settings: { + relationType: RelationType.ONE_TO_MANY, + onDelete: RelationOnDeleteAction.CASCADE, + }, isCustom: false, isActive: true, isSystem: false, isNullable: true, isUnique: false, - workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true, - relationTargetFieldMetadataId: '20202020-2d54-41c7-b886-29deca3c28d5', - relationTargetObjectMetadataId: '20202020-a89e-4e4d-b3d9-c3f99e7c7483', createdAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'), - }, + }) as FieldMetadataEntity, }, fieldIdByName: { name: '20202020-c2f1-4435-adca-22931f8b41b6', diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts index 3fae2eebd..bb8d321a5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/check-filter-enum-values.spec.ts @@ -1,31 +1,32 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { fieldSelectMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('checkFilterEnumValues', () => { - const completeFieldSelectMock: FieldMetadataInterface = { + const completeFieldSelectMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', id: 'field-select-id', type: fieldSelectMock.type, name: fieldSelectMock.name, label: 'Field Select', - objectMetadataId: 'object-metadata-id', isNullable: fieldSelectMock.isNullable, defaultValue: fieldSelectMock.defaultValue, options: fieldSelectMock.options, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); const fieldsById: FieldMetadataMap = { - 'field-select-id': completeFieldSelectMock, + 'field-select-id': completeFieldSelectMock as FieldMetadataEntity, }; const mockObjectMetadataWithFieldMaps = { diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts index 0dc6630db..7cd0d0048 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/filter-utils/__tests__/parse-filter.utils.spec.ts @@ -1,44 +1,46 @@ -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { fieldNumberMock, fieldTextMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('parseFilter', () => { - const completeFieldNumberMock: FieldMetadataInterface = { - id: 'field-number-id', + const completeFieldNumberMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000002', type: fieldNumberMock.type, name: fieldNumberMock.name, label: 'Field Number', - objectMetadataId: 'object-metadata-id', isNullable: fieldNumberMock.isNullable, defaultValue: fieldNumberMock.defaultValue, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); - const completeFieldTextMock: FieldMetadataInterface = { - id: 'field-text-id', + const completeFieldTextMock = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000003', type: fieldTextMock.type, name: fieldTextMock.name, label: 'Field Text', - objectMetadataId: 'object-metadata-id', isNullable: fieldTextMock.isNullable, defaultValue: fieldTextMock.defaultValue, isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }); const fieldsById: FieldMetadataMap = { - 'field-number-id': completeFieldNumberMock, - 'field-text-id': completeFieldTextMock, + 'field-number-id': completeFieldNumberMock as FieldMetadataEntity, + 'field-text-id': completeFieldTextMock as FieldMetadataEntity, }; const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = { @@ -59,7 +61,7 @@ describe('parseFilter', () => { mockObjectMetadataWithFieldMaps, ), ).toEqual({ - and: [{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: 2 } }], + and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }], }); }); @@ -71,8 +73,8 @@ describe('parseFilter', () => { ), ).toEqual({ and: [ - { fieldNumber: { eq: 1 } }, - { or: [{ fieldNumber: { eq: 2 } }, { fieldNumber: { eq: 3 } }] }, + { fieldNumber: { eq: '1' } }, + { or: [{ fieldNumber: { eq: '2' } }, { fieldNumber: { eq: '3' } }] }, ], }); }); @@ -85,15 +87,17 @@ describe('parseFilter', () => { ), ).toEqual({ and: [ - { fieldNumber: { eq: 1 } }, + { fieldNumber: { eq: '1' } }, { or: [ - { fieldNumber: { eq: 2 } }, - { fieldNumber: { eq: 3 } }, - { and: [{ fieldNumber: { eq: 6 } }, { fieldNumber: { eq: 7 } }] }, + { fieldNumber: { eq: '2' } }, + { fieldNumber: { eq: '3' } }, + { + and: [{ fieldNumber: { eq: '6' } }, { fieldNumber: { eq: '7' } }], + }, ], }, - { or: [{ fieldNumber: { eq: 4 } }, { fieldNumber: { eq: 5 } }] }, + { or: [{ fieldNumber: { eq: '4' } }, { fieldNumber: { eq: '5' } }] }, ], }); }); @@ -113,13 +117,13 @@ describe('parseFilter', () => { { not: { fieldText: { startsWith: 'val' } } }, { and: [ - { fieldNumber: { eq: 6 } }, + { fieldNumber: { eq: '6' } }, { fieldText: { ilike: '%val%' } }, ], }, ], }, - { or: [{ fieldNumber: { eq: 4 } }, { fieldText: { is: 'NULL' } }] }, + { or: [{ fieldNumber: { eq: '4' } }, { fieldText: { is: 'NULL' } }] }, ], }); }); @@ -132,9 +136,9 @@ describe('parseFilter', () => { ), ).toEqual({ and: [ - { fieldNumber: { eq: 1 } }, + { fieldNumber: { eq: '1' } }, { - not: { fieldNumber: { eq: 2 } }, + not: { fieldNumber: { eq: '2' } }, }, ], }); diff --git a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts index 63231a374..c7b220825 100644 --- a/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-factories/__tests__/filter-input.factory.spec.ts @@ -1,7 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { fieldCurrencyMock, fieldNumberMock, @@ -9,11 +7,16 @@ import { objectMetadataMapItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('FilterInputFactory', () => { - const completeFieldNumberMock: FieldMetadataInterface = { + const workspaceId = '20202020-cc80-4306-ad69-da9e11997292'; + + const completeFieldNumberMock = getMockFieldMetadataEntity({ + workspaceId, id: 'field-number-id', type: fieldNumberMock.type, name: fieldNumberMock.name, @@ -24,9 +27,10 @@ describe('FilterInputFactory', () => { isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }) as FieldMetadataEntity; - const completeFieldTextMock: FieldMetadataInterface = { + const completeFieldTextMock = getMockFieldMetadataEntity({ + workspaceId, id: 'field-text-id', type: fieldTextMock.type, name: fieldTextMock.name, @@ -37,9 +41,10 @@ describe('FilterInputFactory', () => { isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }) as FieldMetadataEntity; - const completeFieldCurrencyMock: FieldMetadataInterface = { + const completeFieldCurrencyMock = getMockFieldMetadataEntity({ + workspaceId, id: 'field-currency-id', type: fieldCurrencyMock.type, name: fieldCurrencyMock.name, @@ -50,7 +55,7 @@ describe('FilterInputFactory', () => { isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }; + }) as FieldMetadataEntity; const fieldsById: FieldMetadataMap = { 'field-number-id': completeFieldNumberMock, diff --git a/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts b/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts index 285f4714f..fc60ca1ae 100644 --- a/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/utils/__tests__/compute-cursor-arg-filter.utils.spec.ts @@ -4,7 +4,9 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('computeCursorArgFilter', () => { const objectMetadataItemWithFieldMaps = { @@ -30,39 +32,42 @@ describe('computeCursorArgFilter', () => { fullName: 'fullname-id', }, fieldsById: { - 'name-id': { - type: FieldMetadataType.TEXT, + 'name-id': getMockFieldMetadataEntity({ + workspaceId: 'workspace-id', + objectMetadataId: 'object-id', id: 'name-id', + type: FieldMetadataType.TEXT, name: 'name', label: 'Name', - objectMetadataId: 'object-id', isLabelSyncedWithName: true, isNullable: true, createdAt: new Date(), updatedAt: new Date(), - }, - 'age-id': { - type: FieldMetadataType.NUMBER, + }) as FieldMetadataEntity, + 'age-id': getMockFieldMetadataEntity({ + workspaceId: 'workspace-id', + objectMetadataId: 'object-id', id: 'age-id', + type: FieldMetadataType.NUMBER, name: 'age', label: 'Age', - objectMetadataId: 'object-id', isLabelSyncedWithName: true, isNullable: true, createdAt: new Date(), updatedAt: new Date(), - }, - 'fullname-id': { - type: FieldMetadataType.FULL_NAME, + }) as FieldMetadataEntity, + 'fullname-id': getMockFieldMetadataEntity({ + workspaceId: 'workspace-id', + objectMetadataId: 'object-id', id: 'fullname-id', + type: FieldMetadataType.FULL_NAME, name: 'fullName', label: 'Full Name', - objectMetadataId: 'object-id', isLabelSyncedWithName: true, isNullable: true, createdAt: new Date(), updatedAt: new Date(), - }, + }) as FieldMetadataEntity, }, } satisfies ObjectMetadataItemWithFieldMaps; diff --git a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts index 5e0d95efc..0c42e239e 100644 --- a/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts +++ b/packages/twenty-server/src/engine/core-modules/__mocks__/mockObjectMetadataItemsWithFieldMaps.ts @@ -1,6 +1,10 @@ import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; + +const workspaceId = '20202020-0000-0000-0000-000000000000'; export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMaps[] = [ @@ -22,12 +26,13 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isSearchable: true, labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: '', - workspaceId: '', + workspaceId, indexMetadatas: [], fieldsById: { - nameFieldMetadataId: { + nameFieldMetadataId: getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId: '20202020-8dec-43d5-b2ff-6eef05095bec', id: 'nameFieldMetadataId', - objectMetadataId: '', type: FieldMetadataType.FULL_NAME, icon: 'test-field-icon', name: 'name', @@ -36,15 +41,14 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa lastName: "''", firstName: "''", }, - description: 'Contact’s name', + description: "Contact's name", isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, + }) as FieldMetadataEntity, }, fieldIdByName: { name: 'nameFieldMetadataId', @@ -69,12 +73,13 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isSearchable: true, labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: '', - workspaceId: '', + workspaceId, indexMetadatas: [], fieldsById: { - nameFieldMetadataId: { + nameFieldMetadataId: getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', id: 'nameFieldMetadataId', - objectMetadataId: '', type: FieldMetadataType.TEXT, icon: 'test-field-icon', name: 'name', @@ -83,27 +88,30 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, - domainNameFieldMetadataId: { + }) as FieldMetadataEntity, + domainNameFieldMetadataId: getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c', id: 'domainNameFieldMetadataId', - objectMetadataId: '', type: FieldMetadataType.LINKS, icon: 'test-field-icon', name: 'domainName', label: 'Domain Name', - defaultValue: '', + defaultValue: { + primaryLinkLabel: '', + primaryLinkUrl: '', + secondaryLinks: [], + }, isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, + }) as FieldMetadataEntity, }, fieldIdByName: { name: 'nameFieldMetadataId', @@ -129,12 +137,13 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isSearchable: true, labelIdentifierFieldMetadataId: 'nameFieldMetadataId', imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', - workspaceId: '', + workspaceId, indexMetadatas: [], fieldsById: { - nameFieldMetadataId: { + nameFieldMetadataId: getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', id: 'nameFieldMetadataId', - objectMetadataId: '', type: FieldMetadataType.TEXT, icon: 'test-field-icon', name: 'name', @@ -143,14 +152,14 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, - imageIdentifierFieldMetadataId: { + }) as FieldMetadataEntity, + imageIdentifierFieldMetadataId: getMockFieldMetadataEntity({ + workspaceId, + objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca', id: 'imageIdentifierFieldMetadataId', - objectMetadataId: '', type: FieldMetadataType.TEXT, icon: 'test-field-icon', name: 'imageIdentifierFieldName', @@ -159,11 +168,10 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isCustom: false, isNullable: true, isUnique: false, - workspaceId: '', isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - }, + }) as FieldMetadataEntity, }, fieldIdByName: { name: 'nameFieldMetadataId', @@ -189,7 +197,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa isSearchable: false, labelIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '', - workspaceId: '', + workspaceId, indexMetadatas: [], fieldsById: {}, fieldIdByName: {}, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index 8fc2bb58e..97453d850 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -19,7 +19,7 @@ describe('computeSchemaComponents', () => { ).toMatchInlineSnapshot(` { "ObjectName": { - "description": undefined, + "description": "Object description", "example": { "fieldCurrency": { "amountMicros": 284000000, @@ -48,10 +48,13 @@ describe('computeSchemaComponents', () => { "primaryPhoneCountryCode": "FR", "primaryPhoneNumber": "06 10 20 30 40", }, - "fieldSelect": "OPTION_1", + "fieldSelect": [ + "OPTION_1", + ], }, "properties": { "fieldActor": { + "description": "Default field metadata entity description", "properties": { "source": { "enum": [ @@ -70,6 +73,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldAddress": { + "description": "Default field metadata entity description", "properties": { "addressCity": { "type": "string", @@ -99,15 +103,18 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldArray": { + "description": "Default field metadata entity description", "items": { "type": "string", }, "type": "array", }, "fieldBoolean": { + "description": "Default field metadata entity description", "type": "boolean", }, "fieldCurrency": { + "description": "Default field metadata entity description", "properties": { "amountMicros": { "type": "number", @@ -119,14 +126,17 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldDate": { + "description": "Default field metadata entity description", "format": "date", "type": "string", }, "fieldDateTime": { + "description": "Default field metadata entity description", "format": "date-time", "type": "string", }, "fieldEmails": { + "description": "Default field metadata entity description", "properties": { "additionalEmails": { "items": { @@ -142,6 +152,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldFullName": { + "description": "Default field metadata entity description", "properties": { "firstName": { "type": "string", @@ -153,6 +164,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldLinks": { + "description": "Default field metadata entity description", "properties": { "primaryLinkLabel": { "type": "string", @@ -180,6 +192,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldMultiSelect": { + "description": "Default field metadata entity description", "items": { "enum": [ "OPTION_1", @@ -190,12 +203,15 @@ describe('computeSchemaComponents', () => { "type": "array", }, "fieldNumber": { + "description": "Default field metadata entity description", "type": "integer", }, "fieldNumeric": { + "description": "Default field metadata entity description", "type": "number", }, "fieldPhones": { + "description": "Default field metadata entity description", "properties": { "additionalPhones": { "items": { @@ -216,9 +232,11 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldPosition": { + "description": "Default field metadata entity description", "type": "number", }, "fieldRating": { + "description": "Default field metadata entity description", "enum": [ "RATING_1", "RATING_2", @@ -226,6 +244,7 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldRawJson": { + "description": "Default field metadata entity description", "type": "object", }, "fieldRelationId": { @@ -233,9 +252,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldRichText": { + "description": "Default field metadata entity description", "type": "string", }, "fieldSelect": { + "description": "Default field metadata entity description", "enum": [ "OPTION_1", "OPTION_2", @@ -243,9 +264,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldText": { + "description": "Default field metadata entity description", "type": "string", }, "fieldUuid": { + "description": "Default field metadata entity description", "format": "uuid", "type": "string", }, @@ -256,9 +279,10 @@ describe('computeSchemaComponents', () => { "type": "object", }, "ObjectNameForResponse": { - "description": undefined, + "description": "Object description", "properties": { "fieldActor": { + "description": "Default field metadata entity description", "properties": { "name": { "type": "string", @@ -284,6 +308,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldAddress": { + "description": "Default field metadata entity description", "properties": { "addressCity": { "type": "string", @@ -313,15 +338,18 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldArray": { + "description": "Default field metadata entity description", "items": { "type": "string", }, "type": "array", }, "fieldBoolean": { + "description": "Default field metadata entity description", "type": "boolean", }, "fieldCurrency": { + "description": "Default field metadata entity description", "properties": { "amountMicros": { "type": "number", @@ -333,14 +361,17 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldDate": { + "description": "Default field metadata entity description", "format": "date", "type": "string", }, "fieldDateTime": { + "description": "Default field metadata entity description", "format": "date-time", "type": "string", }, "fieldEmails": { + "description": "Default field metadata entity description", "properties": { "additionalEmails": { "items": { @@ -356,6 +387,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldFullName": { + "description": "Default field metadata entity description", "properties": { "firstName": { "type": "string", @@ -367,6 +399,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldLinks": { + "description": "Default field metadata entity description", "properties": { "primaryLinkLabel": { "type": "string", @@ -394,6 +427,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldMultiSelect": { + "description": "Default field metadata entity description", "items": { "enum": [ "OPTION_1", @@ -404,12 +438,15 @@ describe('computeSchemaComponents', () => { "type": "array", }, "fieldNumber": { + "description": "Default field metadata entity description", "type": "integer", }, "fieldNumeric": { + "description": "Default field metadata entity description", "type": "number", }, "fieldPhones": { + "description": "Default field metadata entity description", "properties": { "additionalPhones": { "items": { @@ -430,9 +467,11 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldPosition": { + "description": "Default field metadata entity description", "type": "number", }, "fieldRating": { + "description": "Default field metadata entity description", "enum": [ "RATING_1", "RATING_2", @@ -440,9 +479,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldRawJson": { + "description": "Default field metadata entity description", "type": "object", }, "fieldRelation": { + "description": "Default field metadata entity description", "oneOf": [ { "$ref": "#/components/schemas/RelationTargetObjectForResponse", @@ -455,9 +496,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldRichText": { + "description": "Default field metadata entity description", "type": "string", }, "fieldSelect": { + "description": "Default field metadata entity description", "enum": [ "OPTION_1", "OPTION_2", @@ -465,9 +508,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldText": { + "description": "Default field metadata entity description", "type": "string", }, "fieldUuid": { + "description": "Default field metadata entity description", "format": "uuid", "type": "string", }, @@ -475,7 +520,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "ObjectNameForUpdate": { - "description": undefined, + "description": "Object description", "example": { "fieldCurrency": { "amountMicros": 253000000, @@ -504,10 +549,13 @@ describe('computeSchemaComponents', () => { "primaryPhoneCountryCode": "FR", "primaryPhoneNumber": "06 10 20 30 40", }, - "fieldSelect": "OPTION_1", + "fieldSelect": [ + "OPTION_1", + ], }, "properties": { "fieldActor": { + "description": "Default field metadata entity description", "properties": { "source": { "enum": [ @@ -526,6 +574,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldAddress": { + "description": "Default field metadata entity description", "properties": { "addressCity": { "type": "string", @@ -555,15 +604,18 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldArray": { + "description": "Default field metadata entity description", "items": { "type": "string", }, "type": "array", }, "fieldBoolean": { + "description": "Default field metadata entity description", "type": "boolean", }, "fieldCurrency": { + "description": "Default field metadata entity description", "properties": { "amountMicros": { "type": "number", @@ -575,14 +627,17 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldDate": { + "description": "Default field metadata entity description", "format": "date", "type": "string", }, "fieldDateTime": { + "description": "Default field metadata entity description", "format": "date-time", "type": "string", }, "fieldEmails": { + "description": "Default field metadata entity description", "properties": { "additionalEmails": { "items": { @@ -598,6 +653,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldFullName": { + "description": "Default field metadata entity description", "properties": { "firstName": { "type": "string", @@ -609,6 +665,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldLinks": { + "description": "Default field metadata entity description", "properties": { "primaryLinkLabel": { "type": "string", @@ -636,6 +693,7 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldMultiSelect": { + "description": "Default field metadata entity description", "items": { "enum": [ "OPTION_1", @@ -646,12 +704,15 @@ describe('computeSchemaComponents', () => { "type": "array", }, "fieldNumber": { + "description": "Default field metadata entity description", "type": "integer", }, "fieldNumeric": { + "description": "Default field metadata entity description", "type": "number", }, "fieldPhones": { + "description": "Default field metadata entity description", "properties": { "additionalPhones": { "items": { @@ -672,9 +733,11 @@ describe('computeSchemaComponents', () => { "type": "object", }, "fieldPosition": { + "description": "Default field metadata entity description", "type": "number", }, "fieldRating": { + "description": "Default field metadata entity description", "enum": [ "RATING_1", "RATING_2", @@ -682,6 +745,7 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldRawJson": { + "description": "Default field metadata entity description", "type": "object", }, "fieldRelationId": { @@ -689,9 +753,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldRichText": { + "description": "Default field metadata entity description", "type": "string", }, "fieldSelect": { + "description": "Default field metadata entity description", "enum": [ "OPTION_1", "OPTION_2", @@ -699,9 +765,11 @@ describe('computeSchemaComponents', () => { "type": "string", }, "fieldText": { + "description": "Default field metadata entity description", "type": "string", }, "fieldUuid": { + "description": "Default field metadata entity description", "format": "uuid", "type": "string", }, diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts index c8ea7ebed..3c8dbb1aa 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/generate-random-field-value.util.ts @@ -81,11 +81,19 @@ export const generateRandomFieldValue = ({ } case FieldMetadataType.SELECT: { - return isDefined(field.options[0].value) ? field.options[0].value : []; + if (!isDefined(field.options) || !isDefined(field.options[0].value)) { + return []; + } + + return [field.options[0].value]; } case FieldMetadataType.MULTI_SELECT: { - return isDefined(field.options[0].value) ? [field.options[0].value] : []; + if (!isDefined(field.options) || !isDefined(field.options[0].value)) { + return []; + } + + return [field.options[0].value]; } case FieldMetadataType.RELATION: diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 15c25e1df..097caf9db 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -14,6 +14,7 @@ import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dto import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; +import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util'; import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util'; import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; @@ -211,40 +212,41 @@ export class DataloaderService { return []; } - const fields = Object.values(objectMetadata.fieldsById).map( - // TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface - (fieldMetadata) => { - const overridesFieldToCompute = [ - 'icon', - 'label', - 'description', - ] as const satisfies (keyof FieldMetadataInterface)[]; + const fields = Object.values( + objectMetadata.fieldsById, + ).map((fieldMetadata) => { + const overridesFieldToCompute = [ + 'icon', + 'label', + 'description', + ] as const satisfies (keyof FieldMetadataInterface)[]; - const overrides = overridesFieldToCompute.reduce< - Partial< - Record<(typeof overridesFieldToCompute)[number], string> - > - >( - (acc, field) => ({ - ...acc, - [field]: resolveFieldMetadataStandardOverride( - fieldMetadata, - field, - dataLoaderParams[0].locale, - ), - }), - {}, - ); + const overrides = overridesFieldToCompute.reduce< + Partial> + >( + (acc, field) => ({ + ...acc, + [field]: resolveFieldMetadataStandardOverride( + { + label: fieldMetadata.label, + description: fieldMetadata.description ?? undefined, + icon: fieldMetadata.icon ?? undefined, + isCustom: fieldMetadata.isCustom, + standardOverrides: + fieldMetadata.standardOverrides ?? undefined, + }, + field, + dataLoaderParams[0].locale, + ), + }), + {}, + ); - return { - ...fieldMetadata, - createdAt: new Date(fieldMetadata.createdAt), - updatedAt: new Date(fieldMetadata.updatedAt), - workspaceId: workspaceId, - ...overrides, - }; - }, - ); + return fromFieldMetadataEntityToFieldMetadataDto({ + ...fieldMetadata, + ...overrides, + }); + }); return filterMorphRelationDuplicateFieldsDTO(fields); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts index ccb7ce73e..24b2c4fca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto.ts @@ -56,6 +56,7 @@ registerEnumType(FieldMetadataType, { @Relation('object', () => ObjectMetadataDTO, { nullable: true, }) +// TODO refactor nullable fields to be typed as nullable and not optional export class FieldMetadataDTO { @IsUUID() @IsNotEmpty() @@ -132,7 +133,7 @@ export class FieldMetadataDTO { // @Validate(IsFieldMetadataOptions) @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) - options?: FieldMetadataOptions; + options?: FieldMetadataOptions | null; @IsOptional() @Field(() => GraphQLJSON, { nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index 73e088479..a2aef4b48 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -1,4 +1,4 @@ -import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldMetadataType, IsExactly } from 'twenty-shared/types'; import { Column, CreateDateColumn, @@ -22,8 +22,16 @@ import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-meta import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; +type IsRelationType = + IsExactly extends true + ? null | Ttype + : T extends FieldMetadataType.RELATION + ? Ttype + : T extends FieldMetadataType.MORPH_RELATION + ? Ttype + : never; + @Entity('fieldMetadata') -// max length of index is 63 characters @Index( 'IDX_FIELD_METADATA_NAME_OBJMID_WORKSPACE_ID_EXCEPT_MORPH_UNIQUE', ['name', 'objectMetadataId', 'workspaceId'], @@ -42,6 +50,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis 'objectMetadataId', 'workspaceId', ]) +// TODO add some documentation about this entity export class FieldMetadataEntity< T extends FieldMetadataType = FieldMetadataType, > { @@ -56,6 +65,7 @@ export class FieldMetadataEntity< @ManyToOne(() => ObjectMetadataEntity, (object) => object.fields, { onDelete: 'CASCADE', + nullable: false, }) @JoinColumn({ name: 'objectMetadataId' }) @Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID', ['objectMetadataId']) @@ -74,22 +84,22 @@ export class FieldMetadataEntity< label: string; @Column({ nullable: true, type: 'jsonb' }) - defaultValue: FieldMetadataDefaultValue; + defaultValue: FieldMetadataDefaultValue | null; @Column({ nullable: true, type: 'text' }) - description: string; + description: string | null; - @Column({ nullable: true }) - icon: string; + @Column({ nullable: true, type: 'varchar' }) + icon: string | null; @Column({ type: 'jsonb', nullable: true }) - standardOverrides?: FieldStandardOverridesDTO; + standardOverrides?: FieldStandardOverridesDTO | null; @Column('jsonb', { nullable: true }) - options: FieldMetadataOptions; + options: FieldMetadataOptions | null; @Column('jsonb', { nullable: true }) - settings?: FieldMetadataSettings; + settings?: FieldMetadataSettings | null; @Column({ default: false }) isCustom: boolean; @@ -100,11 +110,13 @@ export class FieldMetadataEntity< @Column({ default: false }) isSystem: boolean; - @Column({ nullable: true, default: true }) - isNullable: boolean; + // Is this really nullable ? + @Column({ nullable: true, default: true, type: 'boolean' }) + isNullable: boolean | null; - @Column({ nullable: true, default: false }) - isUnique: boolean; + // Is this really nullable ? + @Column({ nullable: true, default: false, type: 'boolean' }) + isUnique: boolean | null; @Column({ nullable: false, type: 'uuid' }) @Index('IDX_FIELD_METADATA_WORKSPACE_ID', ['workspaceId']) @@ -114,25 +126,31 @@ export class FieldMetadataEntity< isLabelSyncedWithName: boolean; @Column({ nullable: true, type: 'uuid' }) - relationTargetFieldMetadataId: string; + relationTargetFieldMetadataId: IsRelationType; + @OneToOne( () => FieldMetadataEntity, (fieldMetadata: FieldMetadataEntity) => fieldMetadata.relationTargetFieldMetadataId, + { nullable: true }, ) @JoinColumn({ name: 'relationTargetFieldMetadataId' }) - relationTargetFieldMetadata: Relation; + relationTargetFieldMetadata: IsRelationType, T>; @Column({ nullable: true, type: 'uuid' }) - relationTargetObjectMetadataId: string; + relationTargetObjectMetadataId: IsRelationType; + @ManyToOne( () => ObjectMetadataEntity, (objectMetadata: ObjectMetadataEntity) => objectMetadata.targetRelationFields, - { onDelete: 'CASCADE' }, + { onDelete: 'CASCADE', nullable: true }, ) @JoinColumn({ name: 'relationTargetObjectMetadataId' }) - relationTargetObjectMetadata: Relation; + relationTargetObjectMetadata: IsRelationType< + Relation, + T + >; @OneToMany( () => IndexFieldMetadataEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts index b9f553598..d64b19923 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.resolver.ts @@ -39,6 +39,7 @@ import { import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; +import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util'; import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; @@ -152,7 +153,7 @@ export class FieldMetadataResolver { workspaceId: workspace.id, }); - if (!fieldMetadata.settings) { + if (!isDefined(fieldMetadata.settings)) { throw new FieldMetadataException( 'Relation settings are required', FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, @@ -163,8 +164,10 @@ export class FieldMetadataResolver { type: fieldMetadata.settings.relationType, sourceObjectMetadata, targetObjectMetadata, - sourceFieldMetadata, - targetFieldMetadata, + sourceFieldMetadata: + fromFieldMetadataEntityToFieldMetadataDto(sourceFieldMetadata), + targetFieldMetadata: + fromFieldMetadataEntityToFieldMetadataDto(targetFieldMetadata), }; } catch (error) { fieldMetadataGraphqlApiExceptionHandler(error); @@ -198,12 +201,16 @@ export class FieldMetadataResolver { ); } - return morphRelations.map((morphRelation) => ({ + return morphRelations.map((morphRelation) => ({ type: settings.relationType, sourceObjectMetadata: morphRelation.sourceObjectMetadata, targetObjectMetadata: morphRelation.targetObjectMetadata, - sourceFieldMetadata: morphRelation.sourceFieldMetadata, - targetFieldMetadata: morphRelation.targetFieldMetadata, + sourceFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto( + morphRelation.sourceFieldMetadata, + ), + targetFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto( + morphRelation.targetFieldMetadata, + ), })); } catch (error) { fieldMetadataGraphqlApiExceptionHandler(error); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts index 602e8dacf..d3a5d16a2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/__tests__/before-update-one-field.hook.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { i18n } from '@lingui/core'; import { UpdateOneInputType } from '@ptc-org/nestjs-query-graphql'; +import { FieldMetadataType } from 'twenty-shared/types'; import { ForbiddenError, @@ -11,6 +12,7 @@ import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dto import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; jest.mock('@lingui/core', () => ({ i18n: { @@ -96,14 +98,23 @@ describe('BeforeUpdateOneField', () => { }, }; - const mockField: Partial = { + const mockField = getMockFieldMetadataEntity({ + workspaceId: mockWorkspaceId, + objectMetadataId: '20202020-0000-0000-0000-000000000002', id: mockFieldId, + type: FieldMetadataType.TEXT, + name: 'oldName', + label: 'Old Name', + isNullable: true, isCustom: true, - }; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + }) as FieldMetadataEntity; jest .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); + .mockResolvedValue(mockField); const result = await hook.run( instance as UpdateOneInputType, @@ -124,14 +135,23 @@ describe('BeforeUpdateOneField', () => { }, }; - const mockField: Partial = { + const mockField = getMockFieldMetadataEntity({ + workspaceId: mockWorkspaceId, + objectMetadataId: '20202020-0000-0000-0000-000000000002', id: mockFieldId, + type: FieldMetadataType.TEXT, + name: 'oldName', + label: 'Old Name', + isNullable: true, isCustom: false, - }; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + }) as FieldMetadataEntity; jest .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); + .mockResolvedValue(mockField); await expect( hook.run(instance as UpdateOneInputType, { @@ -149,15 +169,24 @@ describe('BeforeUpdateOneField', () => { }, }; - const mockField: Partial = { + const mockField = getMockFieldMetadataEntity({ + workspaceId: mockWorkspaceId, + objectMetadataId: '20202020-0000-0000-0000-000000000002', id: mockFieldId, + type: FieldMetadataType.TEXT, + name: 'oldName', + label: 'Old Name', + isNullable: true, isCustom: false, isActive: true, - }; + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + }) as FieldMetadataEntity; jest .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); + .mockResolvedValue(mockField); const result = await hook.run( instance as UpdateOneInputType, @@ -186,18 +215,26 @@ describe('BeforeUpdateOneField', () => { }, }; - const mockField: Partial = { + const mockField = getMockFieldMetadataEntity({ + workspaceId: mockWorkspaceId, + objectMetadataId: '20202020-0000-0000-0000-000000000002', id: mockFieldId, + type: FieldMetadataType.TEXT, + name: 'oldName', + label: 'Old Name', + isNullable: true, isCustom: false, isLabelSyncedWithName: false, standardOverrides: { label: 'Custom Label', }, - }; + createdAt: new Date(), + updatedAt: new Date(), + }) as FieldMetadataEntity; jest .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); + .mockResolvedValue(mockField); const result = await hook.run( instance as UpdateOneInputType, @@ -228,16 +265,23 @@ describe('BeforeUpdateOneField', () => { }, }; - const mockField: Partial = { + const mockField = getMockFieldMetadataEntity({ + workspaceId: mockWorkspaceId, + objectMetadataId: '20202020-0000-0000-0000-000000000002', id: mockFieldId, + type: FieldMetadataType.TEXT, + name: 'oldName', + label: 'Default Label', + isNullable: true, isCustom: false, isLabelSyncedWithName: false, - label: 'Default Label', - }; + createdAt: new Date(), + updatedAt: new Date(), + }) as FieldMetadataEntity; jest .spyOn(fieldMetadataService, 'findOneWithinWorkspace') - .mockResolvedValue(mockField as FieldMetadataEntity); + .mockResolvedValue(mockField); const result = await hook.run( instance as UpdateOneInputType, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts index f25374350..ab60f2f1a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook.ts @@ -207,7 +207,7 @@ export class BeforeUpdateOneField update: StandardFieldUpdate; overrideKey: 'label' | 'description' | 'icon'; newValue: string; - originalValue: string; + originalValue: string | null; locale?: keyof typeof APP_LOCALES | undefined; }): boolean { // Handle localized overrides @@ -238,7 +238,7 @@ export class BeforeUpdateOneField update: StandardFieldUpdate, overrideKey: 'label' | 'description' | 'icon', newValue: string, - originalValue: string, + originalValue: string | null, locale: keyof typeof APP_LOCALES, ): boolean { const messageId = generateMessageId(originalValue ?? ''); @@ -268,7 +268,7 @@ export class BeforeUpdateOneField update: StandardFieldUpdate, overrideKey: 'label' | 'description' | 'icon', newValue: string, - originalValue: string, + originalValue: string | null, ): boolean { if (newValue !== originalValue) { return false; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts index 210e84150..6685a308b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts @@ -1,39 +1,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; -import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface'; -import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; - -export interface FieldMetadataInterface< +export type FieldMetadataInterface< T extends FieldMetadataType = FieldMetadataType, -> { - id: string; - type: T; - name: string; - label: string; - defaultValue?: FieldMetadataDefaultValue; - options?: FieldMetadataOptions; - settings?: FieldMetadataSettings; - objectMetadataId: string; - workspaceId?: string; - description?: string; - icon?: string; - isNullable: boolean; - isUnique?: boolean; - relationTargetFieldMetadataId?: string; - relationTargetFieldMetadata?: FieldMetadataInterface; - relationTargetObjectMetadataId?: string; - relationTargetObjectMetadata?: ObjectMetadataInterface; - relation?: RelationDTO; - isCustom?: boolean; - isSystem?: boolean; - isActive?: boolean; - generatedType?: 'STORED' | 'VIRTUAL'; - asExpression?: string; - isLabelSyncedWithName: boolean; - createdAt: Date; - updatedAt: Date; -} +> = FieldMetadataEntity; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts index 680fca7a0..033f97f8c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts @@ -57,8 +57,8 @@ export class FieldMetadataRelatedRecordsService { ); const { created, updated, deleted } = this.getOptionsDifferences( - oldFieldMetadata.options, - newFieldMetadata.options, + oldFieldMetadata.options ?? [], + newFieldMetadata.options ?? [], ); const viewGroupRepository = @@ -175,9 +175,15 @@ export class FieldMetadataRelatedRecordsService { } const viewFilterOptions = viewFilterValue - .map((value) => - oldFieldMetadata.options.find((option) => option.value === value), - ) + .map((value) => { + if (!isDefined(oldFieldMetadata.options)) { + return undefined; + } + + return oldFieldMetadata.options.find( + (option) => option.value === value, + ); + }) .filter(isDefined); const afterDeleteViewFilterOptions = viewFilterOptions.filter( @@ -247,8 +253,12 @@ export class FieldMetadataRelatedRecordsService { } public getOptionsDifferences( - oldOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], - newOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], + rawOldOptions: + | (FieldMetadataDefaultOption | FieldMetadataComplexOption)[] + | null, + rawNewOptions: + | (FieldMetadataDefaultOption | FieldMetadataComplexOption)[] + | null, compareLabel = false, ): GetOptionsDifferences { const differences: Differences< @@ -259,6 +269,9 @@ export class FieldMetadataRelatedRecordsService { deleted: [], }; + const oldOptions = rawOldOptions ?? []; + const newOptions = rawNewOptions ?? []; + const oldOptionsMap = new Map(oldOptions.map((opt) => [opt.id, opt])); for (const newOption of newOptions) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts index 991bd7b69..ba8c47165 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service.ts @@ -91,6 +91,7 @@ export class FieldMetadataRelationService { label: relationCreationPayload.targetFieldLabel, icon: relationCreationPayload.targetFieldIcon, workspaceId: fieldMetadataInput.workspaceId, + defaultValue: fieldMetadataInput.defaultValue, }); const targetFieldMetadataToCreateWithRelation = diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index c31ae175c..a02700119 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -39,6 +39,7 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field- import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; @@ -190,8 +191,8 @@ export class FieldMetadataService extends TypeOrmQueryService; + relationTargetObjectMetadataId: string; + relationTargetObjectMetadata: TypeOrmRelation; +}; + +type NotDefinedRelationRecord = { + relationTargetFieldMetadataId: never; + relationTargetFieldMetadata: never; + relationTargetObjectMetadataId: never; + relationTargetObjectMetadata: never; +}; + +type UUIDFieldMetadata = FieldMetadataEntity; + +type TextFieldMetadata = FieldMetadataEntity; + +type NumberFieldMetadata = FieldMetadataEntity; + +type BooleanFieldMetadata = FieldMetadataEntity; + +type DateFieldMetadata = FieldMetadataEntity; + +type DateTimeFieldMetadata = FieldMetadataEntity; + +type CurrencyFieldMetadata = FieldMetadataEntity; + +type FullNameFieldMetadata = FieldMetadataEntity; + +type RatingFieldMetadata = FieldMetadataEntity; + +type SelectFieldMetadata = FieldMetadataEntity; + +type MultiSelectFieldMetadata = + FieldMetadataEntity; + +type PositionFieldMetadata = FieldMetadataEntity; + +type RawJsonFieldMetadata = FieldMetadataEntity; + +type RichTextFieldMetadata = FieldMetadataEntity; + +type ActorFieldMetadata = FieldMetadataEntity; + +type ArrayFieldMetadata = FieldMetadataEntity; + +type PhonesFieldMetadata = FieldMetadataEntity; + +type EmailsFieldMetadata = FieldMetadataEntity; + +type LinksFieldMetadata = FieldMetadataEntity; + +type RelationFieldMetadata = FieldMetadataEntity; + +type MorphRelationFieldMetadata = + FieldMetadataEntity; + +// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars +type Assertions = [ + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + + Expect>, + Expect>, +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts index efcea1f48..809c66927 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/assert-does-not-nullify-default-value-for-non-nullable-field.util.ts @@ -7,7 +7,7 @@ export const assertDoesNotNullifyDefaultValueForNonNullableField = ({ isNullable, defaultValueFromUpdate, }: { - isNullable: boolean; + isNullable: boolean | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any defaultValueFromUpdate?: any; }) => { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util.ts new file mode 100644 index 000000000..1774e7a9f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util.ts @@ -0,0 +1,31 @@ +import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; + +export const fromFieldMetadataEntityToFieldMetadataDto = ( + fieldMetadataEntity: FieldMetadataEntity, +): FieldMetadataDTO => { + const { + createdAt, + updatedAt, + description, + icon, + standardOverrides, + isNullable, + isUnique, + settings, + ...rest + } = fieldMetadataEntity; + + return { + ...rest, + // Should we ? seems to be typed a dateString from classValidator, should be typed as string in TypeScript ? + createdAt: new Date(createdAt), + updatedAt: new Date(updatedAt), + description: description ?? undefined, + icon: icon ?? undefined, + standardOverrides: standardOverrides ?? undefined, + isNullable: isNullable ?? false, + isUnique: isUnique ?? false, + settings: settings ?? undefined, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts index 34e584729..1831353c4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util.ts @@ -9,7 +9,8 @@ export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity< >; export const isSelectOrMultiSelectFieldMetadata = ( fieldMetadata: FieldMetadataInterface, -): fieldMetadata is SelectOrMultiSelectFieldMetadataEntity => { +): fieldMetadata is FieldMetadataInterface & + SelectOrMultiSelectFieldMetadataEntity => { return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes( fieldMetadata.type, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 2f35973ad..7a57bf0cc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -11,6 +11,7 @@ import { QueryRunner, Repository, } from 'typeorm'; +import { FieldMetadataType } from 'twenty-shared/types'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -46,6 +47,7 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { ObjectMetadataEntity } from './object-metadata.entity'; @@ -471,9 +473,15 @@ export class ObjectMetadataService extends TypeOrmQueryService field.id); - const relationMetadataIds = objectMetadata.fields - .map((field) => field.relationTargetFieldMetadata?.id) - .filter(isDefined); + const relationMetadataIds = objectMetadata.fields.flatMap((field) => { + if ( + isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION) + ) { + return field.relationTargetFieldMetadata.id; + } + + return []; + }); await fieldMetadataRepository.delete({ id: In(fieldMetadataIds.concat(relationMetadataIds)), diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts index 89cd5aa15..9f1bb7357 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts @@ -4,12 +4,11 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; import { In, Repository } from 'typeorm'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { fieldTextMock, objectMetadataItemMock, } from 'src/engine/api/__mocks__/object-metadata-item.mock'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; @@ -21,6 +20,7 @@ import { import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('FieldPermissionService', () => { let service: FieldPermissionService; @@ -31,9 +31,9 @@ describe('FieldPermissionService', () => { let workspacePermissionsCacheService: jest.Mocked; let workspaceCacheStorageService: jest.Mocked; - const testWorkspaceId = 'test-workspace-id'; - const testRoleId = 'test-role-id'; - const testObjectMetadataId = 'test-object-metadata-id'; + const testWorkspaceId = '20202020-0000-0000-0000-000000000000'; + const testRoleId = '20202020-0000-0000-0000-000000000001'; + const testObjectMetadataId = '20202020-0000-0000-0000-000000000002'; const testFieldMetadataId = fieldTextMock.id; const mockRole: RoleEntity = { @@ -121,11 +121,13 @@ describe('FieldPermissionService', () => { [testObjectMetadataId]: { ...objectMetadataItemMock, fieldsById: { - [fieldTextMock.id]: { + [fieldTextMock.id]: getMockFieldMetadataEntity({ ...fieldTextMock, label: 'Test Field', objectMetadataId: testObjectMetadataId, - } as FieldMetadataInterface, + workspaceId: testWorkspaceId, + id: '20202020-0000-0000-0000-000000000003', + }) as FieldMetadataEntity, }, fieldIdByJoinColumnName: {}, fieldIdByName: {}, diff --git a/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts b/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts index db6c7609d..ad50595a9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/search-vector/search-vector.service.ts @@ -5,8 +5,6 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { QueryRunner, Repository } from 'typeorm'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; @@ -14,7 +12,10 @@ import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/inde import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; +import { + TsVectorColumnActionFactory, + TsVectorFieldMetadata, +} from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { WorkspaceMigrationColumnActionType, @@ -23,6 +24,7 @@ import { import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { computeTableName } from 'src/engine/utils/compute-table-name.util'; +import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { FieldTypeAndNameMetadata, @@ -66,6 +68,17 @@ export class SearchVectorService { isNullable: true, }); + if ( + !isFieldMetadataEntityOfType( + searchVectorFieldMetadata, + FieldMetadataType.TS_VECTOR, + ) + ) { + throw new Error( + 'Should never occur, created searchVectorFieldMetadata is not a TS_VECTOR field', + ); + } + const searchableFieldForCustomObject = createdObjectMetadata.labelIdentifierFieldMetadataId ? createdObjectMetadata.fields.find( @@ -94,7 +107,6 @@ export class SearchVectorService { action: WorkspaceMigrationTableActionType.ALTER, columns: this.tsVectorColumnActionFactory.handleCreateAction({ ...searchVectorFieldMetadata, - defaultValue: undefined, generatedType: 'STORED', asExpression: getTsVectorColumnExpressionFromFields([ { @@ -102,8 +114,7 @@ export class SearchVectorService { name: searchableFieldForCustomObject.name, }, ]), - options: undefined, - } as FieldMetadataInterface), + }), }, ], queryRunner, @@ -159,8 +170,8 @@ export class SearchVectorService { fieldMetadataNameAndTypeForSearch, ), generatedType: 'STORED', // Not stored on fieldMetadata - options: undefined, - }, + options: null, + } as TsVectorFieldMetadata, ), }, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts index a772c8de7..605bbd322 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts @@ -72,7 +72,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory & { + generatedType?: 'STORED' | 'VIRTUAL'; + asExpression?: string; + }; +export type TsVectorFieldMetadataType = FieldMetadataType.TS_VECTOR; @Injectable() export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory { protected readonly logger = new Logger(TsVectorColumnActionFactory.name); handleCreateAction( - fieldMetadata: FieldMetadataInterface, + fieldMetadata: TsVectorFieldMetadata, ): WorkspaceMigrationColumnCreate[] { return [ { @@ -37,8 +42,8 @@ export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory, - alteredFieldMetadata: FieldMetadataInterface, + currentFieldMetadata: TsVectorFieldMetadata, + alteredFieldMetadata: TsVectorFieldMetadata, ): WorkspaceMigrationColumnAlter[] { return [ { diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts index 1d8f73631..98ed1fa46 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts @@ -11,7 +11,10 @@ import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/worksp import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; import { MorphRelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory'; import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory'; -import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; +import { + TsVectorColumnActionFactory, + TsVectorFieldMetadata, +} from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { WorkspaceMigrationColumnAction, WorkspaceMigrationColumnActionType, @@ -115,9 +118,9 @@ export class WorkspaceMigrationFactory { ]); } - createColumnActions( + createColumnActions( action: WorkspaceMigrationColumnActionType.CREATE, - fieldMetadata: FieldMetadataInterface, + fieldMetadata: FieldMetadataInterface, ): WorkspaceMigrationColumnAction[]; createColumnActions( @@ -126,6 +129,12 @@ export class WorkspaceMigrationFactory { alteredFieldMetadata: FieldMetadataInterface, ): WorkspaceMigrationColumnAction[]; + createColumnActions( + action: WorkspaceMigrationColumnActionType.ALTER, + currentFieldMetadata: FieldMetadataInterface, + alteredFieldMetadata: TsVectorFieldMetadata, + ): WorkspaceMigrationColumnAction[]; + createColumnActions( action: | WorkspaceMigrationColumnActionType.CREATE diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts index f99cd222e..20aae952f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts @@ -7,6 +7,7 @@ import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/works import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils'; +import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { WorkspaceEntityManager } from './workspace-entity-manager'; @@ -88,7 +89,7 @@ describe('WorkspaceEntityManager', () => { }, fieldIdByName: { fieldName: 'field-id' }, fieldIdByJoinColumnName: {}, - }, + } as unknown as ObjectMetadataItemWithFieldMaps, }, idByNameSingular: { 'test-entity': 'test-entity-id', diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts index 620746cd4..901584e57 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts @@ -67,7 +67,7 @@ export class EntitySchemaColumnFactory { entitySchemaColumnMap[joinColumnName] = { name: joinColumnName, type: 'uuid', - nullable: fieldMetadata.isNullable, + nullable: fieldMetadata.isNullable ?? false, }; continue; @@ -92,7 +92,7 @@ export class EntitySchemaColumnFactory { type: columnType as ColumnType, // TODO: We should double check that primary: key === 'id', - nullable: fieldMetadata.isNullable, + nullable: fieldMetadata.isNullable ?? false, createDate: key === 'createdAt', updateDate: key === 'updatedAt', deleteDate: key === 'deletedAt', diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts index 670e193aa..6e0db3a9a 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-delete-query-builder.ts @@ -56,7 +56,7 @@ export class WorkspaceDeleteQueryBuilder< ) as this; } - override async execute(): Promise { + override async execute(): Promise { validateQueryIsPermittedOrThrow( this.expressionMap, this.objectRecordsPermissions, diff --git a/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts b/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts index d442499c0..43ff1f748 100644 --- a/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts +++ b/packages/twenty-server/src/engine/utils/convert-object-metadata-to-schema-properties.util.ts @@ -130,7 +130,7 @@ export const convertObjectMetadataToSchemaProperties = ({ type: 'array', items: { type: 'string', - enum: field.options.map( + enum: (field.options ?? []).map( (option: { value: string }) => option.value, ), }, @@ -139,7 +139,9 @@ export const convertObjectMetadataToSchemaProperties = ({ case FieldMetadataType.SELECT: itemProperty = { type: 'string', - enum: field.options.map((option: { value: string }) => option.value), + enum: (field.options ?? []).map( + (option: { value: string }) => option.value, + ), }; break; case FieldMetadataType.ARRAY: @@ -153,7 +155,9 @@ export const convertObjectMetadataToSchemaProperties = ({ case FieldMetadataType.RATING: itemProperty = { type: 'string', - enum: field.options.map((option: { value: string }) => option.value), + enum: (field.options ?? []).map( + (option: { value: string }) => option.value, + ), }; break; case FieldMetadataType.LINKS: diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts index 15d5fd54d..8df84f8fa 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/services/field-metadata-health.service.ts @@ -172,7 +172,16 @@ export class FieldMetadataHealthService { serializeDefaultValue(`'${option.value}'`), ); - if (!enumValues.includes(columnDefaultValue)) { + if (!isDefined(enumValues)) { + issues.push({ + type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, + fieldMetadata, + columnStructure, + message: `Column options of ${fieldMetadata.name} are not defined`, + }); + } + + if (isDefined(enumValues) && !enumValues.includes(columnDefaultValue)) { issues.push({ type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, fieldMetadata, @@ -247,8 +256,17 @@ export class FieldMetadataHealthService { }); } + if (!isDefined(fieldMetadata.options)) { + issues.push({ + type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID, + fieldMetadata, + message: `Column options of ${fieldMetadata.name} are not defined`, + }); + } + if ( isEnumFieldMetadataType(fieldMetadata.type) && + isDefined(fieldMetadata.options) && !validateOptionsForType(fieldMetadata.type, fieldMetadata.options) ) { issues.push({ @@ -300,7 +318,7 @@ export class FieldMetadataHealthService { }); } else { metadataDefaultValue.forEach((value) => { - if (!enumValues.includes(value)) { + if (isDefined(enumValues) && !enumValues.includes(value)) { issues.push({ type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, fieldMetadata, @@ -309,7 +327,10 @@ export class FieldMetadataHealthService { } }); } - } else if (enumValues.includes(metadataDefaultValue as string)) { + } else if ( + isDefined(enumValues) && + !enumValues.includes(metadataDefaultValue as string) + ) { issues.push({ type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, fieldMetadata, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/utils/__tests__/workspace-migration-index.factory.util.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/utils/__tests__/workspace-migration-index.factory.util.spec.ts index 7c80bba03..0f403759a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/utils/__tests__/workspace-migration-index.factory.util.spec.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/utils/__tests__/workspace-migration-index.factory.util.spec.ts @@ -2,17 +2,30 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; -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 { createIndexMigration } from 'src/engine/workspace-manager/workspace-migration-builder/factories/utils/workspace-migration-index.factory.utils'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('WorkspaceMigrationIndexFactory', () => { it('should create index migrations for simple fields', async () => { + const simpleField = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: 'f1', + type: FieldMetadataType.TEXT, + name: 'simpleField', + label: 'Simple Field', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), + }); + const objectMetadata = { - id: 'obj1', - workspaceId: 'ws1', + id: '20202020-0000-0000-0000-000000000002', + workspaceId: '20202020-0000-0000-0000-000000000000', nameSingular: 'Test', - fields: [{ id: 'f1', name: 'simpleField', type: FieldMetadataType.TEXT }], + fields: [simpleField], isCustom: false, }; const indexMetadata = { @@ -39,25 +52,28 @@ describe('WorkspaceMigrationIndexFactory', () => { }); it('should create index migrations for relation fields', async () => { - const fieldMetadata: Pick< - FieldMetadataEntity, - 'id' | 'name' | 'type' | 'settings' | 'isCustom' - > = { + const relationField = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', id: 'f2', - name: 'author', type: FieldMetadataType.RELATION, + name: 'author', + label: 'Author', + isNullable: true, + isLabelSyncedWithName: true, + createdAt: new Date(), + updatedAt: new Date(), settings: { relationType: RelationType.MANY_TO_ONE, joinColumnName: 'authorId', }, - isCustom: false, - }; + }); const objectMetadata = { - id: 'obj2', - workspaceId: 'ws1', + id: '20202020-0000-0000-0000-000000000003', + workspaceId: '20202020-0000-0000-0000-000000000000', nameSingular: 'Attachment', - fields: [fieldMetadata], + fields: [relationField], isCustom: false, }; const indexMetadata = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock.ts index 781fea151..34b56ff29 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock.ts @@ -11,13 +11,14 @@ type FlatFieldMetadataOverrides< Partial>; export const getFlatFieldMetadataMock = < - T extends FieldMetadataType = FieldMetadataType, + T extends FieldMetadataType = FieldMetadataType.TEXT, >( overrides: FlatFieldMetadataOverrides, ): FlatFieldMetadata => { const createdAt = faker.date.anytime(); return { + type: FieldMetadataType.TEXT as T, createdAt, description: 'default flat field metadata description', icon: 'icon', @@ -28,15 +29,18 @@ export const getFlatFieldMetadataMock = < label: 'flat field metadata label', isNullable: true, isUnique: false, - relationTargetFieldMetadataId: undefined, - relationTargetObjectMetadataId: undefined, - type: FieldMetadataType.TEXT as T, isLabelSyncedWithName: false, isSystem: false, - standardId: undefined, + standardId: null, standardOverrides: undefined, updatedAt: createdAt, workspaceId: faker.string.uuid(), + defaultValue: null, + options: null, + relationTargetFieldMetadata: undefined as never, + relationTargetFieldMetadataId: undefined as never, + relationTargetObjectMetadata: undefined as never, + relationTargetObjectMetadataId: undefined as never, ...overrides, }; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata.ts index 55919e45d..d0e02114a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata.ts @@ -11,8 +11,6 @@ type FieldMetadataEntityRelationProperties = >; export type FlatFieldMetadata = - Partial< - Omit, FieldMetadataEntityRelationProperties> - > & { + Omit, FieldMetadataEntityRelationProperties> & { uniqueIdentifier: string; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap index 4b1bbe25c..76d589d56 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/__snapshots__/workspace-migration-builder.spec.ts.snap @@ -7,6 +7,7 @@ exports[`Workspace migration builder field actions test suite It should build a { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -19,9 +20,12 @@ exports[`Workspace migration builder field actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -36,6 +40,7 @@ exports[`Workspace migration builder field actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -48,9 +53,12 @@ exports[`Workspace migration builder field actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -59,6 +67,7 @@ exports[`Workspace migration builder field actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -71,9 +80,12 @@ exports[`Workspace migration builder field actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -82,6 +94,7 @@ exports[`Workspace migration builder field actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -94,9 +107,12 @@ exports[`Workspace migration builder field actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -105,6 +121,7 @@ exports[`Workspace migration builder field actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -117,9 +134,12 @@ exports[`Workspace migration builder field actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -128,6 +148,7 @@ exports[`Workspace migration builder field actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -140,9 +161,12 @@ exports[`Workspace migration builder field actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -184,6 +208,7 @@ exports[`Workspace migration builder field actions test suite It should build an { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -196,9 +221,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -213,6 +241,7 @@ exports[`Workspace migration builder field actions test suite It should build an "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -225,9 +254,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -269,6 +301,7 @@ exports[`Workspace migration builder field actions test suite It should build an { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -281,9 +314,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": Any, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "RELATION", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -298,6 +334,7 @@ exports[`Workspace migration builder field actions test suite It should build an "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -310,9 +347,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": Any, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "RELATION", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -354,6 +394,7 @@ exports[`Workspace migration builder field actions test suite It should build an { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "new description", "icon": "new icon", "id": Any, @@ -366,9 +407,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "new name", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -383,6 +427,7 @@ exports[`Workspace migration builder field actions test suite It should build an "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -395,9 +440,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -406,6 +454,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -418,9 +467,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -429,6 +481,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -441,9 +494,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -452,6 +508,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -464,9 +521,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -475,6 +535,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -487,9 +548,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -498,6 +562,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "new description", "icon": "new icon", "id": Any, @@ -510,9 +575,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "new name", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -576,6 +644,7 @@ exports[`Workspace migration builder field actions test suite It should build an { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "new description", "icon": "icon", "id": Any, @@ -588,9 +657,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "new label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": Any, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "RELATION", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -605,6 +677,7 @@ exports[`Workspace migration builder field actions test suite It should build an "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -617,9 +690,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -628,6 +704,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -640,9 +717,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -651,6 +731,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -663,9 +744,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -674,6 +758,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -686,9 +771,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -697,6 +785,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -709,9 +798,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -720,6 +812,7 @@ exports[`Workspace migration builder field actions test suite It should build an }, { "createdAt": Any, + "defaultValue": null, "description": "new description", "icon": "icon", "id": Any, @@ -732,9 +825,12 @@ exports[`Workspace migration builder field actions test suite It should build an "label": "new label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": Any, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": Any, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "RELATION", "uniqueIdentifier": "field-metadata-unique-identifier-1", @@ -924,6 +1020,7 @@ exports[`Workspace migration builder object actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -936,9 +1033,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -947,6 +1047,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -959,9 +1060,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -970,6 +1074,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -982,9 +1087,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -993,6 +1101,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1005,9 +1114,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1016,6 +1128,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1028,9 +1141,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -1067,6 +1183,7 @@ exports[`Workspace migration builder object actions test suite It should build a { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1079,9 +1196,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -1096,6 +1216,7 @@ exports[`Workspace migration builder object actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1108,9 +1229,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -1119,6 +1243,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1131,9 +1256,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -1142,6 +1270,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1154,9 +1283,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -1165,6 +1297,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1177,9 +1310,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1188,6 +1324,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1200,9 +1337,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -1239,6 +1379,7 @@ exports[`Workspace migration builder object actions test suite It should build a { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1251,9 +1392,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -1268,6 +1412,7 @@ exports[`Workspace migration builder object actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1280,9 +1425,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -1291,6 +1439,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1303,9 +1452,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -1314,6 +1466,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1326,9 +1479,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -1337,6 +1493,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1349,9 +1506,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1360,6 +1520,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1372,9 +1533,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -1411,6 +1575,7 @@ exports[`Workspace migration builder object actions test suite It should build a { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1423,9 +1588,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -1440,6 +1608,7 @@ exports[`Workspace migration builder object actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1452,9 +1621,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -1463,6 +1635,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1475,9 +1648,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -1486,6 +1662,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1498,9 +1675,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -1509,6 +1689,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1521,9 +1702,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1532,6 +1716,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1544,9 +1729,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -1583,6 +1771,7 @@ exports[`Workspace migration builder object actions test suite It should build a { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1595,9 +1784,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1612,6 +1804,7 @@ exports[`Workspace migration builder object actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1624,9 +1817,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -1635,6 +1831,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1647,9 +1844,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -1658,6 +1858,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1670,9 +1871,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -1681,6 +1885,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1693,9 +1898,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1704,6 +1912,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1716,9 +1925,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -1755,6 +1967,7 @@ exports[`Workspace migration builder object actions test suite It should build a { "flatFieldMetadata": { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1767,9 +1980,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", @@ -1784,6 +2000,7 @@ exports[`Workspace migration builder object actions test suite It should build a "flatFieldMetadatas": [ { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1796,9 +2013,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_0", @@ -1807,6 +2027,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1819,9 +2040,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_1", @@ -1830,6 +2054,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1842,9 +2067,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_2", @@ -1853,6 +2081,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1865,9 +2094,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_3", @@ -1876,6 +2108,7 @@ exports[`Workspace migration builder object actions test suite It should build a }, { "createdAt": Any, + "defaultValue": null, "description": "default flat field metadata description", "icon": "icon", "id": Any, @@ -1888,9 +2121,12 @@ exports[`Workspace migration builder object actions test suite It should build a "label": "flat field metadata label", "name": "flatFieldMetadataName", "objectMetadataId": Any, + "options": null, + "relationTargetFieldMetadata": undefined, "relationTargetFieldMetadataId": undefined, + "relationTargetObjectMetadata": undefined, "relationTargetObjectMetadataId": undefined, - "standardId": undefined, + "standardId": null, "standardOverrides": undefined, "type": "TEXT", "uniqueIdentifier": "field_4", diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts index 4161a5dd8..4e0adfae3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/common/workspace-migration-builder-field-test-case.ts @@ -104,20 +104,19 @@ const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [ context: { input: () => { const objectMetadataId = faker.string.uuid(); - const updatedFieldMetadata = - getFlatFieldMetadataMock({ - uniqueIdentifier: 'field-metadata-unique-identifier-1', - objectMetadataId, - type: FieldMetadataType.RELATION, - settings: { - relationType: RelationType.MANY_TO_ONE, - isForeignKey: true, - joinColumnName: 'column-name', - onDelete: undefined, - }, - relationTargetFieldMetadataId: faker.string.uuid(), - relationTargetObjectMetadataId: faker.string.uuid(), - }); + const updatedFieldMetadata = getFlatFieldMetadataMock({ + uniqueIdentifier: 'field-metadata-unique-identifier-1', + objectMetadataId, + type: FieldMetadataType.RELATION, + settings: { + relationType: RelationType.MANY_TO_ONE, + isForeignKey: true, + joinColumnName: 'column-name', + onDelete: undefined, + }, + relationTargetFieldMetadataId: faker.string.uuid(), + relationTargetObjectMetadataId: faker.string.uuid(), + }); const flatObjectMetadata = getFlatObjectMetadataMock({ uniqueIdentifier: 'object-metadata-unique-identifier-1', isLabelSyncedWithName: true, @@ -130,7 +129,7 @@ const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [ { ...flatObjectMetadata, flatFieldMetadatas: [ - { + getFlatFieldMetadataMock({ ...updatedFieldMetadata, settings: { relationType: RelationType.ONE_TO_MANY, @@ -140,7 +139,7 @@ const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [ }, relationTargetFieldMetadataId: faker.string.uuid(), relationTargetObjectMetadataId: faker.string.uuid(), - }, + }), ], }, ], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts index be47ed085..9ff9a023c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator.ts @@ -195,6 +195,8 @@ export class WorkspaceFieldRelationComparator { throw new Error(`Field ${fieldId} not found in standardObjectMetadata`); } + const relationFieldMetadata = propertiesMap[fieldId]; + if (relationTypeChange) { result.push({ action: ComparatorAction.DELETE, @@ -204,9 +206,11 @@ export class WorkspaceFieldRelationComparator { result.push({ action: ComparatorAction.CREATE, object: { - ...propertiesMap[fieldId], + ...relationFieldMetadata, id: originalFieldMetadata.id, standardId: standardFieldMetadata.standardId ?? undefined, + description: relationFieldMetadata.description ?? undefined, + icon: relationFieldMetadata.icon ?? undefined, }, }); } else if (allOldPropertiesAreNull) { @@ -216,6 +220,8 @@ export class WorkspaceFieldRelationComparator { ...propertiesMap[fieldId], id: originalFieldMetadata.id, standardId: standardFieldMetadata.standardId ?? undefined, + description: relationFieldMetadata.description ?? undefined, + icon: relationFieldMetadata.icon ?? undefined, }, }); } else if (allNewPropertiesAreNull) { @@ -230,6 +236,8 @@ export class WorkspaceFieldRelationComparator { ...propertiesMap[fieldId], id: originalFieldMetadata.id, standardId: standardFieldMetadata.standardId ?? undefined, + description: relationFieldMetadata.description ?? undefined, + icon: relationFieldMetadata.icon ?? undefined, }, }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts index 06332755b..4285ded7c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts @@ -143,8 +143,8 @@ export class StandardFieldFactory { icon: workspaceFieldMetadataArgs.icon, label: workspaceFieldMetadataArgs.label, description: workspaceFieldMetadataArgs.description, - defaultValue: workspaceFieldMetadataArgs.defaultValue, - options: workspaceFieldMetadataArgs.options, + defaultValue: workspaceFieldMetadataArgs.defaultValue ?? null, + options: workspaceFieldMetadataArgs.options ?? null, settings: workspaceFieldMetadataArgs.settings, workspaceId: context.workspaceId, isNullable: workspaceFieldMetadataArgs.isNullable, @@ -155,6 +155,10 @@ export class StandardFieldFactory { asExpression: workspaceFieldMetadataArgs.asExpression, generatedType: workspaceFieldMetadataArgs.generatedType, isLabelSyncedWithName: workspaceFieldMetadataArgs.isLabelSyncedWithName, + relationTargetFieldMetadata: null, + relationTargetFieldMetadataId: null, + relationTargetObjectMetadata: null, + relationTargetObjectMetadataId: null, }, ]; } @@ -195,6 +199,12 @@ export class StandardFieldFactory { isActive: workspaceRelationMetadataArgs.isActive ?? true, isLabelSyncedWithName: workspaceRelationMetadataArgs.isLabelSyncedWithName, + defaultValue: null, + options: null, + relationTargetFieldMetadata: null, + relationTargetFieldMetadataId: null, + relationTargetObjectMetadata: null, + relationTargetObjectMetadataId: null, }); return fieldMetadataCollection; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts index 4701cfaa3..ee13a2491 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts @@ -5,6 +5,7 @@ import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-o import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +// Should get deprecated in favor of the FlatFieldMetadata export type PartialFieldMetadata< T extends FieldMetadataType = FieldMetadataType, > = Omit< @@ -15,6 +16,15 @@ export type PartialFieldMetadata< | 'objectMetadataId' | 'createdAt' | 'updatedAt' + | 'standardId' + | 'icon' + | 'isSystem' + | 'workspaceId' + | 'isActive' + | 'asExpression' + | 'indexFieldMetadatas' + | 'fieldPermissions' + | 'object' > & { standardId: string; label: string | ((objectMetadata: ObjectMetadataEntity) => string); @@ -24,8 +34,8 @@ export type PartialFieldMetadata< workspaceId: string; objectMetadataId?: string; isActive?: boolean; - asExpression?: string; - generatedType?: 'STORED' | 'VIRTUAL'; + asExpression?: string; // not accurate + generatedType?: 'STORED' | 'VIRTUAL'; // not accurate }; export type PartialComputedFieldMetadata = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts index 8710fb70a..a3ce8568d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/compute-standard-fields.util.ts @@ -48,6 +48,12 @@ export const computeStandardFields = ( defaultValue: null, isNullable: true, isLabelSyncedWithName: true, + isUnique: null, + options: null, + relationTargetFieldMetadata: null, + relationTargetFieldMetadataId: null, + relationTargetObjectMetadata: null, + relationTargetObjectMetadataId: null, }); } } else { diff --git a/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts b/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts index ae2eb77e9..34ab6e78b 100644 --- a/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts +++ b/packages/twenty-server/src/modules/timeline/repositories/timeline-activity.repository.ts @@ -13,17 +13,27 @@ export class TimelineActivityRepository { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} - async upsertOne( - name: string, - properties: Partial, - objectName: string, - recordId: string, - workspaceId: string, - workspaceMemberId?: string, - linkedRecordCachedName?: string, - linkedRecordId?: string, - linkedObjectMetadataId?: string, - ) { + async upsertOne({ + name, + objectName, + properties, + recordId, + workspaceId, + linkedObjectMetadataId, + linkedRecordCachedName, + linkedRecordId, + workspaceMemberId, + }: { + name: string; + properties: Partial; + objectName: string; + recordId: string; + workspaceId: string; + workspaceMemberId?: string; + linkedRecordCachedName?: string; + linkedRecordId?: string; + linkedObjectMetadataId: string | null; + }) { const recentTimelineActivity = await this.findRecentTimelineActivity( name, objectName, @@ -58,17 +68,17 @@ export class TimelineActivityRepository { ); } - return this.insertTimelineActivity( + return this.insertTimelineActivity({ name, properties, objectName, recordId, workspaceMemberId, - linkedRecordCachedName ?? '', + linkedRecordCachedName: linkedRecordCachedName ?? '', linkedRecordId, linkedObjectMetadataId, workspaceId, - ); + }); } private async findRecentTimelineActivity( @@ -131,17 +141,27 @@ export class TimelineActivityRepository { }); } - private async insertTimelineActivity( - name: string, - properties: Partial, - objectName: string, - recordId: string, - workspaceMemberId: string | undefined, - linkedRecordCachedName: string, - linkedRecordId: string | undefined, - linkedObjectMetadataId: string | undefined, - workspaceId: string, - ) { + private async insertTimelineActivity({ + linkedObjectMetadataId, + linkedRecordCachedName, + linkedRecordId, + name, + objectName, + properties, + recordId, + workspaceId, + workspaceMemberId, + }: { + name: string; + properties: Partial; + objectName: string; + recordId: string; + workspaceMemberId: string | undefined; + linkedRecordCachedName: string; + linkedRecordId: string | undefined; + linkedObjectMetadataId: string | null; + workspaceId: string; + }) { const timelineActivityTypeORMRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, diff --git a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts index dcfc5c212..dd1c44ba2 100644 --- a/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts +++ b/packages/twenty-server/src/modules/timeline/services/timeline-activity.service.ts @@ -14,7 +14,7 @@ type TimelineActivity = Omit & { objectName?: string; linkedRecordCachedName?: string; linkedRecordId?: string; - linkedObjectMetadataId?: string; + linkedObjectMetadataId?: string | null; // eslint-disable-next-line @typescript-eslint/no-explicit-any properties: Record; // more relaxed conditions than for internal events }; @@ -50,17 +50,28 @@ export class TimelineActivityService { if (!timelineActivities || timelineActivities.length === 0) return; for (const timelineActivity of timelineActivities) { - await this.timelineActivityRepository.upsertOne( - timelineActivity.name, - timelineActivity.properties, - timelineActivity.objectName ?? event.objectMetadata.nameSingular, - timelineActivity.recordId, + const { + name, + properties, + recordId, + linkedObjectMetadataId, + linkedRecordCachedName, + linkedRecordId, + objectName, + workspaceMemberId, + } = timelineActivity; + + await this.timelineActivityRepository.upsertOne({ + linkedObjectMetadataId: linkedObjectMetadataId ?? null, + name, + objectName: objectName ?? event.objectMetadata.nameSingular, + properties, + recordId, workspaceId, - timelineActivity.workspaceMemberId, - timelineActivity.linkedRecordCachedName, - timelineActivity.linkedRecordId, - timelineActivity.linkedObjectMetadataId, - ); + linkedRecordCachedName, + linkedRecordId, + workspaceMemberId, + }); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts index f184c6773..19bc0b956 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -56,44 +56,80 @@ describe('generateFakeFormResponse', () => { objectMetadataMaps: mockObjectMetadataMaps, }); - expect(result).toEqual({ - name: { - isLeaf: true, - label: 'Name', - type: FieldMetadataType.TEXT, - value: 'My text', - icon: undefined, - }, - age: { - isLeaf: true, - label: 'Age', - type: FieldMetadataType.NUMBER, - value: 20, - icon: undefined, - }, - company: { - isLeaf: false, - label: 'Company', - value: { - _outputSchemaType: 'RECORD', - fields: {}, - object: { - isLeaf: true, - label: 'Company', - fieldIdName: 'id', - icon: 'test-company-icon', - nameSingular: 'company', - value: 'A company', + expect(result).toMatchInlineSnapshot(` +{ + "age": { + "icon": undefined, + "isLeaf": true, + "label": "Age", + "type": "NUMBER", + "value": 20, + }, + "company": { + "isLeaf": false, + "label": "Company", + "value": { + "_outputSchemaType": "RECORD", + "fields": { + "domainName": { + "icon": "test-field-icon", + "isLeaf": false, + "label": "Domain Name", + "type": "LINKS", + "value": { + "primaryLinkLabel": { + "isLeaf": true, + "label": "Primary Link Label", + "type": "TEXT", + "value": "My text", + }, + "primaryLinkUrl": { + "isLeaf": true, + "label": "Primary Link Url", + "type": "TEXT", + "value": "My text", + }, + "secondaryLinks": { + "isLeaf": true, + "label": "Secondary Links", + "type": "RAW_JSON", + "value": null, + }, }, }, + "name": { + "icon": "test-field-icon", + "isLeaf": true, + "label": "Name", + "type": "TEXT", + "value": "My text", + }, }, - date: { - isLeaf: true, - label: 'Date', - type: FieldMetadataType.DATE, - value: 'mm/dd/yyyy', - icon: undefined, + "object": { + "fieldIdName": "id", + "icon": "test-company-icon", + "isLeaf": true, + "label": "Company", + "nameSingular": "company", + "value": "A company", }, - }); + }, + }, + "date": { + "icon": undefined, + "isLeaf": true, + "label": "Date", + "type": "DATE", + "value": "mm/dd/yyyy", + }, + "name": { + "icon": undefined, + "isLeaf": true, + "label": "Name", + "type": "TEXT", + "value": "My text", + }, +} +`); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts index 2a44d6670..949ec36dc 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/should-generate-field-fake-value.spec.ts @@ -1,71 +1,100 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; +import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('shouldGenerateFieldFakeValue', () => { it('should return true for active non-system fields', () => { - const field = { + const field = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000002', isSystem: false, isActive: true, type: FieldMetadataType.TEXT, name: 'testField', + label: 'Test Field', + isNullable: true, + isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - } as FieldMetadataInterface; + }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); it('should return true for system id field', () => { - const field = { + const field = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000003', isSystem: true, isActive: true, type: FieldMetadataType.UUID, name: 'id', + label: 'ID', + isNullable: false, + isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - } as FieldMetadataInterface; + }); expect(shouldGenerateFieldFakeValue(field)).toBe(true); }); it('should return false for inactive fields', () => { - const field = { + const field = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000004', isSystem: false, isActive: false, type: FieldMetadataType.TEXT, name: 'testField', + label: 'Test Field', + isNullable: true, + isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - } as FieldMetadataInterface; + }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); it('should return false for system fields (except id)', () => { - const field = { + const field = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000005', isSystem: true, isActive: true, type: FieldMetadataType.TEXT, name: 'testField', + label: 'Test Field', + isNullable: true, + isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - } as FieldMetadataInterface; + }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); it('should return false for relation fields', () => { - const field = { + const field = getMockFieldMetadataEntity({ + workspaceId: '20202020-0000-0000-0000-000000000000', + objectMetadataId: '20202020-0000-0000-0000-000000000001', + id: '20202020-0000-0000-0000-000000000006', isSystem: false, isActive: true, type: FieldMetadataType.RELATION, name: 'testField', + label: 'Test Field', + isNullable: true, + isLabelSyncedWithName: true, createdAt: new Date(), updatedAt: new Date(), - } as FieldMetadataInterface; + }); expect(shouldGenerateFieldFakeValue(field)).toBe(false); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts index 78d2dafe4..54f24aca3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields.ts @@ -28,7 +28,7 @@ export const generateObjectRecordFields = ({ acc[field.name] = generateFakeField({ type: field.type, label: field.label, - icon: field.icon, + icon: field.icon ?? undefined, }); return acc; @@ -49,7 +49,7 @@ export const generateObjectRecordFields = ({ acc[field.name] = { isLeaf: false, - icon: field.icon, + icon: field.icon ?? undefined, label: field.label, value: generateFakeObjectRecord({ objectMetadataInfo: { diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts index aa61a1618..94a5b4847 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value.ts @@ -1,18 +1,19 @@ import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; - import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -const isManyToOneRelationField = (field: FieldMetadataInterface) => +const isManyToOneRelationField = (field: FieldMetadataEntity) => (field as FieldMetadataEntity).settings ?.relationType === 'MANY_TO_ONE'; -export const shouldGenerateFieldFakeValue = (field: FieldMetadataInterface) => { +// TODO refactor +export const shouldGenerateFieldFakeValue = ( + field: FieldMetadataEntity, +) => { return ( field.isActive && (!field.isSystem || field.name === 'id' || field.name === 'userEmail') && (field.type !== FieldMetadataType.RELATION || - isManyToOneRelationField(field)) + isManyToOneRelationField(field as unknown as FieldMetadataEntity)) ); }; diff --git a/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts b/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts new file mode 100644 index 000000000..ab18d0148 --- /dev/null +++ b/packages/twenty-server/src/utils/__test__/get-field-metadata-entity.mock.ts @@ -0,0 +1,47 @@ +import { faker } from '@faker-js/faker'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; + +type GetMockFieldMetadataEntityOverride< + T extends FieldMetadataType = FieldMetadataType, +> = Partial> & + Required, 'workspaceId' | 'objectMetadataId'>>; + +export const getMockFieldMetadataEntity = < + T extends FieldMetadataType = FieldMetadataType.TEXT, +>( + overrides: GetMockFieldMetadataEntityOverride, +): FieldMetadataEntity => { + return { + type: FieldMetadataType.TEXT as T, + fieldPermissions: [], + icon: null, + indexFieldMetadatas: {} as IndexFieldMetadataEntity, + isCustom: true, + isLabelSyncedWithName: false, + isNullable: null, + isSystem: false, + isUnique: null, + object: {} as ObjectMetadataEntity, + relationTargetFieldMetadata: null as never, + relationTargetFieldMetadataId: null as never, + relationTargetObjectMetadata: null as never, + relationTargetObjectMetadataId: null as never, + standardId: null, + standardOverrides: null, + id: faker.string.uuid(), + name: 'defaultFieldMetadataName', + label: 'Default field metadata entity label', + description: 'Default field metadata entity description', + defaultValue: null, + options: null, + settings: null, + createdAt: new Date(), + updatedAt: new Date(), + isActive: true, + ...overrides, + }; +}; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/create-one-field-metadata-enum.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/create-one-field-metadata-enum.integration-spec.ts index 40b4c7316..e04fd6849 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/create-one-field-metadata-enum.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/create-one-field-metadata-enum.integration-spec.ts @@ -71,7 +71,7 @@ describe.each(fieldMetadataEnumTypes)( expect(data.createOneField.type).toEqual(testedFieldMetadataType); const createdOptions = data.createOneField.options; - const optionsToCompare = expectedOptions ?? input.options; + const optionsToCompare = expectedOptions ?? input.options ?? []; expect(errors).toBeUndefined(); expect(createdOptions?.length).toBe(optionsToCompare.length); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/update-one-enum-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/update-one-enum-field-metadata.integration-spec.ts index d4fad2e46..221c82ba1 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/update-one-enum-field-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/enum/update-one-enum-field-metadata.integration-spec.ts @@ -168,12 +168,19 @@ describe.each(fieldMetadataEnumTypes)( expect(data.updateOneField).toBeDefined(); const updatedOptions: | FieldMetadataComplexOption[] - | FieldMetadataDefaultOption[] = data.updateOneField.options; + | FieldMetadataDefaultOption[] + | null = data.updateOneField.options; + + expect(updatedOptions).toBeDefined(); + if (!isDefined(updatedOptions)) + throw new Error( + 'Should never occur, type invariant post test assertion', + ); expect(errors).toBeUndefined(); updatedOptions.forEach((option) => expect(option.id).toBeDefined()); - const optionsToCompare = expectedOptions ?? input.options; + const optionsToCompare = expectedOptions ?? input.options ?? []; expect(updatedOptions.length).toBe(optionsToCompare.length); expect(updatedOptions).toMatchObject(optionsToCompare); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util.ts index ff7528658..f2e770272 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util.ts @@ -8,14 +8,14 @@ import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perf import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; import { FieldMetadataType } from 'twenty-shared/types'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; export const createOneFieldMetadata = async ({ input, gqlFields, expectToFail = false, }: PerformMetadataQueryParams): CommonResponseBody<{ - createOneField: FieldMetadataInterface; + createOneField: FieldMetadataDTO; }> => { const graphqlOperation = createOneFieldMetadataQueryFactory({ input, diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/relation/delete-one-object-metadata-with-relation.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/relation/delete-one-object-metadata-with-relation.integration-spec.ts index 424834df3..fb090ae4e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/relation/delete-one-object-metadata-with-relation.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/relation/delete-one-object-metadata-with-relation.integration-spec.ts @@ -1,16 +1,16 @@ -import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; import { EachTestingContext } from 'twenty-shared/testing'; import { FieldMetadataType } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; +import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util'; -import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; + type DeleteOneObjectMetadataItemTestingContext = EachTestingContext< (args: { objectMetadataIdToDelete: string; relationFieldId: string }) => { objectMetadataIdToDelete: string; @@ -44,11 +44,12 @@ const successfulDeleteTargetUseCase: DeleteOneObjectMetadataItemTestingContext = describe('Delete Object metadata with relation should succeed', () => { let createdObjectMetadataPersonId = ''; let createdObjectMetadataOpportunityId = ''; - let createdRelationField: FieldMetadataInterface; let globalTestContext: { opportunityMetadataId: string; personMetadataId: string; - relationField: FieldMetadataInterface; + relationField: FieldMetadataDTO & { + relation: RelationDTO; + }; }; beforeEach(async () => { @@ -56,7 +57,7 @@ describe('Delete Object metadata with relation should succeed', () => { data: { createOneObject: { id: objectMetadataPersonId }, }, - } = await createOneObjectMetadata({ + } = await forceCreateOneObjectMetadata({ input: { nameSingular: 'personForRelation', namePlural: 'peopleForRelation', @@ -72,7 +73,7 @@ describe('Delete Object metadata with relation should succeed', () => { data: { createOneObject: { id: objectMetadataOpportunityId }, }, - } = await createOneObjectMetadata({ + } = await forceCreateOneObjectMetadata({ input: { nameSingular: 'opportunityForRelation', namePlural: 'opportunitiesForRelation', @@ -84,24 +85,20 @@ describe('Delete Object metadata with relation should succeed', () => { createdObjectMetadataOpportunityId = objectMetadataOpportunityId; - createdRelationField = - await createRelationBetweenObjects({ - objectMetadataId: createdObjectMetadataOpportunityId, - targetObjectMetadataId: createdObjectMetadataPersonId, - type: FieldMetadataType.RELATION, - relationType: RelationType.MANY_TO_ONE, - }); - globalTestContext = { opportunityMetadataId: createdObjectMetadataOpportunityId, personMetadataId: createdObjectMetadataPersonId, - relationField: createdRelationField, + relationField: + await createRelationBetweenObjects({ + objectMetadataId: createdObjectMetadataOpportunityId, + targetObjectMetadataId: createdObjectMetadataPersonId, + type: FieldMetadataType.RELATION, + relationType: RelationType.MANY_TO_ONE, + }), }; }); + afterEach(async () => { - await deleteOneFieldMetadata({ - input: { idToDelete: createdRelationField.id }, - }); await deleteOneObjectMetadata({ input: { idToDelete: createdObjectMetadataPersonId }, }); @@ -128,14 +125,9 @@ describe('Delete Object metadata with relation should succeed', () => { }); it.each(successfulDeleteTargetUseCase)('$title', async ({ context }) => { - if (!isDefined(globalTestContext.relationField.relation)) { - throw new Error('Relation field relation is undefined'); - } - const computedContext = context({ objectMetadataIdToDelete: globalTestContext.opportunityMetadataId, - relationFieldId: - globalTestContext.relationField.relation.targetFieldMetadata.id, + relationFieldId: globalTestContext.relationField.id, }); await deleteOneObjectMetadata({ diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util.ts index ef84b479a..9d0d047ab 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util.ts @@ -4,6 +4,9 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; +import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; + export const createRelationBetweenObjects = async < T extends FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION, >({ @@ -41,22 +44,35 @@ export const createRelationBetweenObjects = async < }, }; - const { - data: { createOneField: createdFieldPerson }, - } = await createOneFieldMetadata({ + const { data } = await createOneFieldMetadata({ input: createFieldInput, gqlFields: ` id name label isLabelSyncedWithName + settings relation { type + sourceObjectMetadata { + id + nameSingular + namePlural + } + targetObjectMetadata { + id + nameSingular + namePlural + } + sourceFieldMetadata { + id + name + } targetFieldMetadata { id + name } } - settings object { id nameSingular @@ -65,5 +81,7 @@ export const createRelationBetweenObjects = async < expectToFail: false, }); - return createdFieldPerson; + return data.createOneField as unknown as FieldMetadataDTO & { + relation: RelationDTO; + }; }; diff --git a/packages/twenty-shared/src/testing/EachTestingContextFilter.ts b/packages/twenty-shared/src/testing/EachTestingContextFilter.ts index 5edf01bdd..c67d1d9e0 100644 --- a/packages/twenty-shared/src/testing/EachTestingContextFilter.ts +++ b/packages/twenty-shared/src/testing/EachTestingContextFilter.ts @@ -6,6 +6,7 @@ export const eachTestingContextFilter = ( const onlyTestsCases = testCases.filter((testCase) => testCase.only === true); if (process.env.CI && onlyTestsCases.length > 0) { + // eslint-disable-next-line no-console console.warn( 'Should never push tests cases with an only to true, only to use in dev env\n returning the whole test suite anyway', ); diff --git a/packages/twenty-shared/src/testing/index.ts b/packages/twenty-shared/src/testing/index.ts index 512d92d86..36c26e241 100644 --- a/packages/twenty-shared/src/testing/index.ts +++ b/packages/twenty-shared/src/testing/index.ts @@ -10,3 +10,8 @@ export { eachTestingContextFilter } from './EachTestingContextFilter'; export type { EachTestingContext } from './types/EachTestingContext.type'; export type { SuccessfulAndFailingTestCases } from './types/SuccessfulAndFailingTestCases'; +export type { + Expect, + Equal, + HasAllProperties, +} from './types/TestingGenerics.type'; diff --git a/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts b/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts new file mode 100644 index 000000000..4fec6332f --- /dev/null +++ b/packages/twenty-shared/src/testing/types/TestingGenerics.type.ts @@ -0,0 +1,59 @@ +/** + * Type testing utilities for TypeScript + * @module TypeTesting + */ + +/** + * A type utility for testing TypeScript type assertions + * + * @template T - The actual type to test + */ +export type Expect = T; + +/** + * Tests if two types are exactly equal + * + * @template A - First type to compare + * @template B - Second type to compare + */ +export type Equal = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 + ? true + : false; + +/** + * Tests if a type has all required properties of another type + * Works with both class types and regular object types + * + * @template T - Type that should contain the properties + * @template U - Type whose properties should be contained in T + */ +export type HasAllProperties = [T] extends [new (...args: any[]) => any] + ? HasAllProperties, U> + : [U] extends [new (...args: any[]) => any] + ? HasAllProperties> + : { + [K in keyof U]: K extends keyof T ? Equal : false; + }[keyof U] extends true + ? true + : false; + +class TestClass { + id!: string; + name!: string; +} + +// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars +type BasicTests = [ + Expect>, + Expect>, + Expect>, + + Expect>, + Expect>, + + Expect>, + + Expect>, + Expect>, +];