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

@ -131,7 +131,6 @@ export const fieldRelationMock = getMockFieldMetadataEntity({
type: FieldMetadataType.RELATION,
label: 'Field Relation',
isNullable: true,
defaultValue: null,
settings: {
relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'fieldRelationId',

View File

@ -1,7 +1,6 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
@ -57,7 +56,7 @@ export const mockPersonObjectMetadataWithFieldMaps = (
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
'emails-id': getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '',
@ -75,7 +74,7 @@ export const mockPersonObjectMetadataWithFieldMaps = (
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
'linkedinLink-id': getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '',
@ -95,7 +94,7 @@ export const mockPersonObjectMetadataWithFieldMaps = (
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
'jobTitle-id': getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '',
@ -111,6 +110,6 @@ export const mockPersonObjectMetadataWithFieldMaps = (
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
},
});

View File

@ -5,7 +5,6 @@ import {
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { getFieldType } from 'src/engine/api/rest/core/query-builder/utils/get-field-type.utils';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
@ -25,7 +24,7 @@ describe('getFieldType', () => {
});
const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock as FieldMetadataEntity,
'field-number-id': completeFieldNumberMock,
};
const mockObjectMetadataWithFieldMaps = {

View File

@ -1,6 +1,6 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataRelationSettings } 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 {
@ -10,7 +10,6 @@ import {
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { mapFieldMetadataToGraphqlQuery } from 'src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@ -60,9 +59,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
});
const fieldsById: FieldMetadataMap = {
'field-number-id': typedFieldNumberMock as FieldMetadataEntity,
'field-text-id': typedFieldTextMock as FieldMetadataEntity,
'field-currency-id': typedFieldCurrencyMock as FieldMetadataEntity,
'field-number-id': typedFieldNumberMock,
'field-text-id': typedFieldTextMock,
'field-currency-id': typedFieldCurrencyMock,
};
const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = {
@ -90,19 +89,19 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
expect(
mapFieldMetadataToGraphqlQuery(
objectMetadataMapsMock,
typedFieldNumberMock as FieldMetadataEntity,
typedFieldNumberMock,
),
).toEqual('fieldNumber');
expect(
mapFieldMetadataToGraphqlQuery(
objectMetadataMapsMock,
typedFieldTextMock as FieldMetadataEntity,
typedFieldTextMock,
),
).toEqual('fieldText');
expect(
mapFieldMetadataToGraphqlQuery(
objectMetadataMapsMock,
typedFieldCurrencyMock as FieldMetadataEntity,
typedFieldCurrencyMock,
),
).toEqual(`
fieldCurrency
@ -132,8 +131,8 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
fieldMetadataType === FieldMetadataType.MORPH_RELATION
? ({
relationType: RelationType.MANY_TO_ONE,
} as FieldMetadataDefaultSettings)
: undefined,
} as FieldMetadataRelationSettings)
: null,
});
expect(

View File

@ -4,7 +4,6 @@ import { DateDisplayFormat } from 'src/engine/metadata-modules/field-metadata/in
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
@ -50,7 +49,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-5eef-417a-b517-ebeedaa8e10b': getMockFieldMetadataEntity({
id: '20202020-5eef-417a-b517-ebeedaa8e10b',
workspaceId,
@ -69,7 +68,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-597c-44d3-98ec-ea71aea5256b': getMockFieldMetadataEntity({
id: '20202020-597c-44d3-98ec-ea71aea5256b',
workspaceId,
@ -88,7 +87,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-9b94-454a-94ca-8afb09c68faf': getMockFieldMetadataEntity({
id: '20202020-9b94-454a-94ca-8afb09c68faf',
workspaceId,
@ -144,7 +143,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-30a5-4d8e-9b93-12d31ece0aaa': getMockFieldMetadataEntity({
id: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
workspaceId,
@ -163,7 +162,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-f95f-424f-ab32-65961e8e9635': getMockFieldMetadataEntity({
id: '20202020-f95f-424f-ab32-65961e8e9635',
workspaceId,
@ -182,7 +181,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-5e10-4780-babb-38a465ac546c': getMockFieldMetadataEntity({
id: '20202020-5e10-4780-babb-38a465ac546c',
workspaceId,
@ -201,7 +200,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-8f4a-4f8d-822e-90fe72f75b79': getMockFieldMetadataEntity({
id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
workspaceId,
@ -220,7 +219,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-f120-4b59-b239-f7f1d8eb243e': getMockFieldMetadataEntity({
id: '20202020-f120-4b59-b239-f7f1d8eb243e',
workspaceId,
@ -240,7 +239,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-dcc8-4318-9756-b87377692561': getMockFieldMetadataEntity({
id: '20202020-dcc8-4318-9756-b87377692561',
workspaceId,
@ -260,7 +259,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-1694-4f8b-8760-61a5ff330022': getMockFieldMetadataEntity({
id: '20202020-1694-4f8b-8760-61a5ff330022',
workspaceId,
@ -280,7 +279,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-4f52-4dea-a116-723f9bf7f082': getMockFieldMetadataEntity({
id: '20202020-4f52-4dea-a116-723f9bf7f082',
workspaceId,
@ -288,7 +287,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'pointOfContact',
label: 'Point of Contact',
defaultValue: null,
description: 'The point of contact for this opportunity',
icon: 'IconUser',
settings: {
@ -304,7 +302,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-fc02-4be2-be1a-e121daf5400d': getMockFieldMetadataEntity({
id: '20202020-fc02-4be2-be1a-e121daf5400d',
workspaceId,
@ -312,7 +310,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'company',
label: 'Company',
defaultValue: null,
description: 'The company this opportunity is associated with',
icon: 'IconBuildingSkyscraper',
settings: {
@ -328,7 +325,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-fd9f-48f0-bd5f-5b0fec6a5de4': getMockFieldMetadataEntity({
id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
workspaceId,
@ -336,7 +333,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
defaultValue: null,
description: 'Users who favorited this opportunity',
icon: 'IconStar',
settings: {
@ -351,7 +347,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-88ab-4138-98ce-80533bb423e3': getMockFieldMetadataEntity({
id: '20202020-88ab-4138-98ce-80533bb423e3',
workspaceId,
@ -359,7 +355,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'taskTargets',
label: 'Task Targets',
defaultValue: null,
description: 'Tasks targeting this opportunity',
icon: 'IconCheckbox',
settings: {
@ -374,7 +369,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-4258-422b-b35b-db3f090af8da': getMockFieldMetadataEntity({
id: '20202020-4258-422b-b35b-db3f090af8da',
workspaceId,
@ -382,7 +377,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'noteTargets',
label: 'Note Targets',
defaultValue: null,
description: 'Notes targeting this opportunity',
icon: 'IconNotes',
settings: {
@ -397,7 +391,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-16ca-40a7-a1ba-712975c916cd': getMockFieldMetadataEntity({
id: '20202020-16ca-40a7-a1ba-712975c916cd',
workspaceId,
@ -405,7 +399,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'attachments',
label: 'Attachments',
defaultValue: null,
description: 'Attachments for this opportunity',
icon: 'IconPaperclip',
settings: {
@ -420,7 +413,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
'20202020-92a5-47bf-a38d-c1c72b2c3e4d': getMockFieldMetadataEntity({
id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
workspaceId,
@ -428,7 +421,6 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
type: FieldMetadataType.RELATION,
name: 'timelineActivities',
label: 'Timeline Activities',
defaultValue: null,
description: 'Timeline activities for this opportunity',
icon: 'IconTimeline',
settings: {
@ -443,7 +435,7 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS =
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}) as FieldMetadataEntity,
}),
},
fieldIdByName: {
name: '20202020-c2f1-4435-adca-22931f8b41b6',

View File

@ -5,7 +5,6 @@ import {
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { checkFilterEnumValues } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/check-filter-enum-values';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
@ -26,7 +25,7 @@ describe('checkFilterEnumValues', () => {
});
const fieldsById: FieldMetadataMap = {
'field-select-id': completeFieldSelectMock as FieldMetadataEntity,
'field-select-id': completeFieldSelectMock,
};
const mockObjectMetadataWithFieldMaps = {

View File

@ -4,7 +4,6 @@ import {
objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { parseFilter } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
@ -39,8 +38,8 @@ describe('parseFilter', () => {
});
const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock as FieldMetadataEntity,
'field-text-id': completeFieldTextMock as FieldMetadataEntity,
'field-number-id': completeFieldNumberMock,
'field-text-id': completeFieldTextMock,
};
const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = {

View File

@ -7,7 +7,6 @@ import {
objectMetadataMapItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
@ -27,7 +26,7 @@ describe('FilterInputFactory', () => {
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
const completeFieldTextMock = getMockFieldMetadataEntity({
workspaceId,
@ -41,7 +40,7 @@ describe('FilterInputFactory', () => {
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
const completeFieldCurrencyMock = getMockFieldMetadataEntity({
workspaceId,
@ -55,7 +54,7 @@ describe('FilterInputFactory', () => {
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
});
const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock,

View File

@ -4,7 +4,6 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
import { getMockObjectMetadataItemWithFieldsMaps } from 'src/utils/__test__/get-object-metadata-item-with-fields-maps.mock';
@ -44,7 +43,7 @@ describe('computeCursorArgFilter', () => {
isNullable: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
'age-id': getMockFieldMetadataEntity({
workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
@ -56,7 +55,7 @@ describe('computeCursorArgFilter', () => {
isNullable: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
'fullname-id': getMockFieldMetadataEntity({
workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
@ -68,7 +67,7 @@ describe('computeCursorArgFilter', () => {
isNullable: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity,
}),
},
});