diff --git a/.cursor/rules/creating-syncable-entity.mdc b/.cursor/rules/creating-syncable-entity.mdc index 61d5a49f5b..eb24e519b5 100644 --- a/.cursor/rules/creating-syncable-entity.mdc +++ b/.cursor/rules/creating-syncable-entity.mdc @@ -27,6 +27,7 @@ Examples of existing syncable entities: `skill`, `agent`, `view`, `viewField`, ` 3. [Step-by-Step Implementation](#step-by-step-implementation) - [Step 1: Add Metadata Name Constant](#step-1-add-metadata-name-constant-twenty-shared) - [Step 2: Create TypeORM Entity](#step-2-create-typeorm-entity) + - [Step 2b: Using JsonbProperty and SerializedRelation Types](#step-2b-using-jsonbproperty-and-serializedrelation-types) - [Step 3: Define Flat Entity Type](#step-3-define-flat-entity-type) - [Step 4: Define Editable Properties](#step-4-define-editable-properties) - [Step 5: Register in Central Constants](#step-5-register-in-central-constants) @@ -237,6 +238,160 @@ export abstract class WorkspaceRelatedEntity { --- +### Step 2b: Using JsonbProperty and SerializedRelation Types + +When your entity has JSONB columns or stores foreign key references inside JSONB structures, you must use the branded type wrappers to enable automatic universal identifier mapping. + +#### JsonbProperty Wrapper + +Wrap all JSONB column types with `JsonbProperty` to mark them for the universal entity transformation system: + +```typescript +import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; + +@Entity('myEntity') +export class MyEntityEntity extends SyncableEntity { + // Simple JSONB column - wrap the type + @Column({ type: 'jsonb', nullable: true }) + settings: JsonbProperty | null; + + // JSONB column with complex type + @Column({ type: 'jsonb', nullable: false }) + configuration: JsonbProperty; + + // Array stored as JSONB + @Column({ type: 'jsonb', nullable: true }) + tags: JsonbProperty | null; +} +``` + +**When to use `JsonbProperty`:** +- Any column with `type: 'jsonb'` that stores an object or array +- Configuration objects, settings, metadata blobs +- Any structured data stored as JSON in the database + +**What it enables:** +- The type system can identify which properties are JSONB columns +- Automatic transformation of serialized relations within JSONB structures +- Type-safe universal entity mapping + +#### SerializedRelation Type + +Use `SerializedRelation` for properties **inside JSONB structures** that store foreign key references (entity IDs): + +```typescript +import { SerializedRelation } from 'twenty-shared/types'; +import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; + +// Define the JSONB structure type +type MyEntityConfiguration = { + name: string; + // This stores a reference to another field's ID - use SerializedRelation + targetFieldMetadataId: SerializedRelation; + // This stores a reference to an object's ID + sourceObjectMetadataId: SerializedRelation; + // Regular string - NOT a foreign key reference + displayFormat: string; +}; + +@Entity('myEntity') +export class MyEntityEntity extends SyncableEntity { + @Column({ type: 'jsonb', nullable: false }) + configuration: JsonbProperty; +} +``` + +**When to use `SerializedRelation`:** +- Properties inside JSONB that store UUIDs referencing other entities +- Foreign key relationships that can't use TypeORM relations (because they're in JSONB) +- Any `*Id` property inside a JSONB structure that references another metadata entity + +**What it enables:** +- Automatic renaming from `*Id` to `*UniversalIdentifier` in universal entities +- Type-safe extraction of serialized relation properties +- Proper handling during workspace sync/migration + +#### Complete Example + +```typescript +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { SerializedRelation } from 'twenty-shared/types'; + +import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; + +// JSONB structure with serialized relations +type WidgetConfiguration = { + title: string; + // Foreign keys stored in JSONB - use SerializedRelation + fieldMetadataId: SerializedRelation; + objectMetadataId: SerializedRelation; + // Optional foreign key + viewId?: SerializedRelation; + // Regular properties (not foreign keys) + displayMode: 'compact' | 'expanded'; + maxItems: number; +}; + +type GridPosition = { + row: number; + column: number; + width: number; + height: number; +}; + +@Entity('widget') +export class WidgetEntity extends SyncableEntity implements Required { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ nullable: true, type: 'uuid' }) + standardId: string | null; + + @Column({ nullable: false }) + name: string; + + // JSONB column with serialized relations - wrap with JsonbProperty + @Column({ type: 'jsonb', nullable: false }) + configuration: JsonbProperty; + + // JSONB column without serialized relations - still wrap with JsonbProperty + @Column({ type: 'jsonb', nullable: false }) + gridPosition: JsonbProperty; + + @Column({ default: false }) + isCustom: boolean; + + // ... other columns +} +``` + +**Result in Universal Entity:** + +When transformed to a universal entity, the `configuration` property will have its `SerializedRelation` fields automatically renamed: + +```typescript +// Original (in database/flat entity) +{ + fieldMetadataId: "abc-123", + objectMetadataId: "def-456", + viewId: "ghi-789", + displayMode: "compact", + maxItems: 10, +} + +// Transformed (in universal entity) +{ + fieldMetadataUniversalIdentifier: "abc-123", + objectMetadataUniversalIdentifier: "def-456", + viewUniversalIdentifier: "ghi-789", + displayMode: "compact", + maxItems: 10, +} +``` + +--- + ### Step 3: Define Flat Entity Type **File:** `src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts` @@ -1143,6 +1298,11 @@ Before considering your syncable entity complete, verify: - [ ] Entity has `isCustom` boolean column - [ ] Entity-to-flat transform sets `universalIdentifier` correctly (`standardId || id`) +### JSONB Properties and Serialized Relations +- [ ] All JSONB columns are wrapped with `JsonbProperty` +- [ ] Foreign key references inside JSONB structures use `SerializedRelation` type +- [ ] JSONB structure types are properly defined with `SerializedRelation` for `*Id` properties + ### Registration (twenty-shared) - [ ] Metadata name added to `ALL_METADATA_NAME` diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command.ts index 4abda192f9..d01ef3f7f4 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command.ts @@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Command } from 'nest-commander'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; import { - FieldMetadataRelationSettings, + FieldMetadataSettingsMapping, FieldMetadataType, RelationOnDeleteAction, } from 'twenty-shared/types'; @@ -106,7 +106,7 @@ export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspaces } const taskFieldSettings = - taskField.settings as FieldMetadataRelationSettings; + taskField.settings as FieldMetadataSettingsMapping['RELATION']; if (taskFieldSettings?.onDelete === RelationOnDeleteAction.CASCADE) { this.logger.log( @@ -121,7 +121,7 @@ export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspaces ); if (!isDryRun) { - const updatedSettings: FieldMetadataRelationSettings = { + const updatedSettings: FieldMetadataSettingsMapping['RELATION'] = { ...taskFieldSettings, onDelete: RelationOnDeleteAction.CASCADE, }; diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts index df484bd759..bb06119545 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor.ts @@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common'; import { msg } from '@lingui/core/macro'; import { isNull, isUndefined } from '@sniptt/guards'; import { - FieldMetadataFilesSettings, - FieldMetadataRelationSettings, + FieldMetadataSettingsMapping, FieldMetadataType, ObjectRecord, RelationType, @@ -219,7 +218,7 @@ export class DataArgProcessor { case FieldMetadataType.RELATION: case FieldMetadataType.MORPH_RELATION: { const fieldMetadataRelationSettings = - fieldMetadata.settings as FieldMetadataRelationSettings; + fieldMetadata.settings as FieldMetadataSettingsMapping['RELATION']; if ( fieldMetadataRelationSettings.relationType === @@ -252,7 +251,7 @@ export class DataArgProcessor { const validatedValue = validateFilesFieldOrThrow( value, key, - fieldMetadata.settings as FieldMetadataFilesSettings, + fieldMetadata.settings as FieldMetadataSettingsMapping['FILES'], ); return transformRawJsonField(validatedValue); diff --git a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-files-field-or-throw.util.ts b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-files-field-or-throw.util.ts index 7f68251af3..67ae44113f 100644 --- a/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-files-field-or-throw.util.ts +++ b/packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/validator-utils/validate-files-field-or-throw.util.ts @@ -2,8 +2,8 @@ import { inspect } from 'util'; import { msg } from '@lingui/core/macro'; import { isNull } from '@sniptt/guards'; -import { type FieldMetadataFilesSettings } from 'twenty-shared/types'; import { z } from 'zod'; +import { type FieldMetadataSettingsMapping } from 'twenty-shared/types'; import { CommonQueryRunnerException, @@ -24,7 +24,7 @@ export type FileItem = z.infer; export const validateFilesFieldOrThrow = ( value: unknown, fieldName: string, - settings: FieldMetadataFilesSettings, + settings: FieldMetadataSettingsMapping['FILES'], ): FileItem[] | null => { if (isNull(value)) return null; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts index f1628214a3..a9199fb9fb 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts @@ -6,7 +6,7 @@ import { QUERY_MAX_RECORDS_FROM_RELATION, } from 'twenty-shared/constants'; import { - FieldMetadataRelationSettings, + FieldMetadataSettingsMapping, FieldMetadataType, ObjectRecord, RelationType, @@ -256,8 +256,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ const relationType = isDryRun && fieldMetadata.type === FieldMetadataType.RELATION - ? (fieldMetadata.settings as FieldMetadataRelationSettings) - ?.relationType + ? ( + fieldMetadata.settings as FieldMetadataSettingsMapping['RELATION'] + )?.relationType : undefined; mergedResult[fieldName] = mergeFieldValues( @@ -376,7 +377,7 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ } const relationSettings = field.settings as - | FieldMetadataRelationSettings + | FieldMetadataSettingsMapping['RELATION'] | undefined; if ( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface.ts index b60d741e13..531112adad 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface.ts @@ -1,7 +1,8 @@ import { type GraphQLScalarType } from 'graphql'; -import { type FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { + type FieldMetadataType, + type FieldMetadataDefaultValue, +} from 'twenty-shared/types'; import { type GqlInputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/enums/gql-input-type-definition-kind.enum'; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts index ad071e28e6..6b387f534e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts @@ -19,11 +19,10 @@ import { type FieldMetadataSettings, FieldMetadataType, NumberDataType, + type FieldMetadataDefaultValue, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum'; import { 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 8e98f8b253..ae94efc3c6 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 @@ -53,7 +53,7 @@ describe('computeSchemaComponents', () => { "description": "Object description", "example": { "fieldCurrency": { - "amountMicros": 284000000, + "amountMicros": "284000000", "currencyCode": "EUR", }, "fieldEmails": { @@ -554,7 +554,7 @@ describe('computeSchemaComponents', () => { "description": "Object description", "example": { "fieldCurrency": { - "amountMicros": 253000000, + "amountMicros": "253000000", "currencyCode": "EUR", }, "fieldEmails": { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 0a1f03422f..3caf54cd98 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -1,8 +1,10 @@ import { type OpenAPIV3_1 } from 'openapi-types'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + type FieldMetadataDefaultValue, + FieldMetadataType, +} from 'twenty-shared/types'; import { capitalize } from 'twenty-shared/utils'; -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { generateRandomFieldValue } from 'src/engine/core-modules/open-api/utils/generate-random-field-value.util'; 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 75e346dfeb..d9dd8b25f3 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 @@ -1,10 +1,11 @@ import { faker } from '@faker-js/faker'; -import { FieldMetadataType } from 'twenty-shared/types'; +import { + type FieldMetadataDefaultValue, + FieldMetadataType, +} from 'twenty-shared/types'; import { assertUnreachable, isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; @@ -65,7 +66,7 @@ export const generateRandomFieldValue = ({ case FieldMetadataType.CURRENCY: { return { - amountMicros: faker.number.int({ min: 100, max: 1_000 }) * 1_000_000, + amountMicros: `${faker.number.int({ min: 100, max: 1_000 }) * 1_000_000}`, currencyCode: 'EUR', }; } @@ -131,7 +132,6 @@ export const generateRandomFieldValue = ({ case FieldMetadataType.ACTOR: { return { source: 'MANUAL', - context: {}, name: faker.person.fullName(), workspaceMemberId: null, }; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/entities/agent.entity.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/entities/agent.entity.ts index 3b4255cc29..ea570eae6e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/entities/agent.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/entities/agent.entity.ts @@ -15,6 +15,7 @@ import { ModelId, } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; @Entity('agent') @Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt']) @@ -52,7 +53,7 @@ export class AgentEntity // Should not be nullable @Column({ nullable: true, type: 'jsonb', default: { type: 'text' } }) - responseFormat: AgentResponseFormat; + responseFormat: JsonbProperty; @Column({ default: false }) isCustom: boolean; @@ -67,7 +68,7 @@ export class AgentEntity deletedAt: Date | null; @Column({ nullable: true, type: 'jsonb' }) - modelConfiguration: ModelConfiguration | null; + modelConfiguration: JsonbProperty | null; @Column({ type: 'text', array: true, default: '{}' }) evaluationInputs: string[]; diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts index 282ebe5158..002879f6d4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity.ts @@ -10,6 +10,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @@ -28,7 +29,7 @@ export class CronTriggerEntity id: string; @Column({ nullable: false, type: 'jsonb' }) - settings: CronTriggerSettings; + settings: JsonbProperty; @ManyToOne( () => ServerlessFunctionEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts index c33b984069..9927becacc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity.ts @@ -10,6 +10,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @@ -31,7 +32,7 @@ export class DatabaseEventTriggerEntity id: string; @Column({ nullable: false, type: 'jsonb' }) - settings: DatabaseEventTriggerSettings; + settings: JsonbProperty; @ManyToOne( () => ServerlessFunctionEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts deleted file mode 100644 index 91f99c75d1..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - IsArray, - IsBoolean, - IsDate, - IsNotEmpty, - IsNumber, - IsNumberString, - IsObject, - IsOptional, - IsString, - IsUUID, - Matches, - ValidateIf, -} from 'class-validator'; - -import { IsQuotedString } from 'src/engine/metadata-modules/field-metadata/validators/is-quoted-string.validator'; - -export const fieldMetadataDefaultValueFunctionName = { - UUID: 'uuid', - NOW: 'now', -} as const; - -export type FieldMetadataDefaultValueFunctionNames = - (typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName]; - -export class FieldMetadataDefaultValueString { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - value: string | null; -} - -export class FieldMetadataDefaultValueRawJson { - @ValidateIf((_object, value) => value !== null) - @IsObject() // TODO: Should this also allow arrays? - value: object | null; -} - -export class FieldMetadataDefaultValueRichTextV2 { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - blocknote: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - markdown: string | null; -} - -export class FieldMetadataDefaultValueRichText { - @ValidateIf((_object, value) => value !== null) - @IsString() - value: string | null; -} - -export class FieldMetadataDefaultValueNumber { - @ValidateIf((_object, value) => value !== null) - @IsNumber() - value: number | null; -} - -export class FieldMetadataDefaultValueBoolean { - @ValidateIf((_object, value) => value !== null) - @IsBoolean() - value: boolean | null; -} - -export class FieldMetadataDefaultValueStringArray { - @ValidateIf((_object, value) => value !== null) - @IsArray() - @IsQuotedString({ each: true }) - value: string[] | null; -} - -export class FieldMetadataDefaultValueDateTime { - @ValidateIf((_object, value) => value !== null) - @IsDate() - value: Date | null; -} - -export class FieldMetadataDefaultValueDate { - @ValidateIf((_object, value) => value !== null) - @IsDate() - value: Date | null; -} - -export class FieldMetadataDefaultValueCurrency { - @ValidateIf((_object, value) => value !== null) - @IsNumberString() - amountMicros: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - currencyCode: string | null; -} - -export class FieldMetadataDefaultValueFullName { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - firstName: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - lastName: string | null; -} - -export class FieldMetadataDefaultValueUuidFunction { - @Matches(fieldMetadataDefaultValueFunctionName.UUID) - @IsNotEmpty() - value: typeof fieldMetadataDefaultValueFunctionName.UUID; -} - -export class FieldMetadataDefaultValueNowFunction { - @Matches(fieldMetadataDefaultValueFunctionName.NOW) - @IsNotEmpty() - value: typeof fieldMetadataDefaultValueFunctionName.NOW; -} - -export class FieldMetadataDefaultValueAddress { - @ValidateIf((_object, value) => value !== null) - @IsString() - addressStreet1: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressStreet2: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressCity: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressPostcode: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressState: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressCountry: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsNumber() - addressLat: number | null; - - @ValidateIf((_object, value) => value !== null) - @IsNumber() - addressLng: number | null; -} - -class LinkMetadata { - @IsString() - label: string; - - @IsString() - url: string; -} - -export class FieldMetadataDefaultValueLinks { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryLinkLabel: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryLinkUrl: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsArray() - secondaryLinks: LinkMetadata[] | null; -} - -export class FieldMetadataDefaultActor { - @ValidateIf((_object, value) => value !== null) - @IsString() - source: string; - - @ValidateIf((_object, value) => value !== null) - @IsOptional() - @IsUUID() - workspaceMemberId?: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - name: string; -} - -export class FieldMetadataDefaultValueEmails { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryEmail: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsObject() - additionalEmails: object | null; -} - -export class FieldMetadataDefaultValuePhones { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryPhoneNumber: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryPhoneCountryCode: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryPhoneCallingCode: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsObject() - additionalPhones: object | null; -} - -export class FieldMetadataDefaultArray { - @ValidateIf((_object, value) => value !== null) - @IsArray() - value: string[] | null; -} 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 5e6e25c850..58bf017e2e 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 @@ -27,10 +27,9 @@ import { type FieldMetadataOptions, type FieldMetadataSettings, FieldMetadataType, + type FieldMetadataDefaultValue, } from 'twenty-shared/types'; -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator'; import { FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; 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 c539afac64..c2a047b7da 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,5 @@ import { + FieldMetadataDefaultValue, FieldMetadataOptions, FieldMetadataSettings, FieldMetadataType, @@ -19,8 +20,6 @@ import { UpdateDateColumn, } from 'typeorm'; -import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - import { type FieldStandardOverridesDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-standard-overrides.dto'; import { AssignIfIsGivenFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-if-is-given-field-metadata-type.type'; import { AssignTypeIfIsMorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-type-if-is-morph-or-relation-field-metadata-type.type'; @@ -31,6 +30,7 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; // This entity is used as a reference test case for type utilities in: // Modifying relations or properties may require updating type test expectations for Typecheck to pass. @@ -91,7 +91,7 @@ export class FieldMetadataEntity< label: string; @Column({ nullable: true, type: 'jsonb' }) - defaultValue: FieldMetadataDefaultValue; + defaultValue: JsonbProperty>; @Column({ nullable: true, type: 'text' }) description: string | null; @@ -100,13 +100,13 @@ export class FieldMetadataEntity< icon: string | null; @Column({ type: 'jsonb', nullable: true }) - standardOverrides: FieldStandardOverridesDTO | null; + standardOverrides: JsonbProperty | null; @Column('jsonb', { nullable: true }) - options: FieldMetadataOptions; + options: JsonbProperty>; @Column('jsonb', { nullable: true }) - settings: FieldMetadataSettings; + settings: JsonbProperty>; @Column({ default: false }) isCustom: boolean; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts deleted file mode 100644 index 799781bf5c..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { type FieldMetadataType, type IsExactly } from 'twenty-shared/types'; - -import { - type FieldMetadataDefaultActor, - type FieldMetadataDefaultArray, - type FieldMetadataDefaultValueAddress, - type FieldMetadataDefaultValueBoolean, - type FieldMetadataDefaultValueCurrency, - type FieldMetadataDefaultValueDateTime, - type FieldMetadataDefaultValueEmails, - type FieldMetadataDefaultValueFullName, - type FieldMetadataDefaultValueLinks, - type FieldMetadataDefaultValueNowFunction, - type FieldMetadataDefaultValueNumber, - type FieldMetadataDefaultValuePhones, - type FieldMetadataDefaultValueRawJson, - type FieldMetadataDefaultValueRichText, - type FieldMetadataDefaultValueString, - type FieldMetadataDefaultValueStringArray, - type FieldMetadataDefaultValueUuidFunction, -} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; - -type ExtractValueType = T extends { value: infer V } ? V : T; - -type UnionOfValues = T[keyof T]; - -type FieldMetadataDefaultValueMapping = { - [FieldMetadataType.UUID]: - | FieldMetadataDefaultValueString - | FieldMetadataDefaultValueUuidFunction; - [FieldMetadataType.TEXT]: FieldMetadataDefaultValueString; - [FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones; - [FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails; - [FieldMetadataType.DATE_TIME]: - | FieldMetadataDefaultValueDateTime - | FieldMetadataDefaultValueNowFunction; - [FieldMetadataType.DATE]: - | FieldMetadataDefaultValueDateTime - | FieldMetadataDefaultValueNowFunction; - [FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean; - [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; - [FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber; - [FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString; - [FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks; - [FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency; - [FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName; - [FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress; - [FieldMetadataType.RATING]: FieldMetadataDefaultValueString; - [FieldMetadataType.SELECT]: FieldMetadataDefaultValueString; - [FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray; - [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson; - [FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText; - [FieldMetadataType.ACTOR]: FieldMetadataDefaultActor; - [FieldMetadataType.ARRAY]: FieldMetadataDefaultArray; -}; - -export type FieldMetadataClassValidation = - UnionOfValues; - -export type FieldMetadataFunctionDefaultValue = ExtractValueType< - FieldMetadataDefaultValueUuidFunction | FieldMetadataDefaultValueNowFunction ->; - -export type FieldMetadataDefaultValueForType< - T extends keyof FieldMetadataDefaultValueMapping, -> = ExtractValueType | null; - -export type FieldMetadataDefaultValueForAnyType = ExtractValueType< - UnionOfValues -> | null; - -export type FieldMetadataDefaultValue< - T extends FieldMetadataType = FieldMetadataType, -> = - IsExactly extends true - ? FieldMetadataDefaultValueForAnyType | null // Could be improved to be | unknown - : T extends keyof FieldMetadataDefaultValueMapping - ? FieldMetadataDefaultValueForType - : never | null; - -type FieldMetadataDefaultValueExtractedTypes = { - [K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType< - FieldMetadataDefaultValueMapping[K] - >; -}; - -export type FieldMetadataDefaultSerializableValue = - | FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes] - | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/__tests__/field-metadata-entity.test-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/__tests__/field-metadata-entity.test-type.ts index 1168906237..ef065f251a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/__tests__/field-metadata-entity.test-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/types/__tests__/field-metadata-entity.test-type.ts @@ -1,22 +1,16 @@ import { type Expect, type HasAllProperties } from 'twenty-shared/testing'; import { - type FieldMetadataMultiItemSettings, + type AllFieldMetadataSettings, + type FieldMetadataDefaultValueForAnyType, + type FieldMetadataDefaultValueMapping, + type FieldMetadataOptionForAnyType, + type FieldMetadataSettingsMapping, type FieldMetadataType, type NullablePartial, - type AllFieldMetadataSettings, - type FieldMetadataDateSettings, - type FieldMetadataDateTimeSettings, - type FieldMetadataNumberSettings, - type FieldMetadataRelationSettings, - type FieldMetadataTextSettings, } from 'twenty-shared/types'; import { type Relation as TypeOrmRelation } from 'typeorm'; -import { - type FieldMetadataDefaultValueForAnyType, - type FieldMetadataDefaultValueForType, -} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { type FieldMetadataComplexOption, type FieldMetadataDefaultOption, @@ -138,69 +132,109 @@ type SettingsAssertions = [ Expect< HasAllProperties< TextFieldMetadata, - { settings: FieldMetadataTextSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.TEXT] + >; + } > >, Expect< HasAllProperties< NumberFieldMetadata, - { settings: FieldMetadataNumberSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.NUMBER] + >; + } > >, Expect< HasAllProperties< DateFieldMetadata, - { settings: FieldMetadataDateSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.DATE] + >; + } > >, Expect< HasAllProperties< DateTimeFieldMetadata, - { settings: FieldMetadataDateTimeSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.DATE_TIME] + >; + } > >, Expect< HasAllProperties< ArrayFieldMetadata, - { settings: FieldMetadataMultiItemSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.ARRAY] + >; + } > >, Expect< HasAllProperties< PhonesFieldMetadata, - { settings: FieldMetadataMultiItemSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.PHONES] + >; + } > >, Expect< HasAllProperties< EmailsFieldMetadata, - { settings: FieldMetadataMultiItemSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.EMAILS] + >; + } > >, Expect< HasAllProperties< LinksFieldMetadata, - { settings: FieldMetadataMultiItemSettings | null } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.LINKS] + >; + } > >, Expect< HasAllProperties< RelationFieldMetadata, - { settings: FieldMetadataRelationSettings } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.RELATION] + >; + } > >, Expect< HasAllProperties< MorphRelationFieldMetadata, - { settings: FieldMetadataRelationSettings } + { + settings: JsonbProperty< + FieldMetadataSettingsMapping[FieldMetadataType.MORPH_RELATION] + >; + } > >, Expect< HasAllProperties< AbstractFieldMetadata, - { settings: AllFieldMetadataSettings | null } + { settings: JsonbProperty | null } > >, ]; @@ -210,20 +244,30 @@ type DefaultValueAssertions = [ Expect< HasAllProperties< UUIDFieldMetadata, - { defaultValue: FieldMetadataDefaultValueForType } + { + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.UUID] + >; + } > >, Expect< HasAllProperties< TextFieldMetadata, - { defaultValue: FieldMetadataDefaultValueForType } + { + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.TEXT] + >; + } > >, Expect< HasAllProperties< NumberFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.NUMBER] + >; } > >, @@ -231,21 +275,29 @@ type DefaultValueAssertions = [ HasAllProperties< BooleanFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.BOOLEAN] + >; } > >, Expect< HasAllProperties< DateFieldMetadata, - { defaultValue: FieldMetadataDefaultValueForType } + { + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.DATE] + >; + } > >, Expect< HasAllProperties< DateTimeFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.DATE_TIME] + >; } > >, @@ -253,7 +305,9 @@ type DefaultValueAssertions = [ HasAllProperties< CurrencyFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.CURRENCY] + >; } > >, @@ -261,7 +315,9 @@ type DefaultValueAssertions = [ HasAllProperties< FullNameFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.FULL_NAME] + >; } > >, @@ -269,7 +325,9 @@ type DefaultValueAssertions = [ HasAllProperties< RatingFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.RATING] + >; } > >, @@ -277,7 +335,9 @@ type DefaultValueAssertions = [ HasAllProperties< SelectFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.SELECT] + >; } > >, @@ -285,7 +345,9 @@ type DefaultValueAssertions = [ HasAllProperties< MultiSelectFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.MULTI_SELECT] + >; } > >, @@ -293,7 +355,9 @@ type DefaultValueAssertions = [ HasAllProperties< PositionFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.POSITION] + >; } > >, @@ -301,7 +365,9 @@ type DefaultValueAssertions = [ HasAllProperties< RawJsonFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.RAW_JSON] + >; } > >, @@ -309,7 +375,9 @@ type DefaultValueAssertions = [ HasAllProperties< RichTextFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.RICH_TEXT] + >; } > >, @@ -317,7 +385,9 @@ type DefaultValueAssertions = [ HasAllProperties< ActorFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.ACTOR] + >; } > >, @@ -325,7 +395,9 @@ type DefaultValueAssertions = [ HasAllProperties< ArrayFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.ARRAY] + >; } > >, @@ -333,7 +405,9 @@ type DefaultValueAssertions = [ HasAllProperties< PhonesFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.PHONES] + >; } > >, @@ -341,7 +415,9 @@ type DefaultValueAssertions = [ HasAllProperties< EmailsFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.EMAILS] + >; } > >, @@ -349,7 +425,9 @@ type DefaultValueAssertions = [ HasAllProperties< LinksFieldMetadata, { - defaultValue: FieldMetadataDefaultValueForType; + defaultValue: JsonbProperty< + FieldMetadataDefaultValueMapping[FieldMetadataType.LINKS] + >; } > >, @@ -364,7 +442,9 @@ type DefaultValueAssertions = [ Expect< HasAllProperties< AbstractFieldMetadata, - { defaultValue: FieldMetadataDefaultValueForAnyType | null } + { + defaultValue: JsonbProperty; + } > >, ]; @@ -378,19 +458,19 @@ type OptionsAssertions = [ Expect< HasAllProperties< RatingFieldMetadata, - { options: FieldMetadataDefaultOption[] } + { options: JsonbProperty } > >, Expect< HasAllProperties< SelectFieldMetadata, - { options: FieldMetadataComplexOption[] } + { options: JsonbProperty } > >, Expect< HasAllProperties< MultiSelectFieldMetadata, - { options: FieldMetadataComplexOption[] } + { options: JsonbProperty } > >, @@ -417,9 +497,7 @@ type OptionsAssertions = [ HasAllProperties< AbstractFieldMetadata, { - options: - | null - | (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]); + options: JsonbProperty; } > >, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts deleted file mode 100644 index 8e2bcb1384..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { FieldMetadataType } from 'twenty-shared/types'; - -import { validateDefaultValueForType } from 'src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util'; - -describe('validateDefaultValueForType', () => { - it('should return true for null defaultValue', () => { - expect( - validateDefaultValueForType(FieldMetadataType.TEXT, null).isValid, - ).toBe(true); - }); - - // Dynamic default values - it('should validate uuid dynamic default value for UUID type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.UUID, 'uuid').isValid, - ).toBe(true); - }); - - it('should validate now dynamic default value for DATE_TIME type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.DATE_TIME, 'now').isValid, - ).toBe(true); - }); - - it('should return false for mismatched dynamic default value', () => { - expect( - validateDefaultValueForType(FieldMetadataType.UUID, 'now').isValid, - ).toBe(false); - }); - - // Static default values - it('should validate string default value for TEXT type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.TEXT, "'test'").isValid, - ).toBe(true); - }); - - it('should return false for invalid string default value for TEXT type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.TEXT, 123).isValid, - ).toBe(false); - }); - - it('should validate number default value for NUMBER type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.NUMBER, 100).isValid, - ).toBe(true); - }); - - it('should return false for invalid number default value for NUMBER type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.NUMBER, '100').isValid, - ).toBe(false); - }); - - it('should validate boolean default value for BOOLEAN type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.BOOLEAN, true).isValid, - ).toBe(true); - }); - - it('should return false for invalid boolean default value for BOOLEAN type', () => { - expect( - validateDefaultValueForType(FieldMetadataType.BOOLEAN, 'true').isValid, - ).toBe(false); - }); - - // CURRENCY type - it('should validate CURRENCY default value', () => { - expect( - validateDefaultValueForType(FieldMetadataType.CURRENCY, { - amountMicros: '100', - currencyCode: "'USD'", - }).isValid, - ).toBe(true); - }); - - it('should return false for invalid CURRENCY default value', () => { - expect( - validateDefaultValueForType( - // @ts-expect-error Just for testing purposes - { amountMicros: 100, currencyCode: "'USD'" }, - FieldMetadataType.CURRENCY, - ).isValid, - ).toBe(false); - }); - - // Unknown type - it('should return false for unknown type', () => { - expect( - validateDefaultValueForType('unknown' as FieldMetadataType, "'test'") - .isValid, - ).toBe(false); - }); -}); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts index 2e83cd5d0e..ad3aba4a8f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts @@ -1,6 +1,8 @@ -import { FieldActorSource, FieldMetadataType } from 'twenty-shared/types'; - -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { + FieldActorSource, + type FieldMetadataDefaultValue, + FieldMetadataType, +} from 'twenty-shared/types'; // No need to refactor as unused in workspace migration v2 export function generateDefaultValue( diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util.ts index 785a4ae458..a4275cce30 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util.ts @@ -1,15 +1,12 @@ import { - type FieldMetadataDefaultSerializableValue, - type FieldMetadataFunctionDefaultValue, -} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - -import { + type FieldMetadataDefaultValueForAnyType, type FieldMetadataDefaultValueFunctionNames, + type FieldMetadataFunctionDefaultValue, fieldMetadataDefaultValueFunctionName, -} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; +} from 'twenty-shared/types'; export const isFunctionDefaultValue = ( - defaultValue: FieldMetadataDefaultSerializableValue, + defaultValue: FieldMetadataDefaultValueForAnyType, ): defaultValue is FieldMetadataFunctionDefaultValue => { return ( typeof defaultValue === 'string' && diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-default-value.ts index fa867fa0cf..cdd1abafc6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-default-value.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-default-value.ts @@ -1,4 +1,4 @@ -import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types'; import { FieldMetadataException, @@ -8,7 +8,7 @@ import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metada import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util'; export const serializeDefaultValue = ( - defaultValue?: FieldMetadataDefaultSerializableValue, + defaultValue?: FieldMetadataDefaultValueForAnyType, ) => { if (defaultValue === undefined || defaultValue === null) { return null; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util.ts index 18c3c67130..eb8be8af1b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util.ts @@ -1,4 +1,4 @@ -import { type FieldMetadataFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { type FieldMetadataFunctionDefaultValue } from 'twenty-shared/types'; export const serializeFunctionDefaultValue = ( defaultValue?: FieldMetadataFunctionDefaultValue, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/unserialize-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/unserialize-default-value.ts index c662af3e68..1e5a27956e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/unserialize-default-value.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/unserialize-default-value.ts @@ -1,7 +1,7 @@ -import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types'; export const unserializeDefaultValue = ( - serializedDefaultValue: FieldMetadataDefaultSerializableValue, + serializedDefaultValue: FieldMetadataDefaultValueForAnyType, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any => { if (serializedDefaultValue === undefined || serializedDefaultValue === null) { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts deleted file mode 100644 index 7bd7496804..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { type ValidationError, validateSync } from 'class-validator'; -import { FieldMetadataType } from 'twenty-shared/types'; - -import { - type FieldMetadataClassValidation, - type FieldMetadataDefaultValue, -} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; - -import { - FieldMetadataDefaultActor, - FieldMetadataDefaultValueAddress, - FieldMetadataDefaultValueBoolean, - FieldMetadataDefaultValueCurrency, - FieldMetadataDefaultValueDate, - FieldMetadataDefaultValueDateTime, - FieldMetadataDefaultValueEmails, - FieldMetadataDefaultValueFullName, - FieldMetadataDefaultValueLinks, - FieldMetadataDefaultValueNowFunction, - FieldMetadataDefaultValueNumber, - FieldMetadataDefaultValuePhones, - FieldMetadataDefaultValueRawJson, - FieldMetadataDefaultValueRichTextV2, - FieldMetadataDefaultValueString, - FieldMetadataDefaultValueStringArray, - FieldMetadataDefaultValueUuidFunction, -} from 'src/engine/metadata-modules/field-metadata/dtos/default-value.input'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; - -export const defaultValueValidatorsMap = { - [FieldMetadataType.UUID]: [ - FieldMetadataDefaultValueString, - FieldMetadataDefaultValueUuidFunction, - ], - [FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString], - [FieldMetadataType.DATE_TIME]: [ - FieldMetadataDefaultValueDateTime, - FieldMetadataDefaultValueNowFunction, - ], - [FieldMetadataType.DATE]: [FieldMetadataDefaultValueDate], - [FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean], - [FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber], - [FieldMetadataType.NUMERIC]: [FieldMetadataDefaultValueString], - [FieldMetadataType.CURRENCY]: [FieldMetadataDefaultValueCurrency], - [FieldMetadataType.FULL_NAME]: [FieldMetadataDefaultValueFullName], - [FieldMetadataType.RATING]: [FieldMetadataDefaultValueString], - [FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString], - [FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray], - [FieldMetadataType.ADDRESS]: [FieldMetadataDefaultValueAddress], - [FieldMetadataType.RICH_TEXT_V2]: [FieldMetadataDefaultValueRichTextV2], - [FieldMetadataType.RICH_TEXT]: [FieldMetadataDefaultValueString], - [FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson], - [FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks], - [FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor], - [FieldMetadataType.EMAILS]: [FieldMetadataDefaultValueEmails], - [FieldMetadataType.PHONES]: [FieldMetadataDefaultValuePhones], -}; - -type ValidationResult = { - isValid: boolean; - errors: ValidationError[]; -}; - -export const validateDefaultValueForType = ( - type: FieldMetadataType, - defaultValue: FieldMetadataDefaultValue, -): ValidationResult => { - if (defaultValue === null) { - return { - isValid: true, - errors: [], - }; - } - - // @ts-expect-error legacy noImplicitAny - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const validators = defaultValueValidatorsMap[type] as any[]; - - if (!validators) { - return { - isValid: false, - errors: [], - }; - } - - const validationResults = validators.map((validator) => { - const computedDefaultValue = isCompositeFieldMetadataType(type) - ? defaultValue - : { value: defaultValue }; - - const defaultValueInstance = plainToInstance< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any, - FieldMetadataClassValidation - >(validator, computedDefaultValue as FieldMetadataClassValidation); - - const errors = validateSync(defaultValueInstance, { - whitelist: true, - forbidNonWhitelisted: true, - forbidUnknownValues: true, - }); - - const isValid = errors.length === 0; - - return { - isValid, - errors, - }; - }); - - const isValid = validationResults.some((result) => result.isValid); - - return { - isValid, - errors: validationResults.flatMap((result) => result.errors), - }; -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type.ts index 3b739ef8ca..5f558146ce 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type.ts @@ -1,5 +1,3 @@ -import { type Relation } from 'typeorm'; - import { type AllNonWorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/all-non-workspace-related-entity.type'; import { type WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; @@ -10,7 +8,7 @@ export type ExtractEntityManyToOneEntityRelationProperties< { [P in keyof T]: [NonNullable] extends [never] ? never - : NonNullable extends Relation + : NonNullable extends TTarget ? P : never; }[keyof T] diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type.ts index 16c0b5a2bc..fed59afc32 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/types/extract-entity-one-to-many-entity-relation-properties.type.ts @@ -1,5 +1,3 @@ -import { type Relation } from 'typeorm'; - import { type AllNonWorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/all-non-workspace-related-entity.type'; import { type WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; @@ -9,7 +7,7 @@ export type ExtractEntityOneToManyEntityRelationProperties< > = NonNullable< { [P in keyof T]: NonNullable extends Array - ? U extends Relation + ? U extends TTarget ? P : never : never; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts index 27fa8c8b2e..157bd03248 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/__tests__/generate-morph-or-relation-flat-field-metadata-pair.spec.ts @@ -1,6 +1,6 @@ import { type EachTestingContext } from 'twenty-shared/testing'; import { - type FieldMetadataRelationSettings, + type FieldMetadataSettingsMapping, FieldMetadataType, RelationOnDeleteAction, RelationType, @@ -285,9 +285,9 @@ describe('generate Morph Or Relation Flat Field Metadata Pair test suite', () => input.targetFlatObjectMetadata.id, ); const sourceSettings = - sourceFieldMetadata.settings as FieldMetadataRelationSettings; + sourceFieldMetadata.settings as FieldMetadataSettingsMapping['RELATION']; const targetSettings = - targetFieldMetadata.settings as FieldMetadataRelationSettings; + targetFieldMetadata.settings as FieldMetadataSettingsMapping['RELATION']; expect(sourceSettings.relationType).toBe(expectedSourceRelationType); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts index c525a048e7..b0feaaecce 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts @@ -11,6 +11,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -63,7 +64,7 @@ export class ObjectMetadataEntity icon: string | null; @Column({ type: 'jsonb', nullable: true }) - standardOverrides: ObjectStandardOverridesDTO | null; + standardOverrides: JsonbProperty | null; /** * @deprecated @@ -93,7 +94,7 @@ export class ObjectMetadataEntity isSearchable: boolean; @Column({ type: 'jsonb', nullable: true }) - duplicateCriteria: WorkspaceEntityDuplicateCriteria[] | null; + duplicateCriteria: JsonbProperty | null; @Column({ nullable: true, type: 'varchar' }) shortcut: string | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto.ts index 281ad6e530..909bf7aad1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto.ts @@ -17,6 +17,7 @@ import { } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; import { CalendarStartDay } from 'twenty-shared/constants'; +import { SerializedRelation } from 'twenty-shared/types'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -37,7 +38,7 @@ export class AggregateChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - aggregateFieldMetadataId: string; + aggregateFieldMetadataId: SerializedRelation; @Field(() => AggregateOperations) @IsEnum(AggregateOperations) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto.ts index b63a753a67..816ccdbe76 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto.ts @@ -17,6 +17,7 @@ import { } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; import { CalendarStartDay } from 'twenty-shared/constants'; +import { SerializedRelation } from 'twenty-shared/types'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -41,7 +42,7 @@ export class BarChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - aggregateFieldMetadataId: string; + aggregateFieldMetadataId: SerializedRelation; @Field(() => AggregateOperations) @IsEnum(AggregateOperations) @@ -51,7 +52,7 @@ export class BarChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - primaryAxisGroupByFieldMetadataId: string; + primaryAxisGroupByFieldMetadataId: SerializedRelation; @Field(() => String, { nullable: true }) @IsString() @@ -80,7 +81,7 @@ export class BarChartConfigurationDTO @Field(() => UUIDScalarType, { nullable: true }) @IsUUID() @IsOptional() - secondaryAxisGroupByFieldMetadataId?: string; + secondaryAxisGroupByFieldMetadataId?: SerializedRelation; @Field(() => String, { nullable: true }) @IsString() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto.ts index 1269bf790a..f18d4ed5b1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto.ts @@ -15,6 +15,7 @@ import { } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; import { CalendarStartDay } from 'twenty-shared/constants'; +import { SerializedRelation } from 'twenty-shared/types'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -34,7 +35,7 @@ export class GaugeChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - aggregateFieldMetadataId: string; + aggregateFieldMetadataId: SerializedRelation; @Field(() => AggregateOperations) @IsEnum(AggregateOperations) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto.ts index a28eff2695..c108bf16ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/line-chart-configuration.dto.ts @@ -17,6 +17,7 @@ import { } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; import { CalendarStartDay } from 'twenty-shared/constants'; +import { SerializedRelation } from 'twenty-shared/types'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -39,7 +40,7 @@ export class LineChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - aggregateFieldMetadataId: string; + aggregateFieldMetadataId: SerializedRelation; @Field(() => AggregateOperations) @IsEnum(AggregateOperations) @@ -49,7 +50,7 @@ export class LineChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - primaryAxisGroupByFieldMetadataId: string; + primaryAxisGroupByFieldMetadataId: SerializedRelation; @Field(() => String, { nullable: true }) @IsString() @@ -81,7 +82,7 @@ export class LineChartConfigurationDTO @Field(() => UUIDScalarType, { nullable: true }) @IsUUID() @IsOptional() - secondaryAxisGroupByFieldMetadataId?: string; + secondaryAxisGroupByFieldMetadataId?: SerializedRelation; @Field(() => String, { nullable: true }) @IsString() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto.ts index 435488ae8b..e95923808f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/pie-chart-configuration.dto.ts @@ -16,6 +16,7 @@ import { } from 'class-validator'; import { GraphQLJSON } from 'graphql-type-json'; import { CalendarStartDay } from 'twenty-shared/constants'; +import { SerializedRelation } from 'twenty-shared/types'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -37,7 +38,7 @@ export class PieChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - aggregateFieldMetadataId: string; + aggregateFieldMetadataId: SerializedRelation; @Field(() => AggregateOperations) @IsEnum(AggregateOperations) @@ -47,7 +48,7 @@ export class PieChartConfigurationDTO @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - groupByFieldMetadataId: string; + groupByFieldMetadataId: SerializedRelation; @Field(() => String, { nullable: true }) @IsString() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/ratio-aggregate-config.dto.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/ratio-aggregate-config.dto.ts index 5ee17c1e1c..2260afef6c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/ratio-aggregate-config.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/dtos/ratio-aggregate-config.dto.ts @@ -1,6 +1,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { SerializedRelation } from 'twenty-shared/types'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -9,7 +10,7 @@ export class RatioAggregateConfigDTO { @Field(() => UUIDScalarType) @IsUUID() @IsNotEmpty() - fieldMetadataId: string; + fieldMetadataId: SerializedRelation; @Field(() => String) @IsString() diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts index ad065cf889..344e815050 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity.ts @@ -20,6 +20,7 @@ import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums import { type GridPosition } from 'src/engine/metadata-modules/page-layout-widget/types/grid-position.type'; import { PageLayoutWidgetConfigurationTypeSettings } from 'src/engine/metadata-modules/page-layout-widget/types/page-layout-widget-configuration.type'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; @Entity({ name: 'pageLayoutWidget', schema: 'core' }) @ObjectType('PageLayoutWidget') @@ -70,10 +71,12 @@ export class PageLayoutWidgetEntity< objectMetadata: Relation | null; @Column({ type: 'jsonb', nullable: false }) - gridPosition: GridPosition; + gridPosition: JsonbProperty; @Column({ type: 'jsonb', nullable: false }) - configuration: PageLayoutWidgetConfigurationTypeSettings; + configuration: JsonbProperty< + PageLayoutWidgetConfigurationTypeSettings + >; @CreateDateColumn({ type: 'timestamptz' }) createdAt: Date; diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.entity.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.entity.ts index 9586970c91..724c470d22 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.entity.ts @@ -12,6 +12,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; @@ -44,7 +45,7 @@ export class RouteTriggerEntity httpMethod: HTTPMethod; @Column({ nullable: false, type: 'jsonb', default: [] }) - forwardedRequestHeaders: string[]; + forwardedRequestHeaders: JsonbProperty; @ManyToOne( () => ServerlessFunctionEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts index 07f03b0041..47c175cf4d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity.ts @@ -23,6 +23,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity'; import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; @Entity({ name: 'rowLevelPermissionPredicate', schema: 'core' }) @Index('IDX_RLPP_WORKSPACE_ID_ROLE_ID_OBJECT_METADATA_ID', [ @@ -71,7 +72,7 @@ export class RowLevelPermissionPredicateEntity operand: RowLevelPermissionPredicateOperand; @Column({ nullable: true, type: 'jsonb' }) - value: RowLevelPermissionPredicateValue | null; + value: JsonbProperty | null; @Column({ nullable: true, type: 'text', default: null }) subFieldName: string | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts index 05db212ec0..0836ee9af7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -13,6 +13,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; @@ -59,7 +60,7 @@ export class ServerlessFunctionEntity latestVersion: string | null; @Column({ nullable: false, type: 'jsonb', default: [] }) - publishedVersions: string[]; + publishedVersions: JsonbProperty; @Column({ nullable: false, default: ServerlessFunctionRuntime.NODE22 }) runtime: ServerlessFunctionRuntime; @@ -72,7 +73,7 @@ export class ServerlessFunctionEntity checksum: string | null; @Column({ nullable: true, type: 'jsonb' }) - toolInputSchema: object | null; + toolInputSchema: JsonbProperty | null; @Column({ nullable: false, default: false }) isTool: boolean; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/entities/view-filter.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/entities/view-filter.entity.ts index ae930e8bae..055e564681 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/entities/view-filter.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/entities/view-filter.entity.ts @@ -12,6 +12,7 @@ import { UpdateDateColumn, } from 'typeorm'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { type ViewFilterValue } from 'src/engine/metadata-modules/view-filter/types/view-filter-value.type'; @@ -47,7 +48,7 @@ export class ViewFilterEntity operand: ViewFilterOperand; @Column({ nullable: false, type: 'jsonb' }) - value: ViewFilterValue; + value: JsonbProperty; @Column({ nullable: true, type: 'uuid' }) viewFilterGroupId: string | null; diff --git a/packages/twenty-server/src/engine/metadata-modules/view-filter/types/view-filter-value.type.ts b/packages/twenty-server/src/engine/metadata-modules/view-filter/types/view-filter-value.type.ts index debc20a162..c520bf0c4a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view-filter/types/view-filter-value.type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view-filter/types/view-filter-value.type.ts @@ -9,6 +9,4 @@ export type ViewFilterValue = | boolean | number | RelationFilterValue - | Record - | null - | undefined; + | Record; diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts index 2b332b137f..587c3cd22f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts @@ -1,10 +1,10 @@ import { - type FieldMetadataType, - type FieldMetadataSettings, type FieldMetadataOptions, + type FieldMetadataSettings, + type FieldMetadataType, + type FieldMetadataDefaultValue, } from 'twenty-shared/types'; -import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { type Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-column-name-for-relation-field.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-column-name-for-relation-field.util.ts index 27783095f4..e6c039edea 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-column-name-for-relation-field.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-column-name-for-relation-field.util.ts @@ -1,12 +1,12 @@ import { - type FieldMetadataRelationSettings, + type FieldMetadataSettingsMapping, RelationType, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; export const formatColumnNameForRelationField = ( fieldName: string, - fieldMetadataSettings: FieldMetadataRelationSettings, + fieldMetadataSettings: FieldMetadataSettingsMapping['RELATION'], ): string => { if (fieldMetadataSettings.relationType === RelationType.ONE_TO_MANY) { throw new Error('No column exists for one to many relation fields'); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/extract-jsonb-properties.type-test.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/extract-jsonb-properties.type-test.ts new file mode 100644 index 0000000000..001b8c1557 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/extract-jsonb-properties.type-test.ts @@ -0,0 +1,71 @@ +import { type Equal, type Expect } from 'twenty-shared/testing'; + +import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type'; +import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +type EmptyObject = {}; + +type TestedRecord = { + // Non-JsonbProperty fields + plainString: string; + plainNumber: number; + plainObject: EmptyObject; + plainObjectNullable: EmptyObject | null; + plainArray: string[]; + plainUnknown: unknown; + jsonbString: JsonbProperty; + jsonbPlainUnknown: JsonbProperty; + jsonbNumber: JsonbProperty; + jsonbull: JsonbProperty; + + // JsonbProperty fields - should be extracted + jsonbPlainObject: JsonbProperty; + jsonbPlainArray: JsonbProperty; + jsonbPlainObjectNullable: JsonbProperty; + jsonbEmpty: JsonbProperty; + jsonbArray: JsonbProperty; + jsonbNested: JsonbProperty<{ nested: { deep: number } }>; + jsonbNullable: JsonbProperty | null; + jsonbUndefinable: JsonbProperty | undefined; + jsonbOptional?: JsonbProperty; + jsonbInnerNullable: JsonbProperty; + jsonbInnerUndefinable: JsonbProperty; + jsonbUnionWithPrimitive: JsonbProperty | string | null; + jsonbInnerNullableWithProperties: JsonbProperty; + wrongUsageButPassing: + | JsonbProperty + | string + | { foo: string }; +}; + +type TestResult = ExtractJsonbProperties; + +// eslint-disable-next-line unused-imports/no-unused-vars +type Assertions = [ + Expect< + Equal< + TestResult, + | 'jsonbPlainObject' + | 'jsonbPlainArray' + | 'jsonbPlainObjectNullable' + | 'jsonbEmpty' + | 'jsonbArray' + | 'jsonbNested' + | 'jsonbNullable' + | 'jsonbUndefinable' + | 'jsonbOptional' + | 'jsonbInnerNullable' + | 'jsonbInnerUndefinable' + | 'jsonbInnerNullableWithProperties' + | 'jsonbUnionWithPrimitive' + | 'wrongUsageButPassing' + > + >, + + // Empty object returns never + Expect, never>>, + + // Object with no JsonbProperty fields returns never + Expect, never>>, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-jsonb-serialized-relation.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-jsonb-serialized-relation.test-type.ts new file mode 100644 index 0000000000..d1ede3975d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/format-jsonb-serialized-relation.test-type.ts @@ -0,0 +1,173 @@ +import { type Equal, type Expect } from 'twenty-shared/testing'; +import { type SerializedRelation } from 'twenty-shared/types'; + +import { type FormatJsonbSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type'; +import { + type JSONB_PROPERTY_BRAND, + type JsonbProperty, +} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; + +type BrandedObjectWithRelation = JsonbProperty<{ + name: string; + targetFieldMetadataId: SerializedRelation; +}>; + +type BrandedObjectWithoutRelation = JsonbProperty<{ + name: string; + count: number; +}>; + +type UnbrandedObject = { + name: string; + targetFieldMetadataId: SerializedRelation; +}; + +// eslint-disable-next-line unused-imports/no-unused-vars +type BrandedObjectAssertions = [ + // Branded object with SerializedRelation: Id suffix renamed to UniversalIdentifier + Expect< + Equal< + FormatJsonbSerializedRelation, + { + name: string; + targetFieldMetadataUniversalIdentifier: SerializedRelation; + } + > + >, + + // Branded object without SerializedRelation: no renaming, just removes brand + Expect< + Equal< + FormatJsonbSerializedRelation, + { + name: string; + count: number; + } + > + >, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type UnbrandedObjectAssertions = [ + // Unbranded objects pass through unchanged + Expect< + Equal, UnbrandedObject> + >, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type PrimitiveAssertions = [ + // Primitives pass through unchanged + Expect, string>>, + Expect, number>>, + Expect, null>>, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type ArrayAssertions = [ + // Array of branded objects: transforms each element + Expect< + Equal< + FormatJsonbSerializedRelation, + { + name: string; + targetFieldMetadataUniversalIdentifier: SerializedRelation; + }[] + > + >, + + // Array of unbranded objects: passes through unchanged + Expect< + Equal, UnbrandedObject[]> + >, + + // Array of primitives: passes through unchanged + Expect, string[]>>, + + // Nested array of branded objects: transforms innermost elements + Expect< + Equal< + FormatJsonbSerializedRelation, + { + name: string; + targetFieldMetadataUniversalIdentifier: SerializedRelation; + }[][] + > + >, + + // Array of unbranded and branded objects union: transforms branded element + Expect< + Equal< + FormatJsonbSerializedRelation< + (BrandedObjectWithRelation | UnbrandedObject)[] + >, + ( + | { + name: string; + targetFieldMetadataUniversalIdentifier: SerializedRelation; + } + | UnbrandedObject + )[] + > + >, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type UnionAssertions = [ + // Union with null: transforms branded object, keeps null + Expect< + Equal< + FormatJsonbSerializedRelation, + { + name: string; + targetFieldMetadataUniversalIdentifier: SerializedRelation; + } | null + > + >, + + // Array of union: transforms elements appropriately + Expect< + Equal< + FormatJsonbSerializedRelation<(BrandedObjectWithRelation | null)[]>, + ({ + name: string; + targetFieldMetadataUniversalIdentifier: SerializedRelation; + } | null)[] + > + >, +]; + +type MultipleRelationsObject = JsonbProperty<{ + name: string; + sourceFieldId: SerializedRelation; + targetFieldId: SerializedRelation; + regularId: string; +}>; + +// eslint-disable-next-line unused-imports/no-unused-vars +type MultipleRelationsAssertions = [ + // Multiple SerializedRelation properties: all get renamed + Expect< + Equal< + FormatJsonbSerializedRelation, + { + name: string; + sourceFieldUniversalIdentifier: SerializedRelation; + targetFieldUniversalIdentifier: SerializedRelation; + regularId: string; + } + > + >, +]; + +// Verify brand is removed +type BrandRemovedCheck = + FormatJsonbSerializedRelation; + +// eslint-disable-next-line unused-imports/no-unused-vars +type BrandRemovedAssertion = Expect< + Equal< + typeof JSONB_PROPERTY_BRAND extends keyof BrandRemovedCheck ? true : false, + false + > +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/jsonb-property.type-test.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/jsonb-property.type-test.ts new file mode 100644 index 0000000000..e82dd7abee --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/jsonb-property.type-test.ts @@ -0,0 +1,97 @@ +import { type Equal, type Expect } from 'twenty-shared/testing'; + +import { + type JSONB_PROPERTY_BRAND, + type JsonbProperty, +} from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +type EmptyObject = {}; + +type SimpleObject = { value: string }; + +type NestedObject = { nested: { deep: number } }; + +// eslint-disable-next-line unused-imports/no-unused-vars +type PrimitiveAssertions = [ + // Primitives pass through unchanged (not objects) + Expect, string>>, + Expect, number>>, + Expect, boolean>>, + Expect, null>>, + Expect, undefined>>, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type ObjectAssertions = [ + // Objects get branded + Expect< + Equal< + JsonbProperty, + SimpleObject & { [JSONB_PROPERTY_BRAND]?: never } + > + >, + Expect< + Equal< + JsonbProperty, + NestedObject & { [JSONB_PROPERTY_BRAND]?: never } + > + >, + Expect< + Equal< + JsonbProperty, + EmptyObject & { [JSONB_PROPERTY_BRAND]?: never } + > + >, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type ArrayAssertions = [ + // Arrays are objects, so they get branded on the array itself + Expect< + Equal< + JsonbProperty, + string[] & { [JSONB_PROPERTY_BRAND]?: never } + > + >, + Expect< + Equal< + JsonbProperty, + number[] & { [JSONB_PROPERTY_BRAND]?: never } + > + >, + Expect< + Equal< + JsonbProperty, + SimpleObject[] & { [JSONB_PROPERTY_BRAND]?: never } + > + >, +]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type UnionAssertions = [ + // Union of object and null: object gets branded, null passes through + Expect< + Equal< + JsonbProperty, + (SimpleObject & { [JSONB_PROPERTY_BRAND]?: never }) | null + > + >, + + // Union of objects: both get branded (distributive conditional) + Expect< + Equal< + JsonbProperty, + | (SimpleObject & { [JSONB_PROPERTY_BRAND]?: never }) + | (NestedObject & { [JSONB_PROPERTY_BRAND]?: never }) + > + >, + + // Array in union: array gets branded + Expect< + Equal< + JsonbProperty, + (SimpleObject[] & { [JSONB_PROPERTY_BRAND]?: never }) | null + > + >, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts index c53a94bf39..722886afd9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/__tests__/universal-flat-field-metadata.test-type.ts @@ -1,7 +1,18 @@ -import { type Expect, type HasAllProperties } from 'twenty-shared/testing'; import { + type Equal, + type Expect, + type HasAllProperties, +} from 'twenty-shared/testing'; +import { + type FieldMetadataDefaultOption, type FieldMetadataType, + type FieldNumberVariant, + type LinkMetadata, type NullablePartial, + type NumberDataType, + type RelationOnDeleteAction, + type RelationType, + type SerializedRelation, } from 'twenty-shared/types'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; @@ -79,3 +90,69 @@ type UniversalFlatTransformationAssertions = [ > >, ]; + +type NarrowedTestCase = + UniversalFlatFieldMetadata['settings']; + +type NarrowedExpectedResult = { + relationType: RelationType; + onDelete?: RelationOnDeleteAction | undefined; + joinColumnName?: string | null | undefined; + junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined; +}; + +type SettingsTestCase = UniversalFlatFieldMetadata< + FieldMetadataType.RELATION | FieldMetadataType.NUMBER | FieldMetadataType.TEXT +>['settings']; + +type SettingsExpectedResult = + | { + relationType: RelationType; + onDelete?: RelationOnDeleteAction | undefined; + joinColumnName?: string | null | undefined; + junctionTargetFieldUniversalIdentifier?: SerializedRelation | undefined; + } + | { + dataType?: NumberDataType | undefined; + decimals?: number | undefined; + type?: FieldNumberVariant | undefined; + } + | { + displayedMaxRows?: number | undefined; + } + | null; + +type DefaultValueTestCase = UniversalFlatFieldMetadata< + | FieldMetadataType.RELATION + | FieldMetadataType.NUMBER + | FieldMetadataType.TEXT + | FieldMetadataType.LINKS + | FieldMetadataType.CURRENCY +>['defaultValue']; + +type DefaultValueExpectedResult = + | string + | number + | null + | { + amountMicros: string | null; + currencyCode: string | null; + } + | { + primaryLinkLabel: string | null; + primaryLinkUrl: string | null; + secondaryLinks: LinkMetadata[] | null; + }; + +type OptionsTestCase = + UniversalFlatFieldMetadata['options']; + +type OptionsExpectedResult = FieldMetadataDefaultOption[]; + +// eslint-disable-next-line unused-imports/no-unused-vars +type Assertions = [ + Expect>, + Expect>, + Expect>, + Expect>, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type.ts new file mode 100644 index 0000000000..11d18aeeb7 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type.ts @@ -0,0 +1,17 @@ +import { type JSONB_PROPERTY_BRAND } from './jsonb-property.type'; + +export type HasJsonbPropertyBrand = + typeof JSONB_PROPERTY_BRAND extends keyof T ? true : false; + +// Distributive check: returns `true` if any member of a union has the brand +type HasJsonbBrandInUnion = T extends unknown + ? HasJsonbPropertyBrand + : never; + +export type ExtractJsonbProperties = NonNullable< + { + [P in keyof T]-?: true extends HasJsonbBrandInUnion> + ? P + : never; + }[keyof T] +>; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type.ts new file mode 100644 index 0000000000..251c1aea5c --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type.ts @@ -0,0 +1,21 @@ +import { type ExtractSerializedRelationProperties } from 'twenty-shared/types'; + +import { type HasJsonbPropertyBrand } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type'; +import { type JSONB_PROPERTY_BRAND } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type'; +import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; + +export type FormatJsonbSerializedRelation = T extends unknown + ? T extends (infer U)[] + ? FormatJsonbSerializedRelation[] + : HasJsonbPropertyBrand extends true + ? Omit< + { + [P in keyof T as P extends ExtractSerializedRelationProperties & + string + ? `${RemoveSuffix}UniversalIdentifier` + : P]: T[P]; + }, + typeof JSONB_PROPERTY_BRAND + > + : T + : never; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type.ts new file mode 100644 index 0000000000..23f7f060b1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type.ts @@ -0,0 +1,7 @@ +export const JSONB_PROPERTY_BRAND = '__JsonbPropertyBrand__' as const; + +export type JsonbProperty = T extends unknown + ? T extends object + ? T & { [JSONB_PROPERTY_BRAND]?: never } + : T + : never; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts index cc5d538f79..1708741bb0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type.ts @@ -6,6 +6,8 @@ import { type ExtractEntityRelatedEntityProperties } from 'src/engine/metadata-m import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modules/flat-entity/types/from-metadata-entity-to-metadata-name.type'; import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type'; import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; +import { type ExtractJsonbProperties } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/extract-jsonb-properties.type'; +import { type FormatJsonbSerializedRelation } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/format-jsonb-serialized-relation.type'; import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type'; // TODO Handle universal settings @@ -22,6 +24,7 @@ export type UniversalFlatEntityFrom< | ExtractEntityRelatedEntityProperties | Extract, keyof TEntity> | keyof CastRecordTypeOrmDatePropertiesToString + | ExtractJsonbProperties > & CastRecordTypeOrmDatePropertiesToString & { [P in ExtractEntityOneToManyEntityRelationProperties< @@ -34,4 +37,8 @@ export type UniversalFlatEntityFrom< string as `${RemoveSuffix}UniversalIdentifier`]: TEntity[P]; } & { applicationUniversalIdentifier: string; + } & { + [P in ExtractJsonbProperties]: FormatJsonbSerializedRelation< + TEntity[P] + >; }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util.ts index 3b33248266..bafb58532b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/serialize-default-value.util.ts @@ -1,6 +1,5 @@ import { type ColumnType } from 'typeorm'; - -import { type FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; +import { type FieldMetadataDefaultValueForAnyType } from 'twenty-shared/types'; import { FieldMetadataException, @@ -11,7 +10,7 @@ import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field import { removeSqlDDLInjection } from 'src/engine/workspace-manager/workspace-migration/utils/remove-sql-injection.util'; type SerializeDefaultValueArgs = { - defaultValue?: FieldMetadataDefaultSerializableValue; + defaultValue?: FieldMetadataDefaultValueForAnyType; columnType?: ColumnType; schemaName: string; tableName: string; diff --git a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/utils/get-field-metadata-creation-inputs.util.ts b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/utils/get-field-metadata-creation-inputs.util.ts index 1c127e791a..713a52f98d 100644 --- a/packages/twenty-server/test/integration/graphql/suites/inputs-validation/utils/get-field-metadata-creation-inputs.util.ts +++ b/packages/twenty-server/test/integration/graphql/suites/inputs-validation/utils/get-field-metadata-creation-inputs.util.ts @@ -3,9 +3,9 @@ import { type FieldMetadataTypesToTestForFilterInputValidation, } from 'test/integration/graphql/suites/inputs-validation/types/field-metadata-type-to-test'; import { + type FieldMetadataSettingsMapping, FieldMetadataType, RelationType, - type FieldMetadataFilesSettings, type FieldMetadataMultiItemSettings, type RelationCreationPayload, } from 'twenty-shared/types'; @@ -20,7 +20,9 @@ type FieldMetadataCreationInput = { options?: FieldMetadataComplexOption[]; relationCreationPayload?: RelationCreationPayload; morphRelationsCreationPayload?: RelationCreationPayload[]; - settings?: FieldMetadataMultiItemSettings | FieldMetadataFilesSettings; + settings?: + | FieldMetadataMultiItemSettings + | FieldMetadataSettingsMapping['FILES']; }; export const getFieldMetadataCreationInputs = ( diff --git a/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts index e08c6f6e18..a86d7b512e 100644 --- a/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts +++ b/packages/twenty-server/test/integration/graphql/utils/view-data-factory.util.ts @@ -1,11 +1,5 @@ -import { ViewFilterOperand } from 'twenty-shared/types'; - -import { type UpdateViewFieldInput } from 'src/engine/metadata-modules/view-field/dtos/inputs/update-view-field.input'; -import { type ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; import { type ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity'; import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator'; -import { type ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; -import { type ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { type ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity'; import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction'; import { type ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; @@ -25,33 +19,6 @@ export const createViewData = (overrides: Partial = {}) => ({ ...overrides, }); -export const updateViewData = (overrides: Partial = {}) => ({ - name: 'Updated View', - type: ViewType.KANBAN, - isCompact: true, - ...overrides, -}); - -export const createViewFieldData = ( - viewId: string, - overrides: Partial = {}, -) => ({ - viewId, - position: 0, - isVisible: true, - size: 150, - ...overrides, -}); - -export const updateViewFieldData = ( - overrides: Partial = {}, -) => ({ - position: 5, - isVisible: false, - size: 300, - ...overrides, -}); - export const createViewSortData = ( viewId: string, overrides: Partial = {}, @@ -68,44 +35,6 @@ export const updateViewSortData = ( ...overrides, }); -export const createViewFilterData = ( - viewId: string, - overrides: Partial = {}, -) => ({ - viewId, - operand: ViewFilterOperand.IS, - value: 'test-value', - ...overrides, -}); - -export const updateViewFilterData = ( - overrides: Partial = {}, -) => ({ - operand: ViewFilterOperand.IS_NOT, - value: 'updated-value', - ...overrides, -}); - -export const createViewGroupData = ( - viewId: string, - overrides: Partial = {}, -) => ({ - viewId, - fieldValue: 'test-group-value', - isVisible: true, - position: 0, - ...overrides, -}); - -export const updateViewGroupData = ( - overrides: Partial = {}, -) => ({ - fieldValue: 'updated-group-value', - isVisible: false, - position: 1, - ...overrides, -}); - export const createViewFilterGroupData = ( viewId: string, overrides: Partial = {}, diff --git a/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts b/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts new file mode 100644 index 0000000000..feac1e8d3f --- /dev/null +++ b/packages/twenty-shared/src/types/ExtractSerializedRelationProperties.type.ts @@ -0,0 +1,16 @@ +import { type SERIALIZED_RELATION_BRAND } from './SerializedRelation.type'; + +type HasSerializedRelationPropertyBrand = + typeof SERIALIZED_RELATION_BRAND extends keyof T ? true : false; + +export type ExtractSerializedRelationProperties = T extends unknown + ? T extends object + ? { + [P in keyof T]-?: [NonNullable] extends [never] + ? never + : HasSerializedRelationPropertyBrand> extends true + ? P + : never; + }[keyof T] + : never + : never; diff --git a/packages/twenty-shared/src/types/FieldMetadataDefaultValue.ts b/packages/twenty-shared/src/types/FieldMetadataDefaultValue.ts index bf681e708c..cf7d0a5fec 100644 --- a/packages/twenty-shared/src/types/FieldMetadataDefaultValue.ts +++ b/packages/twenty-shared/src/types/FieldMetadataDefaultValue.ts @@ -1,43 +1,7 @@ +import { type LinkMetadata } from '@/types/composite-types/links.composite-type'; import { type FieldMetadataType } from '@/types/FieldMetadataType'; import { type IsExactly } from '@/types/IsExactly'; -import { - IsArray, - IsBoolean, - IsDate, - IsNotEmpty, - IsNumber, - IsNumberString, - IsObject, - IsOptional, - IsString, - IsUUID, - Matches, - ValidateIf, - type ValidationArguments, - type ValidationOptions, - registerDecorator, -} from 'class-validator'; - -const IsQuotedString = (validationOptions?: ValidationOptions) => { - return (object: object, propertyName: string) => { - registerDecorator({ - name: 'isQuotedString', - target: object.constructor, - propertyName: propertyName, - options: validationOptions, - validator: { - validate: (value: any) => { - return typeof value === 'string' && /^'{1}.*'{1}$/.test(value); - }, - defaultMessage: (args: ValidationArguments) => { - return `${args.property} must be a quoted string`; - }, - }, - }); - }; -}; - export const fieldMetadataDefaultValueFunctionName = { UUID: 'uuid', NOW: 'now', @@ -46,266 +10,105 @@ export const fieldMetadataDefaultValueFunctionName = { export type FieldMetadataDefaultValueFunctionNames = (typeof fieldMetadataDefaultValueFunctionName)[keyof typeof fieldMetadataDefaultValueFunctionName]; -export class FieldMetadataDefaultValueString { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - value!: string | null; -} +export type FieldMetadataDefaultValueUuidFunction = + typeof fieldMetadataDefaultValueFunctionName.UUID; +export type FieldMetadataDefaultValueNowFunction = + typeof fieldMetadataDefaultValueFunctionName.NOW; -export class FieldMetadataDefaultValueRawJson { - @ValidateIf((_object, value) => value !== null) - @IsObject() // TODO: Should this also allow arrays? - value!: object | null; -} - -export class FieldMetadataDefaultValueRichTextV2 { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - blocknote!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - markdown!: string | null; -} - -export class FieldMetadataDefaultValueRichText { - @ValidateIf((_object, value) => value !== null) - @IsString() - value!: string | null; -} - -export class FieldMetadataDefaultValueNumber { - @ValidateIf((_object, value) => value !== null) - @IsNumber() - value!: number | null; -} - -export class FieldMetadataDefaultValueBoolean { - @ValidateIf((_object, value) => value !== null) - @IsBoolean() - value!: boolean | null; -} - -export class FieldMetadataDefaultValueStringArray { - @ValidateIf((_object, value) => value !== null) - @IsArray() - @IsQuotedString({ each: true }) - value!: string[] | null; -} - -export class FieldMetadataDefaultValueDateTime { - @ValidateIf((_object, value) => value !== null) - @IsDate() - value!: Date | null; -} - -export class FieldMetadataDefaultValueDate { - @ValidateIf((_object, value) => value !== null) - @IsDate() - value!: Date | null; -} - -export class FieldMetadataDefaultValueCurrency { - @ValidateIf((_object, value) => value !== null) - @IsNumberString() - amountMicros!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - currencyCode!: string | null; -} - -export class FieldMetadataDefaultValueFullName { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - firstName!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - lastName!: string | null; -} - -export class FieldMetadataDefaultValueUuidFunction { - @Matches(fieldMetadataDefaultValueFunctionName.UUID) - @IsNotEmpty() - value!: typeof fieldMetadataDefaultValueFunctionName.UUID; -} - -export class FieldMetadataDefaultValueNowFunction { - @Matches(fieldMetadataDefaultValueFunctionName.NOW) - @IsNotEmpty() - value!: typeof fieldMetadataDefaultValueFunctionName.NOW; -} - -export class FieldMetadataDefaultValueAddress { - @ValidateIf((_object, value) => value !== null) - @IsString() - addressStreet1!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressStreet2!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressCity!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressPostcode!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressState!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - addressCountry!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsNumber() - addressLat!: number | null; - - @ValidateIf((_object, value) => value !== null) - @IsNumber() - addressLng!: number | null; -} - -class LinkMetadata { - @IsString() - label!: string; - - @IsString() - url!: string; -} - -export class FieldMetadataDefaultValueLinks { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryLinkLabel!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryLinkUrl!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsArray() - secondaryLinks!: LinkMetadata[] | null; -} - -export class FieldMetadataDefaultActor { - @ValidateIf((_object, value) => value !== null) - @IsString() - source!: string; - - @ValidateIf((_object, value) => value !== null) - @IsOptional() - @IsUUID() - workspaceMemberId?: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsString() - name!: string; -} - -export class FieldMetadataDefaultValueEmails { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryEmail!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsObject() - additionalEmails!: object | null; -} - -export class FieldMetadataDefaultValuePhones { - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryPhoneNumber!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryPhoneCountryCode!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsQuotedString() - primaryPhoneCallingCode!: string | null; - - @ValidateIf((_object, value) => value !== null) - @IsObject() - additionalPhones!: object | null; -} - -export class FieldMetadataDefaultArray { - @ValidateIf((_object, value) => value !== null) - @IsArray() - value!: string[] | null; -} - -type ExtractValueType = T extends { value: infer V } ? V : T; - -type UnionOfValues = T[keyof T]; - -type FieldMetadataDefaultValueMapping = { - [FieldMetadataType.UUID]: - | FieldMetadataDefaultValueString - | FieldMetadataDefaultValueUuidFunction; - [FieldMetadataType.TEXT]: FieldMetadataDefaultValueString; - [FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones; - [FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails; - [FieldMetadataType.DATE_TIME]: - | FieldMetadataDefaultValueDateTime - | FieldMetadataDefaultValueNowFunction; - [FieldMetadataType.DATE]: - | FieldMetadataDefaultValueDateTime - | FieldMetadataDefaultValueNowFunction; - [FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean; - [FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber; - [FieldMetadataType.POSITION]: FieldMetadataDefaultValueNumber; - [FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString; - [FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks; - [FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency; - [FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName; - [FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress; - [FieldMetadataType.RATING]: FieldMetadataDefaultValueString; - [FieldMetadataType.SELECT]: FieldMetadataDefaultValueString; - [FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueStringArray; - [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson; - [FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText; - [FieldMetadataType.ACTOR]: FieldMetadataDefaultActor; - [FieldMetadataType.ARRAY]: FieldMetadataDefaultArray; +export type FieldMetadataDefaultValueRichTextV2 = { + blocknote: string | null; + markdown: string | null; }; -export type FieldMetadataClassValidation = - UnionOfValues; +export type FieldMetadataDefaultValueCurrency = { + amountMicros: string | null; + currencyCode: string | null; +}; -export type FieldMetadataFunctionDefaultValue = ExtractValueType< - FieldMetadataDefaultValueUuidFunction | FieldMetadataDefaultValueNowFunction ->; +export type FieldMetadataDefaultValueFullName = { + firstName: string | null; + lastName: string | null; +}; -export type FieldMetadataDefaultValueForType< - T extends keyof FieldMetadataDefaultValueMapping, -> = ExtractValueType | null; +export type FieldMetadataDefaultValueAddress = { + addressStreet1: string | null; + addressStreet2: string | null; + addressCity: string | null; + addressPostcode: string | null; + addressState: string | null; + addressCountry: string | null; + addressLat: number | null; + addressLng: number | null; +}; -export type FieldMetadataDefaultValueForAnyType = ExtractValueType< - UnionOfValues -> | null; +export type FieldMetadataDefaultValueLinks = { + primaryLinkLabel: string | null; + primaryLinkUrl: string | null; + secondaryLinks: LinkMetadata[] | null; +}; + +export type FieldMetadataDefaultActor = { + source: string; + workspaceMemberId?: string | null; + name: string; +}; + +export type FieldMetadataDefaultValueEmails = { + primaryEmail: string | null; + additionalEmails: object | null; +}; + +export type FieldMetadataDefaultValuePhones = { + primaryPhoneNumber: string | null; + primaryPhoneCountryCode: string | null; + primaryPhoneCallingCode: string | null; + additionalPhones: object | null; +}; + +export type FieldMetadataDefaultValueMapping = { + [FieldMetadataType.UUID]: + | string + | FieldMetadataDefaultValueUuidFunction + | null; + [FieldMetadataType.TEXT]: string | null; + [FieldMetadataType.PHONES]: FieldMetadataDefaultValuePhones | null; + [FieldMetadataType.EMAILS]: FieldMetadataDefaultValueEmails | null; + [FieldMetadataType.DATE_TIME]: + | Date + | FieldMetadataDefaultValueNowFunction + | null; + [FieldMetadataType.DATE]: Date | FieldMetadataDefaultValueNowFunction | null; + [FieldMetadataType.BOOLEAN]: boolean | null; + [FieldMetadataType.NUMBER]: number | null; + [FieldMetadataType.POSITION]: number | null; + [FieldMetadataType.NUMERIC]: string | null; + [FieldMetadataType.LINKS]: FieldMetadataDefaultValueLinks | null; + [FieldMetadataType.CURRENCY]: FieldMetadataDefaultValueCurrency | null; + [FieldMetadataType.FULL_NAME]: FieldMetadataDefaultValueFullName | null; + [FieldMetadataType.ADDRESS]: FieldMetadataDefaultValueAddress | null; + [FieldMetadataType.RATING]: string | null; + [FieldMetadataType.SELECT]: string | null; + [FieldMetadataType.MULTI_SELECT]: string[] | null; + [FieldMetadataType.RAW_JSON]: object | null; + [FieldMetadataType.RICH_TEXT]: string | null; + [FieldMetadataType.RICH_TEXT_V2]: FieldMetadataDefaultValueRichTextV2 | null; + [FieldMetadataType.ACTOR]: FieldMetadataDefaultActor | null; + [FieldMetadataType.ARRAY]: string[] | null; +}; + +export type FieldMetadataFunctionDefaultValue = + | FieldMetadataDefaultValueUuidFunction + | FieldMetadataDefaultValueNowFunction; + +export type FieldMetadataDefaultValueForAnyType = + | null + | FieldMetadataDefaultValueMapping[keyof FieldMetadataDefaultValueMapping]; export type FieldMetadataDefaultValue< T extends FieldMetadataType = FieldMetadataType, > = IsExactly extends true - ? FieldMetadataDefaultValueForAnyType | null // Could be improved to be | unknown + ? FieldMetadataDefaultValueForAnyType : T extends keyof FieldMetadataDefaultValueMapping - ? FieldMetadataDefaultValueForType + ? FieldMetadataDefaultValueMapping[T] : never | null; - -type FieldMetadataDefaultValueExtractedTypes = { - [K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType< - FieldMetadataDefaultValueMapping[K] - >; -}; - -export type FieldMetadataDefaultSerializableValue = - | FieldMetadataDefaultValueExtractedTypes[keyof FieldMetadataDefaultValueExtractedTypes] - | null; diff --git a/packages/twenty-shared/src/types/FieldMetadataOptions.ts b/packages/twenty-shared/src/types/FieldMetadataOptions.ts index e71fbdeafc..c2040c564d 100644 --- a/packages/twenty-shared/src/types/FieldMetadataOptions.ts +++ b/packages/twenty-shared/src/types/FieldMetadataOptions.ts @@ -30,11 +30,15 @@ type FieldMetadataOptionsMapping = { [FieldMetadataType.MULTI_SELECT]: FieldMetadataComplexOption[]; }; +export type FieldMetadataOptionForAnyType = + | null + | FieldMetadataOptionsMapping[keyof FieldMetadataOptionsMapping]; + export type FieldMetadataOptions< T extends FieldMetadataType = FieldMetadataType, > = IsExactly extends true - ? null | (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]) // Could be improved to be | unknown + ? FieldMetadataOptionForAnyType : T extends keyof FieldMetadataOptionsMapping ? FieldMetadataOptionsMapping[T] : never | null; diff --git a/packages/twenty-shared/src/types/FieldMetadataSettings.ts b/packages/twenty-shared/src/types/FieldMetadataSettings.ts index 6b7080e00e..cee9334ef2 100644 --- a/packages/twenty-shared/src/types/FieldMetadataSettings.ts +++ b/packages/twenty-shared/src/types/FieldMetadataSettings.ts @@ -4,6 +4,7 @@ import { type FieldMetadataType } from '@/types/FieldMetadataType'; import { type IsExactly } from '@/types/IsExactly'; import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type'; import { type RelationType } from '@/types/RelationType'; +import { type SerializedRelation } from '@/types/SerializedRelation.type'; export enum NumberDataType { FLOAT = 'float', @@ -19,46 +20,47 @@ export enum DateDisplayFormat { export type FieldNumberVariant = 'number' | 'percentage'; -export type FieldMetadataNumberSettings = { +type FieldMetadataNumberSettings = { dataType?: NumberDataType; decimals?: number; type?: FieldNumberVariant; }; -export type FieldMetadataTextSettings = { +type FieldMetadataTextSettings = { displayedMaxRows?: number; }; -export type FieldMetadataDateSettings = { +type FieldMetadataDateSettings = { displayFormat?: DateDisplayFormat; }; -export type FieldMetadataDateTimeSettings = { +type FieldMetadataDateTimeSettings = { displayFormat?: DateDisplayFormat; }; -export type FieldMetadataRelationSettings = { +type FieldMetadataRelationSettings = { relationType: RelationType; onDelete?: RelationOnDeleteAction; joinColumnName?: string | null; // Points to the target field on the junction object // For MORPH_RELATION fields, morphRelations already contains all targets - junctionTargetFieldId?: string; + junctionTargetFieldId?: SerializedRelation; }; -export type FieldMetadataAddressSettings = { + +type FieldMetadataAddressSettings = { subFields?: AllowedAddressSubField[]; }; -export type FieldMetadataFilesSettings = { +type FieldMetadataFilesSettings = { maxNumberOfValues: number; }; -export type FieldMetadataTsVectorSettings = { +type FieldMetadataTsVectorSettings = { asExpression?: string; generatedType?: 'STORED' | 'VIRTUAL'; }; -type FieldMetadataSettingsMapping = { +export type FieldMetadataSettingsMapping = { [FieldMetadataType.NUMBER]: FieldMetadataNumberSettings | null; [FieldMetadataType.DATE]: FieldMetadataDateSettings | null; [FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings | null; @@ -81,7 +83,7 @@ export type FieldMetadataSettings< T extends FieldMetadataType = FieldMetadataType, > = IsExactly extends true - ? null | AllFieldMetadataSettings // Could be improved to be | unknown + ? null | AllFieldMetadataSettings : T extends keyof FieldMetadataSettingsMapping ? FieldMetadataSettingsMapping[T] : never | null; diff --git a/packages/twenty-shared/src/types/RowLevelPermissionPredicateValue.ts b/packages/twenty-shared/src/types/RowLevelPermissionPredicateValue.ts index 171ffc7e6f..d95e101502 100644 --- a/packages/twenty-shared/src/types/RowLevelPermissionPredicateValue.ts +++ b/packages/twenty-shared/src/types/RowLevelPermissionPredicateValue.ts @@ -12,5 +12,4 @@ export type RowLevelPermissionPredicateValue = | number | RelationPredicateValue | Record - | null - | undefined; + | null; diff --git a/packages/twenty-shared/src/types/SerializedRelation.type.ts b/packages/twenty-shared/src/types/SerializedRelation.type.ts new file mode 100644 index 0000000000..c9a0b35ba7 --- /dev/null +++ b/packages/twenty-shared/src/types/SerializedRelation.type.ts @@ -0,0 +1,5 @@ +export const SERIALIZED_RELATION_BRAND = '__SerializedRelationBrand__' as const; + +export type SerializedRelation = string & { + [SERIALIZED_RELATION_BRAND]?: never; +}; diff --git a/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts b/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts new file mode 100644 index 0000000000..15deaa5e83 --- /dev/null +++ b/packages/twenty-shared/src/types/__tests__/extract-serialized-relation-properties.type-test.ts @@ -0,0 +1,69 @@ +import { Equal, Expect } from '@/testing'; +import { type ExtractSerializedRelationProperties } from '@/types/ExtractSerializedRelationProperties.type'; +import { type SerializedRelation } from '@/types/SerializedRelation.type'; + +type TestedRecord = { + // Non-SerializedRelation fields + plainString: string; + plainNumber: number; + plainBoolean: boolean; + plainObject: { id: string }; + plainArray: string[]; + plainUnknown: unknown; + plainStringNullable: string | null; + plainStringOptional?: string; + + // SerializedRelation fields - should be extracted + relation: SerializedRelation; + relationNullable: SerializedRelation | null; + relationUndefinable: SerializedRelation | undefined; + relationOptional?: SerializedRelation; + relationOptionalNullable?: SerializedRelation | null; +}; + +type TestResult = ExtractSerializedRelationProperties; + +// eslint-disable-next-line unused-imports/no-unused-vars +type Assertions = [ + Expect< + Equal< + TestResult, + | 'relation' + | 'relationNullable' + | 'relationUndefinable' + | 'relationOptional' + | 'relationOptionalNullable' + > + >, + + // Object with no SerializedRelation fields returns never + Expect< + Equal, never> + >, + + // Primitives return never (tests T extends object branch) + Expect, never>>, + Expect, never>>, + Expect, never>>, + + // Empty object returns never + Expect, never>>, + + // Union types distribute correctly + Expect< + Equal< + ExtractSerializedRelationProperties< + { a: SerializedRelation } | { b: SerializedRelation } + >, + 'a' | 'b' + > + >, + + // Mixed union with object and primitive + Expect< + Equal< + ExtractSerializedRelationProperties<{ rel: SerializedRelation } | string>, + 'rel' + > + >, +]; diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index e278c51f81..e0e09cd452 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -55,54 +55,39 @@ export type { EnumFieldMetadataType } from './EnumFieldMetadataType'; export type { ExcludeFunctions } from './ExcludeFunctions'; export type { ExtractPropertiesThatEndsWithId } from './ExtractPropertiesThatEndsWithId'; export type { ExtractPropertiesThatEndsWithIds } from './ExtractPropertiesThatEndsWithIds'; +export type { ExtractSerializedRelationProperties } from './ExtractSerializedRelationProperties.type'; export type { FieldMetadataDefaultValueFunctionNames, - FieldMetadataClassValidation, - FieldMetadataFunctionDefaultValue, - FieldMetadataDefaultValueForType, - FieldMetadataDefaultValueForAnyType, - FieldMetadataDefaultValue, - FieldMetadataDefaultSerializableValue, -} from './FieldMetadataDefaultValue'; -export { - fieldMetadataDefaultValueFunctionName, - FieldMetadataDefaultValueString, - FieldMetadataDefaultValueRawJson, - FieldMetadataDefaultValueRichTextV2, - FieldMetadataDefaultValueRichText, - FieldMetadataDefaultValueNumber, - FieldMetadataDefaultValueBoolean, - FieldMetadataDefaultValueStringArray, - FieldMetadataDefaultValueDateTime, - FieldMetadataDefaultValueDate, - FieldMetadataDefaultValueCurrency, - FieldMetadataDefaultValueFullName, FieldMetadataDefaultValueUuidFunction, FieldMetadataDefaultValueNowFunction, + FieldMetadataDefaultValueRichTextV2, + FieldMetadataDefaultValueCurrency, + FieldMetadataDefaultValueFullName, FieldMetadataDefaultValueAddress, FieldMetadataDefaultValueLinks, FieldMetadataDefaultActor, FieldMetadataDefaultValueEmails, FieldMetadataDefaultValuePhones, - FieldMetadataDefaultArray, + FieldMetadataDefaultValueMapping, + FieldMetadataFunctionDefaultValue, + FieldMetadataDefaultValueForAnyType, + FieldMetadataDefaultValue, } from './FieldMetadataDefaultValue'; +export { fieldMetadataDefaultValueFunctionName } from './FieldMetadataDefaultValue'; export type { FieldMetadataMultiItemSettings } from './FieldMetadataMultiItemSettings'; export { FieldMetadataSettingsOnClickAction } from './FieldMetadataMultiItemSettings'; -export type { TagColor, FieldMetadataOptions } from './FieldMetadataOptions'; +export type { + TagColor, + FieldMetadataOptionForAnyType, + FieldMetadataOptions, +} from './FieldMetadataOptions'; export { FieldMetadataDefaultOption, FieldMetadataComplexOption, } from './FieldMetadataOptions'; export type { FieldNumberVariant, - FieldMetadataNumberSettings, - FieldMetadataTextSettings, - FieldMetadataDateSettings, - FieldMetadataDateTimeSettings, - FieldMetadataRelationSettings, - FieldMetadataAddressSettings, - FieldMetadataFilesSettings, - FieldMetadataTsVectorSettings, + FieldMetadataSettingsMapping, AllFieldMetadataSettings, FieldMetadataSettings, } from './FieldMetadataSettings'; @@ -200,6 +185,8 @@ export type { RelationPredicateValue, RowLevelPermissionPredicateValue, } from './RowLevelPermissionPredicateValue'; +export type { SerializedRelation } from './SerializedRelation.type'; +export { SERIALIZED_RELATION_BRAND } from './SerializedRelation.type'; export type { ServerlessFunctionEvent } from './ServerlessFunctionEvent'; export { SettingsPath } from './SettingsPath'; export type { Sources } from './SourcesType';