Improve FieldMetadataEntity defaultValue, settings and options typing (#13320)

# Introduction
Following https://github.com/twentyhq/twenty/pull/13264, this PR
introduces several `fieldMetadataEntity` typing enhancement suggestions.

Mainly any nullable field metadata entity properties are now either
nullable or defined.
Or never if field is dynamically required or not depending on the field
metadata type

This enhance DevX

## Standards

- field enum ( `MULTI_SELECT`, `SELECT`, `RATING` ) will never have
`options` set to `NULL` in db
- field `RELATION` or `MORH_RELATION` won't ever have its relation
fields set to `NULL` in db
- field of any type `settings`, even if possibly defined, can still be
`NULL` in db
- field of any type `defaultValue`, even if possibly defined, can still
be `NULL` in db

It coud be interesting to guard these standards by adding dedicated pg
constraints on each field

## TypesScript type tests
added coverage for each `settings`, `defaultValue`, and `options`
depending on the current `fieldMetadata`
Honestly I don' know if this typescript assertions test file is not
overkill, but regarding metadata staticness it might be very interesting
to have this guard

## Possible improvements
- We could type as `unknown` instead of "all" on `FieldMetadataType`
inferrance
- We still need to deprecate remaining duplicated entities such as
`Index/Field/MetadataInterface` etc not a huge refactor neither urgent
This commit is contained in:
Paul Rastoin
2025-07-23 13:43:27 +02:00
committed by GitHub
parent 602e446337
commit a0a575fa0b
36 changed files with 611 additions and 214 deletions

View File

@ -133,7 +133,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
// @Validate(IsFieldMetadataOptions)
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions<T> | null;
options?: FieldMetadataOptions<T>;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })

View File

@ -24,7 +24,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis
type IsRelationType<Ttype, T extends FieldMetadataType = FieldMetadataType> =
IsExactly<T, FieldMetadataType> extends true
? null | Ttype
? null | Ttype // Could be improved to be | unknown
: T extends FieldMetadataType.RELATION
? Ttype
: T extends FieldMetadataType.MORPH_RELATION
@ -53,7 +53,8 @@ type IsRelationType<Ttype, T extends FieldMetadataType = FieldMetadataType> =
// TODO add some documentation about this entity
export class FieldMetadataEntity<
T extends FieldMetadataType = FieldMetadataType,
> {
> implements Required<FieldMetadataEntity>
{
@PrimaryGeneratedColumn('uuid')
id: string;
@ -84,7 +85,7 @@ export class FieldMetadataEntity<
label: string;
@Column({ nullable: true, type: 'jsonb' })
defaultValue: FieldMetadataDefaultValue<T> | null;
defaultValue: FieldMetadataDefaultValue<T>;
@Column({ nullable: true, type: 'text' })
description: string | null;
@ -93,13 +94,13 @@ export class FieldMetadataEntity<
icon: string | null;
@Column({ type: 'jsonb', nullable: true })
standardOverrides?: FieldStandardOverridesDTO | null;
standardOverrides: FieldStandardOverridesDTO | null;
@Column('jsonb', { nullable: true })
options: FieldMetadataOptions<T> | null;
options: FieldMetadataOptions<T>;
@Column('jsonb', { nullable: true })
settings?: FieldMetadataSettings<T> | null;
settings: FieldMetadataSettings<T>;
@Column({ default: false })
isCustom: boolean;

View File

@ -110,7 +110,7 @@ describe('BeforeUpdateOneField', () => {
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
@ -147,7 +147,7 @@ describe('BeforeUpdateOneField', () => {
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
@ -182,7 +182,7 @@ describe('BeforeUpdateOneField', () => {
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
@ -230,7 +230,7 @@ describe('BeforeUpdateOneField', () => {
},
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')
@ -277,7 +277,7 @@ describe('BeforeUpdateOneField', () => {
isLabelSyncedWithName: false,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace')

View File

@ -73,7 +73,7 @@ export type FieldMetadataDefaultValue<
T extends FieldMetadataType = FieldMetadataType,
> =
IsExactly<T, FieldMetadataType> extends true
? FieldMetadataDefaultValueForAnyType
? FieldMetadataDefaultValueForAnyType | null // Could be improved to be | unknown
: T extends keyof FieldMetadataDefaultValueMapping
? FieldMetadataDefaultValueForType<T>
: never;

View File

@ -15,7 +15,7 @@ export type FieldMetadataOptions<
T extends FieldMetadataType = FieldMetadataType,
> =
IsExactly<T, FieldMetadataType> extends true
? FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]
? null | (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]) // Could be improved to be | unknown
: T extends keyof FieldMetadataOptionsMapping
? FieldMetadataOptionsMapping[T]
: never;

View File

@ -9,10 +9,6 @@ export enum NumberDataType {
BIGINT = 'bigint',
}
export type FieldMetadataDefaultSettings = {
isForeignKey?: boolean;
};
export enum DateDisplayFormat {
RELATIVE = 'RELATIVE',
USER_SETTINGS = 'USER_SETTINGS',
@ -54,11 +50,14 @@ type FieldMetadataSettingsMapping = {
[FieldMetadataType.MORPH_RELATION]: FieldMetadataRelationSettings;
};
export type AllFieldMetadataSettings =
FieldMetadataSettingsMapping[keyof FieldMetadataSettingsMapping];
export type FieldMetadataSettings<
T extends FieldMetadataType = FieldMetadataType,
> =
IsExactly<T, FieldMetadataType> extends true
? FieldMetadataDefaultSettings
? null | AllFieldMetadataSettings // Could be improved to be | unknown
: T extends keyof FieldMetadataSettingsMapping
? FieldMetadataSettingsMapping[T] & FieldMetadataDefaultSettings
? FieldMetadataSettingsMapping[T] | null
: never;

View File

@ -137,12 +137,17 @@ export class FieldMetadataEnumValidationService {
);
}
private validateDuplicates(options: FieldMetadataOptions) {
private validateDuplicates(
options: FieldMetadataOptions<EnumFieldMetadataType>,
) {
const fieldsToCheckForDuplicates = [
'position',
'id',
'value',
] as const satisfies (keyof FieldMetadataOptions[number])[];
] as const satisfies (keyof (
| FieldMetadataDefaultOption[]
| FieldMetadataComplexOption[]
)[number])[];
const duplicatedValidators = fieldsToCheckForDuplicates.map<
Validator<FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]>
>((field) => ({
@ -178,7 +183,7 @@ export class FieldMetadataEnumValidationService {
}
private validateSelectDefaultValue(
options: FieldMetadataOptions,
options: FieldMetadataOptions<EnumFieldMetadataType>,
defaultValue: unknown,
) {
if (typeof defaultValue !== 'string') {
@ -209,7 +214,7 @@ export class FieldMetadataEnumValidationService {
}
private validateMultiSelectDefaultValue(
options: FieldMetadataOptions,
options: FieldMetadataOptions<EnumFieldMetadataType>,
defaultValue: unknown,
) {
if (!Array.isArray(defaultValue)) {
@ -241,7 +246,7 @@ export class FieldMetadataEnumValidationService {
private validateFieldMetadataDefaultValue(
fieldType: EnumFieldMetadataType,
options: FieldMetadataOptions,
options: FieldMetadataOptions<EnumFieldMetadataType>,
defaultValue: unknown,
) {
switch (fieldType) {

View File

@ -8,8 +8,8 @@ import { v4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
FieldMetadataExceptionCode,

View File

@ -8,6 +8,7 @@ import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
@ -313,6 +314,7 @@ export class FieldMetadataRelationService {
});
}
// TODO refactor and strictly type
computeCustomRelationFieldMetadataForCreation({
fieldMetadataInput,
relationCreationPayload,
@ -332,13 +334,23 @@ export class FieldMetadataRelationService {
FieldMetadataType.MORPH_RELATION,
);
const defaultIcon = 'IconRelationOneToMany';
const isManyToOne =
isRelation && relationCreationPayload?.type === RelationType.MANY_TO_ONE;
const isOneToMany =
isRelation && relationCreationPayload?.type === RelationType.ONE_TO_MANY;
const defaultIcon = 'IconRelationOneToMany';
const settings = isManyToOne
? {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName,
}
: {
...(fieldMetadataInput.settings as FieldMetadataSettings<
FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION
>),
relationType: RelationType.ONE_TO_MANY,
};
return {
...fieldMetadataInput,
@ -346,21 +358,7 @@ export class FieldMetadataRelationService {
relationCreationPayload,
relationTargetObjectMetadataId:
relationCreationPayload?.targetObjectMetadataId,
settings: {
...fieldMetadataInput.settings,
...(isOneToMany
? {
relationType: RelationType.ONE_TO_MANY,
}
: {}),
...(isManyToOne
? {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName,
}
: {}),
},
settings,
};
}
}

View File

@ -77,32 +77,37 @@ export class FieldMetadataValidationService {
}) {
switch (fieldType) {
case FieldMetadataType.NUMBER:
await this.validateSettings<FieldMetadataType.NUMBER>(
NumberSettingsValidation,
await this.validateSettings({
type: FieldMetadataType.NUMBER,
validator: NumberSettingsValidation,
settings,
);
});
break;
case FieldMetadataType.TEXT:
await this.validateSettings<FieldMetadataType.TEXT>(
TextSettingsValidation,
await this.validateSettings({
type: FieldMetadataType.TEXT,
validator: TextSettingsValidation,
settings,
);
});
break;
default:
break;
}
}
private async validateSettings<Type extends FieldMetadataType>(
validator: ClassConstructor<
Type extends FieldMetadataType.NUMBER
? NumberSettingsValidation
: Type extends FieldMetadataType.TEXT
? TextSettingsValidation
: never
>,
settings: FieldMetadataSettings<Type>,
) {
private async validateSettings<
Type extends FieldMetadataType,
TValidator extends ClassConstructor<object>,
TFieldMetadataType extends FieldMetadataType,
>({
type: _type,
settings,
validator,
}: {
validator: TValidator;
settings: FieldMetadataSettings<Type>;
type: TFieldMetadataType;
}) {
try {
const settingsInstance = plainToInstance(validator, settings);

View File

@ -9,13 +9,13 @@ import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FieldMetadataException,
FieldMetadataExceptionCode,

View File

@ -1,7 +1,24 @@
import { Expect, HasAllProperties } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, NullablePartial } from 'twenty-shared/types';
import { Relation as TypeOrmRelation } from 'typeorm';
import {
AllFieldMetadataSettings,
FieldMetadataDateSettings,
FieldMetadataDateTimeSettings,
FieldMetadataNumberSettings,
FieldMetadataRelationSettings,
FieldMetadataTextSettings,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import {
FieldMetadataDefaultValueForAnyType,
FieldMetadataDefaultValueForType,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -19,6 +36,8 @@ type NotDefinedRelationRecord = {
relationTargetObjectMetadata: never;
};
type AbstractFieldMetadata = FieldMetadataEntity<FieldMetadataType>;
type UUIDFieldMetadata = FieldMetadataEntity<FieldMetadataType.UUID>;
type TextFieldMetadata = FieldMetadataEntity<FieldMetadataType.TEXT>;
@ -64,7 +83,7 @@ type MorphRelationFieldMetadata =
FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type Assertions = [
type RelationAssertions = [
Expect<HasAllProperties<UUIDFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<TextFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<NumberFieldMetadata, NotDefinedRelationRecord>>,
@ -87,4 +106,288 @@ type Assertions = [
Expect<HasAllProperties<RelationFieldMetadata, DefinedRelationRecord>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, DefinedRelationRecord>>,
Expect<
HasAllProperties<
AbstractFieldMetadata,
NullablePartial<DefinedRelationRecord>
>
>,
];
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type SettingsAssertions = [
Expect<HasAllProperties<CurrencyFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<FullNameFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<RatingFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<SelectFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<MultiSelectFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<PositionFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<RawJsonFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<RichTextFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<ActorFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<ArrayFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<PhonesFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<EmailsFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<LinksFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<UUIDFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<BooleanFieldMetadata, { settings: never }>>,
Expect<
HasAllProperties<
TextFieldMetadata,
{ settings: FieldMetadataTextSettings | null }
>
>,
Expect<
HasAllProperties<
NumberFieldMetadata,
{ settings: FieldMetadataNumberSettings | null }
>
>,
Expect<
HasAllProperties<
DateFieldMetadata,
{ settings: FieldMetadataDateSettings | null }
>
>,
Expect<
HasAllProperties<
DateTimeFieldMetadata,
{ settings: FieldMetadataDateTimeSettings | null }
>
>,
Expect<
HasAllProperties<
RelationFieldMetadata,
{ settings: FieldMetadataRelationSettings | null }
>
>,
Expect<
HasAllProperties<
MorphRelationFieldMetadata,
{ settings: FieldMetadataRelationSettings | null }
>
>,
Expect<
HasAllProperties<
AbstractFieldMetadata,
{ settings: AllFieldMetadataSettings | null }
>
>,
];
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type DefaultValueAssertions = [
Expect<
HasAllProperties<
UUIDFieldMetadata,
{ defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.UUID> }
>
>,
Expect<
HasAllProperties<
TextFieldMetadata,
{ defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.TEXT> }
>
>,
Expect<
HasAllProperties<
NumberFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.NUMBER>;
}
>
>,
Expect<
HasAllProperties<
BooleanFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.BOOLEAN>;
}
>
>,
Expect<
HasAllProperties<
DateFieldMetadata,
{ defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.DATE> }
>
>,
Expect<
HasAllProperties<
DateTimeFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.DATE_TIME>;
}
>
>,
Expect<
HasAllProperties<
CurrencyFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.CURRENCY>;
}
>
>,
Expect<
HasAllProperties<
FullNameFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.FULL_NAME>;
}
>
>,
Expect<
HasAllProperties<
RatingFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.RATING>;
}
>
>,
Expect<
HasAllProperties<
SelectFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.SELECT>;
}
>
>,
Expect<
HasAllProperties<
MultiSelectFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.MULTI_SELECT>;
}
>
>,
Expect<
HasAllProperties<
PositionFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.POSITION>;
}
>
>,
Expect<
HasAllProperties<
RawJsonFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.RAW_JSON>;
}
>
>,
Expect<
HasAllProperties<
RichTextFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.RICH_TEXT>;
}
>
>,
Expect<
HasAllProperties<
ActorFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.ACTOR>;
}
>
>,
Expect<
HasAllProperties<
ArrayFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.ARRAY>;
}
>
>,
Expect<
HasAllProperties<
PhonesFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.PHONES>;
}
>
>,
Expect<
HasAllProperties<
EmailsFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.EMAILS>;
}
>
>,
Expect<
HasAllProperties<
LinksFieldMetadata,
{
defaultValue: FieldMetadataDefaultValueForType<FieldMetadataType.LINKS>;
}
>
>,
Expect<HasAllProperties<RelationFieldMetadata, { defaultValue: never }>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, { defaultValue: never }>>,
Expect<
HasAllProperties<
AbstractFieldMetadata,
{ defaultValue: FieldMetadataDefaultValueForAnyType | null }
>
>,
];
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type OptionsAssertions = [
Expect<
HasAllProperties<
RatingFieldMetadata,
{ options: FieldMetadataDefaultOption[] }
>
>,
Expect<
HasAllProperties<
SelectFieldMetadata,
{ options: FieldMetadataComplexOption[] }
>
>,
Expect<
HasAllProperties<
MultiSelectFieldMetadata,
{ options: FieldMetadataComplexOption[] }
>
>,
Expect<HasAllProperties<UUIDFieldMetadata, { options: never }>>,
Expect<HasAllProperties<TextFieldMetadata, { options: never }>>,
Expect<HasAllProperties<NumberFieldMetadata, { options: never }>>,
Expect<HasAllProperties<BooleanFieldMetadata, { options: never }>>,
Expect<HasAllProperties<DateFieldMetadata, { options: never }>>,
Expect<HasAllProperties<DateTimeFieldMetadata, { options: never }>>,
Expect<HasAllProperties<CurrencyFieldMetadata, { options: never }>>,
Expect<HasAllProperties<FullNameFieldMetadata, { options: never }>>,
Expect<HasAllProperties<PositionFieldMetadata, { options: never }>>,
Expect<HasAllProperties<RawJsonFieldMetadata, { options: never }>>,
Expect<HasAllProperties<RichTextFieldMetadata, { options: never }>>,
Expect<HasAllProperties<ActorFieldMetadata, { options: never }>>,
Expect<HasAllProperties<ArrayFieldMetadata, { options: never }>>,
Expect<HasAllProperties<PhonesFieldMetadata, { options: never }>>,
Expect<HasAllProperties<EmailsFieldMetadata, { options: never }>>,
Expect<HasAllProperties<LinksFieldMetadata, { options: never }>>,
Expect<HasAllProperties<RelationFieldMetadata, { options: never }>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, { options: never }>>,
Expect<
HasAllProperties<
AbstractFieldMetadata,
{
options:
| null
| (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]);
}
>
>,
];

View File

@ -28,7 +28,10 @@ describe('FieldMetadataValidationService', () => {
});
it('should validate NUMBER settings successfully', async () => {
const settings = { decimals: 2, type: 'number' } as FieldMetadataSettings;
const settings: FieldMetadataSettings<FieldMetadataType.NUMBER> = {
decimals: 2,
type: 'number',
};
await expect(
service.validateSettingsOrThrow({
@ -39,7 +42,10 @@ describe('FieldMetadataValidationService', () => {
});
it('should throw an error for invalid NUMBER settings', async () => {
const settings = { type: 'invalidType' } as FieldMetadataSettings;
const settings: FieldMetadataSettings<FieldMetadataType.NUMBER> = {
// @ts-expect-error expected invalid payload below
type: 'invalidType',
};
await expect(
service.validateSettingsOrThrow({
@ -50,7 +56,9 @@ describe('FieldMetadataValidationService', () => {
});
it('should validate TEXT settings successfully', async () => {
const settings = { displayedMaxRows: 10 } as FieldMetadataSettings;
const settings: FieldMetadataSettings<FieldMetadataType.TEXT> = {
displayedMaxRows: 10,
};
await expect(
service.validateSettingsOrThrow({
@ -61,9 +69,10 @@ describe('FieldMetadataValidationService', () => {
});
it('should throw an error for invalid TEXT settings', async () => {
const settings = {
const settings: FieldMetadataSettings<FieldMetadataType.TEXT> = {
// @ts-expect-error expected invalid payload below
displayedMaxRows: 'NotANumber',
} as FieldMetadataSettings;
};
await expect(
service.validateSettingsOrThrow({

View File

@ -27,7 +27,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi
'namePlural',
'workspaceId',
])
export class ObjectMetadataEntity {
export class ObjectMetadataEntity implements Required<ObjectMetadataEntity> {
@PrimaryGeneratedColumn('uuid')
id: string;

View File

@ -6,7 +6,6 @@ import { capitalize, isDefined } from 'twenty-shared/utils';
import { QueryRunner, Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid';
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -228,7 +227,7 @@ export class ObjectMetadataFieldRelationService {
id: targetFieldMetadataToUpdate.id,
...targetFieldMetadataUpdateData,
settings: {
...(targetFieldMetadataToUpdate.settings as FieldMetadataDefaultSettings),
...targetFieldMetadataToUpdate.settings,
...(isTargetFieldMetadataManyToOneRelation
? {
joinColumnName: `${sourceObjectMetadata.nameSingular}Id`,
@ -260,7 +259,7 @@ export class ObjectMetadataFieldRelationService {
id: sourceFieldMetadataToUpdate.id,
...sourceFieldMetadataUpdateData,
settings: {
...(sourceFieldMetadataToUpdate.settings as FieldMetadataDefaultSettings),
...sourceFieldMetadataToUpdate.settings,
...(isSourceFieldMetadataManyToOneRelation
? {
joinColumnName: `${targetObjectMetadata.nameSingular}Id`,

View File

@ -8,7 +8,6 @@ import {
fieldTextMock,
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
@ -127,7 +126,7 @@ describe('FieldPermissionService', () => {
objectMetadataId: testObjectMetadataId,
workspaceId: testWorkspaceId,
id: '20202020-0000-0000-0000-000000000003',
}) as FieldMetadataEntity,
}),
},
fieldIdByJoinColumnName: {},
fieldIdByName: {},

View File

@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { In, Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
@ -175,7 +176,14 @@ export class RemoteTableRelationsService {
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
...(isDefined(objectPrimaryKeyFieldSettings)
? {
settings: {
...objectPrimaryKeyFieldSettings,
isForeignKey: true,
},
}
: {}),
},
);
@ -215,7 +223,14 @@ export class RemoteTableRelationsService {
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
...(isDefined(objectPrimaryKeyFieldSettings)
? {
settings: {
...objectPrimaryKeyFieldSettings,
isForeignKey: true,
},
}
: {}),
},
);
@ -255,7 +270,14 @@ export class RemoteTableRelationsService {
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
...(isDefined(objectPrimaryKeyFieldSettings)
? {
settings: {
...objectPrimaryKeyFieldSettings,
isForeignKey: true,
},
}
: {}),
},
);