Deprecate FieldMetadataInterface (#13264)

# Introduction

From the moment replaced the FieldMetadataInterface definition to:
```ts
import { FieldMetadataType } from 'twenty-shared/types';

import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';

export type FieldMetadataInterface<
  T extends FieldMetadataType = FieldMetadataType,
> = FieldMetadataEntity<T>;
```
After this PR merge will create a new one removing the type and
replacing it to `FieldMetadataEntity`.
Did not renamed it here to avoid conflicts on naming + type issues fixs
within the same PR

## Field metadata entity RELATION or MORPH
Relations fields cannot be null for those field metadata entity instance
anymore, but are never for the others see
`packages/twenty-server/src/engine/metadata-modules/field-metadata/types/field-metadata-entity-test.type.ts`
( introduced TypeScript tests )

## Concerns
- TS_VECTOR is the most at risk with the `generatedType` and
`asExpression` removal from interface

## What's next
- `FielMetadataInterface` removal and rename ( see introduction )
- Depcrecating `ObjectMetadataInterface`
- Refactor `FieldMetadataEntity` optional fiels to be nullable only
- TO DIG `never` occurences on settings, defaultValue etc
- Some interfaces will be replaced by the `FlatFieldMetadata` when
deprecating the current sync and comparators tools
This commit is contained in:
Paul Rastoin
2025-07-21 11:30:18 +02:00
committed by GitHub
parent c2a5f95675
commit 47b60bd49f
67 changed files with 1780 additions and 769 deletions

View File

@ -71,7 +71,7 @@ export class FixStandardSelectFieldsPositionCommand extends ActiveOrSuspendedWor
let biggestPosition = -1; let biggestPosition = -1;
// Sort options by position for consistent processing // Sort options by position for consistent processing
const sortedOptions = [...taskStatusFieldMetadata.options].sort( const sortedOptions = (taskStatusFieldMetadata.options ?? []).sort(
(a, b) => a.position - b.position, (a, b) => a.position - b.position,
); );

View File

@ -58,7 +58,7 @@ export class AddEnqueuedStatusToWorkflowRunCommand extends ActiveOrSuspendedWork
// check if enqueued status is already in the field metadata options // check if enqueued status is already in the field metadata options
if ( if (
workflowRunStatusFieldMetadataOptions.some( workflowRunStatusFieldMetadataOptions?.some(
(option) => option.value === WorkflowRunStatus.ENQUEUED, (option) => option.value === WorkflowRunStatus.ENQUEUED,
) )
) { ) {
@ -72,7 +72,7 @@ export class AddEnqueuedStatusToWorkflowRunCommand extends ActiveOrSuspendedWork
`Would add enqueued status to workflow run status field metadata for workspace ${workspaceId}`, `Would add enqueued status to workflow run status field metadata for workspace ${workspaceId}`,
); );
} else { } else {
workflowRunStatusFieldMetadataOptions.push({ workflowRunStatusFieldMetadataOptions?.push({
value: WorkflowRunStatus.ENQUEUED, value: WorkflowRunStatus.ENQUEUED,
label: 'Enqueued', label: 'Enqueued',
position: 4, position: 4,

View File

@ -1,10 +1,15 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { FieldMetadataComplexOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
export const FIELD_LINKS_MOCK_NAME = 'fieldLinks'; export const FIELD_LINKS_MOCK_NAME = 'fieldLinks';
export const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency'; export const FIELD_CURRENCY_MOCK_NAME = 'fieldCurrency';
@ -13,194 +18,292 @@ export const FIELD_ACTOR_MOCK_NAME = 'fieldActor';
export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName'; export const FIELD_FULL_NAME_MOCK_NAME = 'fieldFullName';
export const FIELD_PHONES_MOCK_NAME = 'fieldPhones'; export const FIELD_PHONES_MOCK_NAME = 'fieldPhones';
export const fieldNumberMock = { const workspaceId = '20202020-0000-0000-0000-000000000000';
const objectMetadataId = '20202020-0000-0000-0000-000000000001';
export const fieldNumberMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldNumberId', id: 'fieldNumberId',
name: 'fieldNumber', name: 'fieldNumber',
type: FieldMetadataType.NUMBER, type: FieldMetadataType.NUMBER,
label: 'Field Number',
isNullable: false, isNullable: false,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
export const fieldTextMock = { export const fieldTextMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldTextId', id: 'fieldTextId',
name: 'fieldText', name: 'fieldText',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
label: 'Field Text',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
export const fieldCurrencyMock = { export const fieldCurrencyMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldCurrencyId', id: 'fieldCurrencyId',
name: FIELD_CURRENCY_MOCK_NAME, name: FIELD_CURRENCY_MOCK_NAME,
type: FieldMetadataType.CURRENCY, type: FieldMetadataType.CURRENCY,
label: 'Field Currency',
isNullable: true, isNullable: true,
defaultValue: { amountMicros: null, currencyCode: "''" }, defaultValue: { amountMicros: null, currencyCode: "''" },
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
export const fieldSelectMock = { export const fieldSelectMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldSelectId', id: 'fieldSelectId',
name: 'fieldSelect', name: 'fieldSelect',
type: FieldMetadataType.SELECT, type: FieldMetadataType.SELECT,
label: 'Field Select',
isNullable: true, isNullable: true,
defaultValue: 'OPTION_1', defaultValue: 'OPTION_1',
options: [ options: [
{ {
id: '9a519a86-422b-4598-88ae-78751353f683', id: '9a519a86-422b-4598-88ae-78751353f683',
color: 'red',
label: 'Opt 1', label: 'Opt 1',
value: 'OPTION_1', value: 'OPTION_1',
position: 0, position: 0,
color: 'red',
}, },
{ {
id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4',
color: 'purple',
label: 'Opt 2', label: 'Opt 2',
value: 'OPTION_2', value: 'OPTION_2',
position: 1, position: 1,
color: 'purple',
}, },
], ],
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldMultiSelectMock = { export const fieldMultiSelectMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldMultiSelectId', id: 'fieldMultiSelectId',
name: 'fieldMultiSelect', name: 'fieldMultiSelect',
type: FieldMetadataType.MULTI_SELECT, type: FieldMetadataType.MULTI_SELECT,
label: 'Field Multi Select',
isNullable: true, isNullable: true,
defaultValue: "{'OPTION_1'}", defaultValue: ['OPTION_1'],
options: [ options: [
{ {
id: '9a519a86-422b-4598-88ae-78751353f683', id: '9a519a86-422b-4598-88ae-78751353f683',
color: 'red',
label: 'Opt 1', label: 'Opt 1',
value: 'OPTION_1', value: 'OPTION_1',
position: 0, position: 0,
color: 'red',
}, },
{ {
id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4',
color: 'purple',
label: 'Opt 2', label: 'Opt 2',
value: 'OPTION_2', value: 'OPTION_2',
position: 1, position: 1,
color: 'purple',
}, },
], ],
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
export const fieldRelationMock = { export const fieldRelationMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldRelationId', id: 'fieldRelationId',
name: 'fieldRelation', name: 'fieldRelation',
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
label: 'Field Relation',
isNullable: true,
defaultValue: null,
settings: { settings: {
relationType: RelationType.MANY_TO_ONE, relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'fieldRelationId', joinColumnName: 'fieldRelationId',
onDelete: 'CASCADE', onDelete: RelationOnDeleteAction.CASCADE,
}, },
relationTargetObjectMetadata: { relationTargetObjectMetadata: {
id: 'relationTargetObjectId', id: 'relationTargetObjectId',
nameSingular: 'relationTargetObject', nameSingular: 'relationTargetObject',
namePlural: 'relationTargetObjects', namePlural: 'relationTargetObjects',
}, } as ObjectMetadataEntity,
relationTargetFieldMetadata: { relationTargetFieldMetadata: {
id: 'relationTargetFieldId', id: 'relationTargetFieldId',
name: 'relationTargetField', name: 'relationTargetField',
}, } as FieldMetadataEntity,
isNullable: true, isLabelSyncedWithName: true,
defaultValue: null, createdAt: new Date(),
}; updatedAt: new Date(),
});
const fieldLinksMock = { export const fieldLinksMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldLinksId', id: 'fieldLinksId',
name: FIELD_LINKS_MOCK_NAME, name: FIELD_LINKS_MOCK_NAME,
type: FieldMetadataType.LINKS, type: FieldMetadataType.LINKS,
label: 'Field Links',
isNullable: false, isNullable: false,
defaultValue: [ defaultValue: {
{ primaryLinkLabel: '', primaryLinkUrl: '', secondaryLinks: [] }, primaryLinkLabel: '',
], primaryLinkUrl: '',
}; secondaryLinks: [],
},
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldUuidMock = { export const fieldUuidMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldUuidId', id: 'fieldUuidId',
name: 'fieldUuid', name: 'fieldUuid',
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
label: 'Field UUID',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldDateTimeMock = { export const fieldDateTimeMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldDateTimeId', id: 'fieldDateTimeId',
name: 'fieldDateTime', name: 'fieldDateTime',
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
label: 'Field Date Time',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldDateMock = { export const fieldDateMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldDateId', id: 'fieldDateId',
name: 'fieldDate', name: 'fieldDate',
type: FieldMetadataType.DATE, type: FieldMetadataType.DATE,
label: 'Field Date',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldBooleanMock = { export const fieldBooleanMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldBooleanId', id: 'fieldBooleanId',
name: 'fieldBoolean', name: 'fieldBoolean',
type: FieldMetadataType.BOOLEAN, type: FieldMetadataType.BOOLEAN,
label: 'Field Boolean',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldNumericMock = { export const fieldNumericMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldNumericId', id: 'fieldNumericId',
name: 'fieldNumeric', name: 'fieldNumeric',
type: FieldMetadataType.NUMERIC, type: FieldMetadataType.NUMERIC,
label: 'Field Numeric',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldFullNameMock = { export const fieldFullNameMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldFullNameId', id: 'fieldFullNameId',
name: FIELD_FULL_NAME_MOCK_NAME, name: FIELD_FULL_NAME_MOCK_NAME,
type: FieldMetadataType.FULL_NAME, type: FieldMetadataType.FULL_NAME,
label: 'Field Full Name',
isNullable: true, isNullable: true,
defaultValue: { firstName: '', lastName: '' }, defaultValue: { firstName: '', lastName: '' },
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldRatingMock = { export const fieldRatingMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldRatingId', id: 'fieldRatingId',
name: 'fieldRating', name: 'fieldRating',
type: FieldMetadataType.RATING, type: FieldMetadataType.RATING,
label: 'Field Rating',
isNullable: true, isNullable: true,
defaultValue: 'RATING_1', defaultValue: 'RATING_1',
options: [ options: [
{ {
id: '9a519a86-422b-4598-88ae-78751353f683', id: '9a519a86-422b-4598-88ae-78751353f683',
color: 'red',
label: 'Opt 1', label: 'Opt 1',
value: 'RATING_1', value: 'RATING_1',
position: 0, position: 0,
color: 'red',
}, },
{ {
id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4', id: '33f28d51-bc82-4e1d-ae4b-d9e4c0ed0ab4',
color: 'purple',
label: 'Opt 2', label: 'Opt 2',
value: 'RATING_2', value: 'RATING_2',
position: 1, position: 1,
color: 'purple',
}, },
], ] as FieldMetadataComplexOption[],
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldPositionMock = { export const fieldPositionMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldPositionId', id: 'fieldPositionId',
name: 'fieldPosition', name: 'fieldPosition',
type: FieldMetadataType.POSITION, type: FieldMetadataType.POSITION,
label: 'Field Position',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldAddressMock = { export const fieldAddressMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldAddressId', id: 'fieldAddressId',
name: FIELD_ADDRESS_MOCK_NAME, name: FIELD_ADDRESS_MOCK_NAME,
type: FieldMetadataType.ADDRESS, type: FieldMetadataType.ADDRESS,
label: 'Field Address',
isNullable: true, isNullable: true,
defaultValue: { defaultValue: {
addressStreet1: '', addressStreet1: '',
@ -212,65 +315,105 @@ const fieldAddressMock = {
addressLat: null, addressLat: null,
addressLng: null, addressLng: null,
}, },
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldRawJsonMock = { export const fieldRawJsonMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldRawJsonId', id: 'fieldRawJsonId',
name: 'fieldRawJson', name: 'fieldRawJson',
type: FieldMetadataType.RAW_JSON, type: FieldMetadataType.RAW_JSON,
label: 'Field Raw JSON',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldRichTextMock = { export const fieldRichTextMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldRichTextId', id: 'fieldRichTextId',
name: 'fieldRichText', name: 'fieldRichText',
type: FieldMetadataType.RICH_TEXT, type: FieldMetadataType.RICH_TEXT,
label: 'Field Rich Text',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldActorMock = { export const fieldActorMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldActorId', id: 'fieldActorId',
name: FIELD_ACTOR_MOCK_NAME, name: FIELD_ACTOR_MOCK_NAME,
type: FieldMetadataType.ACTOR, type: FieldMetadataType.ACTOR,
label: 'Field Actor',
isNullable: true, isNullable: true,
defaultValue: { defaultValue: {
source: FieldActorSource.MANUAL, source: FieldActorSource.MANUAL,
name: '', name: '',
}, },
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldEmailsMock = { export const fieldEmailsMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldEmailsId', id: 'fieldEmailsId',
name: 'fieldEmails', name: 'fieldEmails',
type: FieldMetadataType.EMAILS, type: FieldMetadataType.EMAILS,
label: 'Field Emails',
isNullable: false, isNullable: false,
defaultValue: [{ primaryEmail: '', additionalEmails: {} }], defaultValue: {
}; primaryEmail: '',
additionalEmails: {},
},
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldArrayMock = { export const fieldArrayMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldArrayId', id: 'fieldArrayId',
name: 'fieldArray', name: 'fieldArray',
type: FieldMetadataType.ARRAY, type: FieldMetadataType.ARRAY,
label: 'Field Array',
isNullable: true, isNullable: true,
defaultValue: null, defaultValue: null,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const fieldPhonesMock = { export const fieldPhonesMock = getMockFieldMetadataEntity({
workspaceId,
objectMetadataId,
id: 'fieldPhonesId', id: 'fieldPhonesId',
name: FIELD_PHONES_MOCK_NAME, name: FIELD_PHONES_MOCK_NAME,
type: FieldMetadataType.PHONES, type: FieldMetadataType.PHONES,
label: 'Field Phones',
isNullable: false, isNullable: false,
defaultValue: [ defaultValue: {
{ primaryPhoneNumber: '',
primaryPhoneNumber: '', primaryPhoneCountryCode: '',
primaryPhoneCountryCode: '', primaryPhoneCallingCode: '',
primaryPhoneCallingCode: '', additionalPhones: {},
additionalPhones: {}, },
}, isLabelSyncedWithName: true,
], createdAt: new Date(),
}; updatedAt: new Date(),
});
export const fields = [ export const fields = [
fieldUuidMock, fieldUuidMock,
@ -297,46 +440,47 @@ export const fields = [
fieldArrayMock, fieldArrayMock,
]; ];
export const objectMetadataItemMock = { export const objectMetadataItemMock: ObjectMetadataEntity = {
targetTableName: 'testingObject', id: objectMetadataId,
id: 'mockObjectId', workspaceId,
nameSingular: 'objectName', nameSingular: 'objectName',
namePlural: 'objectsName', namePlural: 'objectNames',
labelSingular: 'Object Name',
labelPlural: 'Object Names',
description: 'Object description',
icon: 'Icon123',
targetTableName: 'DEPRECATED',
isCustom: false,
isRemote: false,
isActive: true,
isSystem: false,
isAuditLogged: true,
isSearchable: true,
fields, fields,
createdAt: new Date(),
updatedAt: new Date(),
} as ObjectMetadataEntity; } as ObjectMetadataEntity;
export const objectMetadataMapItemMock = { export const objectMetadataMapItemMock: ObjectMetadataItemWithFieldMaps = {
id: 'mockObjectId', ...objectMetadataItemMock,
icon: 'Icon123', fieldsById: fields.reduce(
nameSingular: 'objectName', (acc, field) => ({
namePlural: 'objectsName', ...acc,
fieldsById: fields.reduce((acc, field) => { [field.id]: field,
// @ts-expect-error legacy noImplicitAny }),
acc[field.id] = field; {},
),
return acc; fieldIdByName: fields.reduce(
}, {}), (acc, field) => ({
fieldIdByName: fields.reduce((acc, field) => { ...acc,
// @ts-expect-error legacy noImplicitAny [field.name]: field.id,
acc[field.name] = field; }),
{},
return acc; ),
}, {}),
fieldIdByJoinColumnName: {}, fieldIdByJoinColumnName: {},
labelSingular: 'Object',
labelPlural: 'Objects',
workspaceId: 'mockWorkspaceId',
isCustom: false,
isSystem: false,
targetTableName: '',
indexMetadatas: [], indexMetadatas: [],
isActive: true, };
isRemote: false, export const objectMetadataMapsMock: ObjectMetadataMaps = {
isAuditLogged: false,
isSearchable: false,
} satisfies ObjectMetadataItemWithFieldMaps;
export const objectMetadataMapsMock = {
byId: { byId: {
[objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItemMock, [objectMetadataMapItemMock.id || 'mock-id']: objectMetadataMapItemMock,
}, },

View File

@ -1,7 +1,11 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type'; import { WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
export const mockPersonObjectMetadataWithFieldMaps = ( export const mockPersonObjectMetadataWithFieldMaps = (
duplicateCriteria: WorkspaceEntityDuplicateCriteria[], duplicateCriteria: WorkspaceEntityDuplicateCriteria[],
@ -24,7 +28,7 @@ export const mockPersonObjectMetadataWithFieldMaps = (
duplicateCriteria: duplicateCriteria, duplicateCriteria: duplicateCriteria,
labelIdentifierFieldMetadataId: '', labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId,
indexMetadatas: [], indexMetadatas: [],
fieldIdByName: { fieldIdByName: {
name: 'name-id', name: 'name-id',
@ -34,9 +38,10 @@ export const mockPersonObjectMetadataWithFieldMaps = (
}, },
fieldIdByJoinColumnName: {}, fieldIdByJoinColumnName: {},
fieldsById: { fieldsById: {
'name-id': { 'name-id': getMockFieldMetadataEntity({
id: '', workspaceId,
objectMetadataId: '', objectMetadataId: '',
id: 'name-id',
type: FieldMetadataType.FULL_NAME, type: FieldMetadataType.FULL_NAME,
name: 'name', name: 'name',
label: 'Name', label: 'Name',
@ -44,18 +49,18 @@ export const mockPersonObjectMetadataWithFieldMaps = (
lastName: "''", lastName: "''",
firstName: "''", firstName: "''",
}, },
description: 'Contacts name', description: "Contact's name",
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
'emails-id': { 'emails-id': getMockFieldMetadataEntity({
id: '', workspaceId,
objectMetadataId: '', objectMetadataId: '',
id: 'emails-id',
type: FieldMetadataType.EMAILS, type: FieldMetadataType.EMAILS,
name: 'emails', name: 'emails',
label: 'Emails', label: 'Emails',
@ -63,49 +68,48 @@ export const mockPersonObjectMetadataWithFieldMaps = (
primaryEmail: "''", primaryEmail: "''",
additionalEmails: null, additionalEmails: null,
}, },
description: 'Contacts Emails', description: "Contact's Emails",
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
'linkedinLink-id': { 'linkedinLink-id': getMockFieldMetadataEntity({
id: '', workspaceId,
objectMetadataId: '', objectMetadataId: '',
id: 'linkedinLink-id',
type: FieldMetadataType.LINKS, type: FieldMetadataType.LINKS,
name: 'linkedinLink', name: 'linkedinLink',
label: 'Linkedin', label: 'Linkedin',
defaultValue: { defaultValue: {
primaryLinkUrl: "''", primaryLinkUrl: "''",
secondaryLinks: "'[]'", secondaryLinks: [],
primaryLinkLabel: "''", primaryLinkLabel: "''",
}, },
description: 'Contacts Linkedin account', description: "Contact's Linkedin account",
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
'jobTitle-id': { 'jobTitle-id': getMockFieldMetadataEntity({
id: '', workspaceId,
objectMetadataId: '', objectMetadataId: '',
id: 'jobTitle-id',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'jobTitle', name: 'jobTitle',
label: 'Job Title', label: 'Job Title',
defaultValue: "''", defaultValue: "''",
description: 'Contacts job title', description: "Contact's job title",
isCustom: false, isCustom: false,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
}, },
}); });

View File

@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { GraphQLEnumType } from 'graphql'; import { GraphQLEnumType } from 'graphql';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
@ -54,11 +55,13 @@ export class EnumTypeDefinitionFactory {
): GraphQLEnumType { ): GraphQLEnumType {
// FixMe: It's a hack until Typescript get fixed on union types for reduce function // FixMe: It's a hack until Typescript get fixed on union types for reduce function
// https://github.com/microsoft/TypeScript/issues/36390 // https://github.com/microsoft/TypeScript/issues/36390
const enumOptions = transformEnumValue(fieldMetadata.options) as Array< const enumOptions = transformEnumValue(
FieldMetadataDefaultOption | FieldMetadataComplexOption fieldMetadata.options ?? undefined,
>; ) as
| Array<FieldMetadataDefaultOption | FieldMetadataComplexOption>
| undefined;
if (!enumOptions) { if (!isDefined(enumOptions)) {
this.logger.error( this.logger.error(
`Enum options are not defined for ${fieldMetadata.name}`, `Enum options are not defined for ${fieldMetadata.name}`,
{ {

View File

@ -117,7 +117,7 @@ export class RelationConnectInputTypeDefinitionFactory {
} else { } else {
const scalarType = this.typeMapperService.mapToScalarType( const scalarType = this.typeMapperService.mapToScalarType(
field.type, field.type,
field.settings, field.settings ?? undefined,
field.name === 'id', field.name === 'id',
); );

View File

@ -108,7 +108,7 @@ const getTypeFactoryOptions = <T extends FieldMetadataType>(
) => { ) => {
return isInputTypeDefinitionKind(kind) return isInputTypeDefinitionKind(kind)
? { ? {
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable ?? undefined,
defaultValue: fieldMetadata.defaultValue, defaultValue: fieldMetadata.defaultValue,
isArray: isArray:
kind !== InputTypeDefinitionKind.Filter && kind !== InputTypeDefinitionKind.Filter &&
@ -117,7 +117,7 @@ const getTypeFactoryOptions = <T extends FieldMetadataType>(
isIdField: fieldMetadata.name === 'id', isIdField: fieldMetadata.name === 'id',
} }
: { : {
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable ?? undefined,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings, settings: fieldMetadata.settings,
// Scalar type is already defined in the entity itself. // Scalar type is already defined in the entity itself.

View File

@ -34,6 +34,7 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util'; import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
import { isFieldMetadataInterfaceOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
export interface PageInfo { export interface PageInfo {
hasNextPage?: boolean; hasNextPage?: boolean;
@ -159,7 +160,7 @@ export abstract class RestApiBaseHandler {
Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach( Object.values(objectMetadata.objectMetadataMapItem.fieldsById).forEach(
(field) => { (field) => {
if (field.type === FieldMetadataType.RELATION) { if (isFieldMetadataInterfaceOfType(field, FieldMetadataType.RELATION)) {
if ( if (
depth === MAX_DEPTH && depth === MAX_DEPTH &&
isDefined(field.relationTargetObjectMetadataId) isDefined(field.relationTargetObjectMetadataId)

View File

@ -1,30 +1,31 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { import {
fieldNumberMock, fieldNumberMock,
objectMetadataItemMock, objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } 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 { 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 { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('getFieldType', () => { describe('getFieldType', () => {
const completeFieldNumberMock: FieldMetadataInterface = { const completeFieldNumberMock = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: 'field-number-id', id: 'field-number-id',
type: fieldNumberMock.type, type: fieldNumberMock.type,
name: fieldNumberMock.name, name: fieldNumberMock.name,
label: 'Field Number', label: 'Field Number',
objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock, 'field-number-id': completeFieldNumberMock as FieldMetadataEntity,
}; };
const mockObjectMetadataWithFieldMaps = { const mockObjectMetadataWithFieldMaps = {

View File

@ -1,7 +1,6 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { import {
@ -11,54 +10,59 @@ import {
objectMetadataItemMock, objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } 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 { 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 { 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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('mapFieldMetadataToGraphqlQuery', () => { describe('mapFieldMetadataToGraphqlQuery', () => {
const typedFieldNumberMock: FieldMetadataInterface = { const typedFieldNumberMock = getMockFieldMetadataEntity({
id: 'field-number-id', workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000002',
name: fieldNumberMock.name, name: fieldNumberMock.name,
type: fieldNumberMock.type, type: fieldNumberMock.type,
label: 'Field Number', label: 'Field Number',
objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const typedFieldTextMock: FieldMetadataInterface = { const typedFieldTextMock = getMockFieldMetadataEntity({
id: 'field-text-id', workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000003',
name: fieldTextMock.name, name: fieldTextMock.name,
type: fieldTextMock.type, type: fieldTextMock.type,
label: 'Field Text', label: 'Field Text',
objectMetadataId: 'object-metadata-id',
isNullable: fieldTextMock.isNullable, isNullable: fieldTextMock.isNullable,
defaultValue: fieldTextMock.defaultValue, defaultValue: fieldTextMock.defaultValue,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const typedFieldCurrencyMock: FieldMetadataInterface = { const typedFieldCurrencyMock = getMockFieldMetadataEntity({
id: 'field-currency-id', workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000004',
name: fieldCurrencyMock.name, name: fieldCurrencyMock.name,
type: fieldCurrencyMock.type, type: fieldCurrencyMock.type,
label: 'Field Currency', label: 'Field Currency',
objectMetadataId: 'object-metadata-id',
isNullable: fieldCurrencyMock.isNullable, isNullable: fieldCurrencyMock.isNullable,
defaultValue: fieldCurrencyMock.defaultValue, defaultValue: fieldCurrencyMock.defaultValue,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-number-id': typedFieldNumberMock, 'field-number-id': typedFieldNumberMock as FieldMetadataEntity,
'field-text-id': typedFieldTextMock, 'field-text-id': typedFieldTextMock as FieldMetadataEntity,
'field-currency-id': typedFieldCurrencyMock, 'field-currency-id': typedFieldCurrencyMock as FieldMetadataEntity,
}; };
const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = { const typedObjectMetadataItem: ObjectMetadataItemWithFieldMaps = {
@ -86,19 +90,19 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
expect( expect(
mapFieldMetadataToGraphqlQuery( mapFieldMetadataToGraphqlQuery(
objectMetadataMapsMock, objectMetadataMapsMock,
typedFieldNumberMock, typedFieldNumberMock as FieldMetadataEntity,
), ),
).toEqual('fieldNumber'); ).toEqual('fieldNumber');
expect( expect(
mapFieldMetadataToGraphqlQuery( mapFieldMetadataToGraphqlQuery(
objectMetadataMapsMock, objectMetadataMapsMock,
typedFieldTextMock, typedFieldTextMock as FieldMetadataEntity,
), ),
).toEqual('fieldText'); ).toEqual('fieldText');
expect( expect(
mapFieldMetadataToGraphqlQuery( mapFieldMetadataToGraphqlQuery(
objectMetadataMapsMock, objectMetadataMapsMock,
typedFieldCurrencyMock, typedFieldCurrencyMock as FieldMetadataEntity,
), ),
).toEqual(` ).toEqual(`
fieldCurrency fieldCurrency
@ -112,29 +116,25 @@ describe('mapFieldMetadataToGraphqlQuery', () => {
describe('should handle all field metadata types', () => { describe('should handle all field metadata types', () => {
Object.values(FieldMetadataType).forEach((fieldMetadataType) => { Object.values(FieldMetadataType).forEach((fieldMetadataType) => {
it(`with field type ${fieldMetadataType}`, () => { it(`with field type ${fieldMetadataType}`, () => {
const field: FieldMetadataInterface = { const field = getMockFieldMetadataEntity({
id: 'test-field-id', workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000005',
type: fieldMetadataType, type: fieldMetadataType,
name: 'toObjectMetadataName', name: 'toObjectMetadataName',
label: 'Test Field', label: 'Test Field',
objectMetadataId: 'object-metadata-id',
isNullable: true, isNullable: true,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; settings:
fieldMetadataType === FieldMetadataType.RELATION ||
if (fieldMetadataType === FieldMetadataType.RELATION) { fieldMetadataType === FieldMetadataType.MORPH_RELATION
field.settings = { ? ({
relationType: RelationType.MANY_TO_ONE, relationType: RelationType.MANY_TO_ONE,
} as FieldMetadataDefaultSettings; } as FieldMetadataDefaultSettings)
} : undefined,
});
if (fieldMetadataType === FieldMetadataType.MORPH_RELATION) {
field.settings = {
relationType: RelationType.MANY_TO_ONE,
} as FieldMetadataDefaultSettings;
}
expect( expect(
mapFieldMetadataToGraphqlQuery(objectMetadataMapsMock, field), mapFieldMetadataToGraphqlQuery(objectMetadataMapsMock, field),

View File

@ -1,11 +1,18 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { DateDisplayFormat } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
const workspaceId = '20202020-1c25-4d02-bf25-6aeccf7ea419';
const objectMetadataId = '20202020-6e2c-42f6-a83c-cc58d776af88';
export const OPPORTUNITY_WITH_FIELDS_MAPS = { export const OPPORTUNITY_WITH_FIELDS_MAPS = {
id: '20202020-6e2c-42f6-a83c-cc58d776af88', id: objectMetadataId,
nameSingular: 'opportunity', nameSingular: 'opportunity',
namePlural: 'opportunities', namePlural: 'opportunities',
labelSingular: 'Opportunity', labelSingular: 'Opportunity',
@ -21,12 +28,13 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSearchable: true, isSearchable: true,
labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6', labelIdentifierFieldMetadataId: '20202020-c2f1-4435-adca-22931f8b41b6',
imageIdentifierFieldMetadataId: null, imageIdentifierFieldMetadataId: null,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', workspaceId,
indexMetadatas: [], // unused indexMetadatas: [],
fieldsById: { fieldsById: {
'20202020-c2f1-4435-adca-22931f8b41b6': { '20202020-c2f1-4435-adca-22931f8b41b6': getMockFieldMetadataEntity({
id: '20202020-c2f1-4435-adca-22931f8b41b6', id: '20202020-c2f1-4435-adca-22931f8b41b6',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'name', name: 'name',
label: 'Name', label: 'Name',
@ -38,14 +46,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: false, isSystem: false,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-5eef-417a-b517-ebeedaa8e10b': { '20202020-5eef-417a-b517-ebeedaa8e10b': getMockFieldMetadataEntity({
id: '20202020-5eef-417a-b517-ebeedaa8e10b', id: '20202020-5eef-417a-b517-ebeedaa8e10b',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.CURRENCY, type: FieldMetadataType.CURRENCY,
name: 'amount', name: 'amount',
label: 'Amount', label: 'Amount',
@ -57,14 +65,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-597c-44d3-98ec-ea71aea5256b': { '20202020-597c-44d3-98ec-ea71aea5256b': getMockFieldMetadataEntity({
id: '20202020-597c-44d3-98ec-ea71aea5256b', id: '20202020-597c-44d3-98ec-ea71aea5256b',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
name: 'closeDate', name: 'closeDate',
label: 'Close date', label: 'Close date',
@ -76,14 +84,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-9b94-454a-94ca-8afb09c68faf': { '20202020-9b94-454a-94ca-8afb09c68faf': getMockFieldMetadataEntity({
id: '20202020-9b94-454a-94ca-8afb09c68faf', id: '20202020-9b94-454a-94ca-8afb09c68faf',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.SELECT, type: FieldMetadataType.SELECT,
name: 'stage', name: 'stage',
label: 'Stage', label: 'Stage',
@ -132,14 +140,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: false, isSystem: false,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-30a5-4d8e-9b93-12d31ece0aaa': { '20202020-30a5-4d8e-9b93-12d31ece0aaa': getMockFieldMetadataEntity({
id: '20202020-30a5-4d8e-9b93-12d31ece0aaa', id: '20202020-30a5-4d8e-9b93-12d31ece0aaa',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.POSITION, type: FieldMetadataType.POSITION,
name: 'position', name: 'position',
label: 'Position', label: 'Position',
@ -151,18 +159,18 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: true, isSystem: true,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-f95f-424f-ab32-65961e8e9635': { '20202020-f95f-424f-ab32-65961e8e9635': getMockFieldMetadataEntity({
id: '20202020-f95f-424f-ab32-65961e8e9635', id: '20202020-f95f-424f-ab32-65961e8e9635',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.ACTOR, type: FieldMetadataType.ACTOR,
name: 'createdBy', name: 'createdBy',
label: 'Created by', label: 'Created by',
defaultValue: { name: "'System'", source: "'MANUAL'", context: {} }, defaultValue: { name: "'System'", source: "'MANUAL'" },
description: 'The creator of the record', description: 'The creator of the record',
icon: 'IconCreativeCommonsSa', icon: 'IconCreativeCommonsSa',
isCustom: false, isCustom: false,
@ -170,14 +178,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: false, isSystem: false,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-5e10-4780-babb-38a465ac546c': { '20202020-5e10-4780-babb-38a465ac546c': getMockFieldMetadataEntity({
id: '20202020-5e10-4780-babb-38a465ac546c', id: '20202020-5e10-4780-babb-38a465ac546c',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'searchVector', name: 'searchVector',
label: 'Search vector', label: 'Search vector',
@ -189,14 +197,14 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: true, isSystem: true,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-8f4a-4f8d-822e-90fe72f75b79': { '20202020-8f4a-4f8d-822e-90fe72f75b79': getMockFieldMetadataEntity({
id: '20202020-8f4a-4f8d-822e-90fe72f75b79', id: '20202020-8f4a-4f8d-822e-90fe72f75b79',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: 'id', name: 'id',
label: 'Id', label: 'Id',
@ -208,233 +216,233 @@ export const OPPORTUNITY_WITH_FIELDS_MAPS = {
isSystem: true, isSystem: true,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-f120-4b59-b239-f7f1d8eb243e': { '20202020-f120-4b59-b239-f7f1d8eb243e': getMockFieldMetadataEntity({
id: '20202020-f120-4b59-b239-f7f1d8eb243e', id: '20202020-f120-4b59-b239-f7f1d8eb243e',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
name: 'createdAt', name: 'createdAt',
label: 'Creation date', label: 'Creation date',
defaultValue: 'now', defaultValue: 'now',
description: 'Creation date', description: 'Creation date',
icon: 'IconCalendar', icon: 'IconCalendar',
settings: { displayFormat: 'RELATIVE' } as any, settings: { displayFormat: DateDisplayFormat.RELATIVE },
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: false, isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-dcc8-4318-9756-b87377692561': { '20202020-dcc8-4318-9756-b87377692561': getMockFieldMetadataEntity({
id: '20202020-dcc8-4318-9756-b87377692561', id: '20202020-dcc8-4318-9756-b87377692561',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
name: 'updatedAt', name: 'updatedAt',
label: 'Last update', label: 'Last update',
defaultValue: 'now', defaultValue: 'now',
description: 'Last time the record was changed', description: 'Last time the record was changed',
icon: 'IconCalendarClock', icon: 'IconCalendarClock',
settings: { displayFormat: 'RELATIVE' } as any, settings: { displayFormat: DateDisplayFormat.RELATIVE },
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: false, isNullable: false,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: false, isLabelSyncedWithName: false,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-1694-4f8b-8760-61a5ff330022': { '20202020-1694-4f8b-8760-61a5ff330022': getMockFieldMetadataEntity({
id: '20202020-1694-4f8b-8760-61a5ff330022', id: '20202020-1694-4f8b-8760-61a5ff330022',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.DATE_TIME, type: FieldMetadataType.DATE_TIME,
name: 'deletedAt', name: 'deletedAt',
label: 'Deleted at', label: 'Deletion date',
defaultValue: null, defaultValue: null,
description: 'Date when the record was deleted', description: 'Record deletion date',
icon: 'IconCalendarMinus', icon: 'IconCalendarOff',
settings: { displayFormat: 'RELATIVE' } as any, settings: { displayFormat: DateDisplayFormat.RELATIVE },
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: false,
isLabelSyncedWithName: true,
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-4f52-4dea-a116-723f9bf7f082': { '20202020-4f52-4dea-a116-723f9bf7f082': getMockFieldMetadataEntity({
id: '20202020-4f52-4dea-a116-723f9bf7f082', id: '20202020-4f52-4dea-a116-723f9bf7f082',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'pointOfContact', name: 'pointOfContact',
label: 'Point of Contact', label: 'Point of Contact',
defaultValue: null, defaultValue: null,
description: 'Opportunity point of contact', description: 'The point of contact for this opportunity',
icon: 'IconUser', icon: 'IconUser',
settings: { settings: {
onDelete: 'SET_NULL', relationType: RelationType.MANY_TO_ONE,
relationType: 'MANY_TO_ONE',
joinColumnName: 'pointOfContactId', joinColumnName: 'pointOfContactId',
} as any, onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-a36b-4889-97d4-63a578423688',
relationTargetObjectMetadataId: '20202020-6799-4a38-92d3-8e844a7ea8ab',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-fc02-4be2-be1a-e121daf5400d': { '20202020-fc02-4be2-be1a-e121daf5400d': getMockFieldMetadataEntity({
id: '20202020-fc02-4be2-be1a-e121daf5400d', id: '20202020-fc02-4be2-be1a-e121daf5400d',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'company', name: 'company',
label: 'Company', label: 'Company',
defaultValue: null, defaultValue: null,
description: 'Opportunity company', description: 'The company this opportunity is associated with',
icon: 'IconBuildingSkyscraper', icon: 'IconBuildingSkyscraper',
settings: { settings: {
onDelete: 'SET_NULL', relationType: RelationType.MANY_TO_ONE,
relationType: 'MANY_TO_ONE',
joinColumnName: 'companyId', joinColumnName: 'companyId',
} as any, onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-bd16-4f63-8165-0a7f5d78170d',
relationTargetObjectMetadataId: '20202020-0be8-4764-8e0d-7a2e1c66f78c',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-fd9f-48f0-bd5f-5b0fec6a5de4': { '20202020-fd9f-48f0-bd5f-5b0fec6a5de4': getMockFieldMetadataEntity({
id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4', id: '20202020-fd9f-48f0-bd5f-5b0fec6a5de4',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'favorites', name: 'favorites',
label: 'Favorites', label: 'Favorites',
defaultValue: null, defaultValue: null,
description: 'Favorites linked to the opportunity', description: 'Users who favorited this opportunity',
icon: 'IconHeart', icon: 'IconStar',
settings: { relationType: RelationType.ONE_TO_MANY } as any, settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: true, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-7c3c-4149-b400-5a958910c7a2',
relationTargetObjectMetadataId: '20202020-df10-42dc-baed-ea77db7ad96c',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-88ab-4138-98ce-80533bb423e3': { '20202020-88ab-4138-98ce-80533bb423e3': getMockFieldMetadataEntity({
id: '20202020-88ab-4138-98ce-80533bb423e3', id: '20202020-88ab-4138-98ce-80533bb423e3',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'taskTargets', name: 'taskTargets',
label: 'Tasks', label: 'Task Targets',
defaultValue: null, defaultValue: null,
description: 'Tasks tied to the opportunity', description: 'Tasks targeting this opportunity',
icon: 'IconCheckbox', icon: 'IconCheckbox',
settings: { relationType: RelationType.ONE_TO_MANY } as any, settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true,
isLabelSyncedWithName: false,
relationTargetFieldMetadataId: '20202020-eb77-4b1c-b6a6-d5dcd13b1634',
relationTargetObjectMetadataId: '20202020-3af1-4c4f-90f4-cd43c53f7f41',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-4258-422b-b35b-db3f090af8da': { '20202020-4258-422b-b35b-db3f090af8da': getMockFieldMetadataEntity({
id: '20202020-4258-422b-b35b-db3f090af8da', id: '20202020-4258-422b-b35b-db3f090af8da',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'noteTargets', name: 'noteTargets',
label: 'Notes', label: 'Note Targets',
defaultValue: null, defaultValue: null,
description: 'Notes tied to the opportunity', description: 'Notes targeting this opportunity',
icon: 'IconNotes', icon: 'IconNotes',
settings: { relationType: RelationType.ONE_TO_MANY } as any, settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419', isLabelSyncedWithName: true,
isLabelSyncedWithName: false,
relationTargetFieldMetadataId: '20202020-d927-4c91-9893-28cea5aff979',
relationTargetObjectMetadataId: '20202020-8a5e-4dea-868b-71611e718a73',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-16ca-40a7-a1ba-712975c916cd': { '20202020-16ca-40a7-a1ba-712975c916cd': getMockFieldMetadataEntity({
id: '20202020-16ca-40a7-a1ba-712975c916cd', id: '20202020-16ca-40a7-a1ba-712975c916cd',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'attachments', name: 'attachments',
label: 'Attachments', label: 'Attachments',
defaultValue: null, defaultValue: null,
description: 'Attachments linked to the opportunity', description: 'Attachments for this opportunity',
icon: 'IconFileImport', icon: 'IconPaperclip',
settings: { relationType: RelationType.ONE_TO_MANY } as any, settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-9236-427a-8a8a-a2296b93b542',
relationTargetObjectMetadataId: '20202020-3005-4c93-a04c-2941f7424f54',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
'20202020-92a5-47bf-a38d-c1c72b2c3e4d': { '20202020-92a5-47bf-a38d-c1c72b2c3e4d': getMockFieldMetadataEntity({
id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d', id: '20202020-92a5-47bf-a38d-c1c72b2c3e4d',
objectMetadataId: '20202020-6e2c-42f6-a83c-cc58d776af88', workspaceId,
objectMetadataId,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'timelineActivities', name: 'timelineActivities',
label: 'Timeline Activities', label: 'Timeline Activities',
defaultValue: null, defaultValue: null,
description: 'Timeline Activities linked to the opportunity.', description: 'Timeline activities for this opportunity',
icon: 'IconTimelineEvent', icon: 'IconTimeline',
settings: { relationType: RelationType.ONE_TO_MANY } as any, settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
},
isCustom: false, isCustom: false,
isActive: true, isActive: true,
isSystem: false, isSystem: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
relationTargetFieldMetadataId: '20202020-2d54-41c7-b886-29deca3c28d5',
relationTargetObjectMetadataId: '20202020-a89e-4e4d-b3d9-c3f99e7c7483',
createdAt: new Date('2025-06-27T12:55:13.271Z'), createdAt: new Date('2025-06-27T12:55:13.271Z'),
updatedAt: new Date('2025-06-27T12:55:13.271Z'), updatedAt: new Date('2025-06-27T12:55:13.271Z'),
}, }) as FieldMetadataEntity,
}, },
fieldIdByName: { fieldIdByName: {
name: '20202020-c2f1-4435-adca-22931f8b41b6', name: '20202020-c2f1-4435-adca-22931f8b41b6',

View File

@ -1,31 +1,32 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { import {
fieldSelectMock, fieldSelectMock,
objectMetadataItemMock, objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } 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 { 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 { FieldMetadataMap } from 'src/engine/metadata-modules/types/field-metadata-map';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('checkFilterEnumValues', () => { describe('checkFilterEnumValues', () => {
const completeFieldSelectMock: FieldMetadataInterface = { const completeFieldSelectMock = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: 'field-select-id', id: 'field-select-id',
type: fieldSelectMock.type, type: fieldSelectMock.type,
name: fieldSelectMock.name, name: fieldSelectMock.name,
label: 'Field Select', label: 'Field Select',
objectMetadataId: 'object-metadata-id',
isNullable: fieldSelectMock.isNullable, isNullable: fieldSelectMock.isNullable,
defaultValue: fieldSelectMock.defaultValue, defaultValue: fieldSelectMock.defaultValue,
options: fieldSelectMock.options, options: fieldSelectMock.options,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-select-id': completeFieldSelectMock, 'field-select-id': completeFieldSelectMock as FieldMetadataEntity,
}; };
const mockObjectMetadataWithFieldMaps = { const mockObjectMetadataWithFieldMaps = {

View File

@ -1,44 +1,46 @@
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { import {
fieldNumberMock, fieldNumberMock,
fieldTextMock, fieldTextMock,
objectMetadataItemMock, objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } 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 { 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 { 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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('parseFilter', () => { describe('parseFilter', () => {
const completeFieldNumberMock: FieldMetadataInterface = { const completeFieldNumberMock = getMockFieldMetadataEntity({
id: 'field-number-id', workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000002',
type: fieldNumberMock.type, type: fieldNumberMock.type,
name: fieldNumberMock.name, name: fieldNumberMock.name,
label: 'Field Number', label: 'Field Number',
objectMetadataId: 'object-metadata-id',
isNullable: fieldNumberMock.isNullable, isNullable: fieldNumberMock.isNullable,
defaultValue: fieldNumberMock.defaultValue, defaultValue: fieldNumberMock.defaultValue,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const completeFieldTextMock: FieldMetadataInterface = { const completeFieldTextMock = getMockFieldMetadataEntity({
id: 'field-text-id', workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000003',
type: fieldTextMock.type, type: fieldTextMock.type,
name: fieldTextMock.name, name: fieldTextMock.name,
label: 'Field Text', label: 'Field Text',
objectMetadataId: 'object-metadata-id',
isNullable: fieldTextMock.isNullable, isNullable: fieldTextMock.isNullable,
defaultValue: fieldTextMock.defaultValue, defaultValue: fieldTextMock.defaultValue,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; });
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock, 'field-number-id': completeFieldNumberMock as FieldMetadataEntity,
'field-text-id': completeFieldTextMock, 'field-text-id': completeFieldTextMock as FieldMetadataEntity,
}; };
const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = { const mockObjectMetadataWithFieldMaps: ObjectMetadataItemWithFieldMaps = {
@ -59,7 +61,7 @@ describe('parseFilter', () => {
mockObjectMetadataWithFieldMaps, mockObjectMetadataWithFieldMaps,
), ),
).toEqual({ ).toEqual({
and: [{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: 2 } }], and: [{ fieldNumber: { eq: '1' } }, { fieldNumber: { eq: '2' } }],
}); });
}); });
@ -71,8 +73,8 @@ describe('parseFilter', () => {
), ),
).toEqual({ ).toEqual({
and: [ and: [
{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: '1' } },
{ or: [{ fieldNumber: { eq: 2 } }, { fieldNumber: { eq: 3 } }] }, { or: [{ fieldNumber: { eq: '2' } }, { fieldNumber: { eq: '3' } }] },
], ],
}); });
}); });
@ -85,15 +87,17 @@ describe('parseFilter', () => {
), ),
).toEqual({ ).toEqual({
and: [ and: [
{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: '1' } },
{ {
or: [ or: [
{ fieldNumber: { eq: 2 } }, { fieldNumber: { eq: '2' } },
{ fieldNumber: { eq: 3 } }, { fieldNumber: { eq: '3' } },
{ and: [{ fieldNumber: { eq: 6 } }, { fieldNumber: { eq: 7 } }] }, {
and: [{ fieldNumber: { eq: '6' } }, { fieldNumber: { eq: '7' } }],
},
], ],
}, },
{ or: [{ fieldNumber: { eq: 4 } }, { fieldNumber: { eq: 5 } }] }, { or: [{ fieldNumber: { eq: '4' } }, { fieldNumber: { eq: '5' } }] },
], ],
}); });
}); });
@ -113,13 +117,13 @@ describe('parseFilter', () => {
{ not: { fieldText: { startsWith: 'val' } } }, { not: { fieldText: { startsWith: 'val' } } },
{ {
and: [ and: [
{ fieldNumber: { eq: 6 } }, { fieldNumber: { eq: '6' } },
{ fieldText: { ilike: '%val%' } }, { fieldText: { ilike: '%val%' } },
], ],
}, },
], ],
}, },
{ or: [{ fieldNumber: { eq: 4 } }, { fieldText: { is: 'NULL' } }] }, { or: [{ fieldNumber: { eq: '4' } }, { fieldText: { is: 'NULL' } }] },
], ],
}); });
}); });
@ -132,9 +136,9 @@ describe('parseFilter', () => {
), ),
).toEqual({ ).toEqual({
and: [ and: [
{ fieldNumber: { eq: 1 } }, { fieldNumber: { eq: '1' } },
{ {
not: { fieldNumber: { eq: 2 } }, not: { fieldNumber: { eq: '2' } },
}, },
], ],
}); });

View File

@ -1,7 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { import {
fieldCurrencyMock, fieldCurrencyMock,
fieldNumberMock, fieldNumberMock,
@ -9,11 +7,16 @@ import {
objectMetadataMapItemMock, objectMetadataMapItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { FilterInputFactory } from 'src/engine/api/rest/input-factories/filter-input.factory'; 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 { 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 { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('FilterInputFactory', () => { describe('FilterInputFactory', () => {
const completeFieldNumberMock: FieldMetadataInterface = { const workspaceId = '20202020-cc80-4306-ad69-da9e11997292';
const completeFieldNumberMock = getMockFieldMetadataEntity({
workspaceId,
id: 'field-number-id', id: 'field-number-id',
type: fieldNumberMock.type, type: fieldNumberMock.type,
name: fieldNumberMock.name, name: fieldNumberMock.name,
@ -24,9 +27,10 @@ describe('FilterInputFactory', () => {
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; }) as FieldMetadataEntity;
const completeFieldTextMock: FieldMetadataInterface = { const completeFieldTextMock = getMockFieldMetadataEntity({
workspaceId,
id: 'field-text-id', id: 'field-text-id',
type: fieldTextMock.type, type: fieldTextMock.type,
name: fieldTextMock.name, name: fieldTextMock.name,
@ -37,9 +41,10 @@ describe('FilterInputFactory', () => {
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; }) as FieldMetadataEntity;
const completeFieldCurrencyMock: FieldMetadataInterface = { const completeFieldCurrencyMock = getMockFieldMetadataEntity({
workspaceId,
id: 'field-currency-id', id: 'field-currency-id',
type: fieldCurrencyMock.type, type: fieldCurrencyMock.type,
name: fieldCurrencyMock.name, name: fieldCurrencyMock.name,
@ -50,7 +55,7 @@ describe('FilterInputFactory', () => {
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; }) as FieldMetadataEntity;
const fieldsById: FieldMetadataMap = { const fieldsById: FieldMetadataMap = {
'field-number-id': completeFieldNumberMock, 'field-number-id': completeFieldNumberMock,

View File

@ -4,7 +4,9 @@ import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder
import { GraphqlQueryRunnerException } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; import { 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 { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('computeCursorArgFilter', () => { describe('computeCursorArgFilter', () => {
const objectMetadataItemWithFieldMaps = { const objectMetadataItemWithFieldMaps = {
@ -30,39 +32,42 @@ describe('computeCursorArgFilter', () => {
fullName: 'fullname-id', fullName: 'fullname-id',
}, },
fieldsById: { fieldsById: {
'name-id': { 'name-id': getMockFieldMetadataEntity({
type: FieldMetadataType.TEXT, workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
id: 'name-id', id: 'name-id',
type: FieldMetadataType.TEXT,
name: 'name', name: 'name',
label: 'Name', label: 'Name',
objectMetadataId: 'object-id',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
isNullable: true, isNullable: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
'age-id': { 'age-id': getMockFieldMetadataEntity({
type: FieldMetadataType.NUMBER, workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
id: 'age-id', id: 'age-id',
type: FieldMetadataType.NUMBER,
name: 'age', name: 'age',
label: 'Age', label: 'Age',
objectMetadataId: 'object-id',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
isNullable: true, isNullable: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
'fullname-id': { 'fullname-id': getMockFieldMetadataEntity({
type: FieldMetadataType.FULL_NAME, workspaceId: 'workspace-id',
objectMetadataId: 'object-id',
id: 'fullname-id', id: 'fullname-id',
type: FieldMetadataType.FULL_NAME,
name: 'fullName', name: 'fullName',
label: 'Full Name', label: 'Full Name',
objectMetadataId: 'object-id',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
isNullable: true, isNullable: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
}, },
} satisfies ObjectMetadataItemWithFieldMaps; } satisfies ObjectMetadataItemWithFieldMaps;

View File

@ -1,6 +1,10 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
const workspaceId = '20202020-0000-0000-0000-000000000000';
export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMaps[] = export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMaps[] =
[ [
@ -22,12 +26,13 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isSearchable: true, isSearchable: true,
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId,
indexMetadatas: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '20202020-8dec-43d5-b2ff-6eef05095bec',
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.FULL_NAME, type: FieldMetadataType.FULL_NAME,
icon: 'test-field-icon', icon: 'test-field-icon',
name: 'name', name: 'name',
@ -36,15 +41,14 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
lastName: "''", lastName: "''",
firstName: "''", firstName: "''",
}, },
description: 'Contacts name', description: "Contact's name",
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
}, },
fieldIdByName: { fieldIdByName: {
name: 'nameFieldMetadataId', name: 'nameFieldMetadataId',
@ -69,12 +73,13 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isSearchable: true, isSearchable: true,
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId,
indexMetadatas: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c',
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon', icon: 'test-field-icon',
name: 'name', name: 'name',
@ -83,27 +88,30 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
domainNameFieldMetadataId: { domainNameFieldMetadataId: getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '20202020-c03c-45d6-a4b0-04afe1357c5c',
id: 'domainNameFieldMetadataId', id: 'domainNameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.LINKS, type: FieldMetadataType.LINKS,
icon: 'test-field-icon', icon: 'test-field-icon',
name: 'domainName', name: 'domainName',
label: 'Domain Name', label: 'Domain Name',
defaultValue: '', defaultValue: {
primaryLinkLabel: '',
primaryLinkUrl: '',
secondaryLinks: [],
},
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
}, },
fieldIdByName: { fieldIdByName: {
name: 'nameFieldMetadataId', name: 'nameFieldMetadataId',
@ -129,12 +137,13 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isSearchable: true, isSearchable: true,
labelIdentifierFieldMetadataId: 'nameFieldMetadataId', labelIdentifierFieldMetadataId: 'nameFieldMetadataId',
imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId', imageIdentifierFieldMetadataId: 'imageIdentifierFieldMetadataId',
workspaceId: '', workspaceId,
indexMetadatas: [], indexMetadatas: [],
fieldsById: { fieldsById: {
nameFieldMetadataId: { nameFieldMetadataId: getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca',
id: 'nameFieldMetadataId', id: 'nameFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon', icon: 'test-field-icon',
name: 'name', name: 'name',
@ -143,14 +152,14 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
imageIdentifierFieldMetadataId: { imageIdentifierFieldMetadataId: getMockFieldMetadataEntity({
workspaceId,
objectMetadataId: '20202020-3d75-4aab-bacd-ee176c5f63ca',
id: 'imageIdentifierFieldMetadataId', id: 'imageIdentifierFieldMetadataId',
objectMetadataId: '',
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
icon: 'test-field-icon', icon: 'test-field-icon',
name: 'imageIdentifierFieldName', name: 'imageIdentifierFieldName',
@ -159,11 +168,10 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isCustom: false, isCustom: false,
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
workspaceId: '',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}, }) as FieldMetadataEntity,
}, },
fieldIdByName: { fieldIdByName: {
name: 'nameFieldMetadataId', name: 'nameFieldMetadataId',
@ -189,7 +197,7 @@ export const mockObjectMetadataItemsWithFieldMaps: ObjectMetadataItemWithFieldMa
isSearchable: false, isSearchable: false,
labelIdentifierFieldMetadataId: '', labelIdentifierFieldMetadataId: '',
imageIdentifierFieldMetadataId: '', imageIdentifierFieldMetadataId: '',
workspaceId: '', workspaceId,
indexMetadatas: [], indexMetadatas: [],
fieldsById: {}, fieldsById: {},
fieldIdByName: {}, fieldIdByName: {},

View File

@ -19,7 +19,7 @@ describe('computeSchemaComponents', () => {
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
{ {
"ObjectName": { "ObjectName": {
"description": undefined, "description": "Object description",
"example": { "example": {
"fieldCurrency": { "fieldCurrency": {
"amountMicros": 284000000, "amountMicros": 284000000,
@ -48,10 +48,13 @@ describe('computeSchemaComponents', () => {
"primaryPhoneCountryCode": "FR", "primaryPhoneCountryCode": "FR",
"primaryPhoneNumber": "06 10 20 30 40", "primaryPhoneNumber": "06 10 20 30 40",
}, },
"fieldSelect": "OPTION_1", "fieldSelect": [
"OPTION_1",
],
}, },
"properties": { "properties": {
"fieldActor": { "fieldActor": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"source": { "source": {
"enum": [ "enum": [
@ -70,6 +73,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldAddress": { "fieldAddress": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"addressCity": { "addressCity": {
"type": "string", "type": "string",
@ -99,15 +103,18 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldArray": { "fieldArray": {
"description": "Default field metadata entity description",
"items": { "items": {
"type": "string", "type": "string",
}, },
"type": "array", "type": "array",
}, },
"fieldBoolean": { "fieldBoolean": {
"description": "Default field metadata entity description",
"type": "boolean", "type": "boolean",
}, },
"fieldCurrency": { "fieldCurrency": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"amountMicros": { "amountMicros": {
"type": "number", "type": "number",
@ -119,14 +126,17 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldDate": { "fieldDate": {
"description": "Default field metadata entity description",
"format": "date", "format": "date",
"type": "string", "type": "string",
}, },
"fieldDateTime": { "fieldDateTime": {
"description": "Default field metadata entity description",
"format": "date-time", "format": "date-time",
"type": "string", "type": "string",
}, },
"fieldEmails": { "fieldEmails": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"additionalEmails": { "additionalEmails": {
"items": { "items": {
@ -142,6 +152,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldFullName": { "fieldFullName": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"firstName": { "firstName": {
"type": "string", "type": "string",
@ -153,6 +164,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldLinks": { "fieldLinks": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"primaryLinkLabel": { "primaryLinkLabel": {
"type": "string", "type": "string",
@ -180,6 +192,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldMultiSelect": { "fieldMultiSelect": {
"description": "Default field metadata entity description",
"items": { "items": {
"enum": [ "enum": [
"OPTION_1", "OPTION_1",
@ -190,12 +203,15 @@ describe('computeSchemaComponents', () => {
"type": "array", "type": "array",
}, },
"fieldNumber": { "fieldNumber": {
"description": "Default field metadata entity description",
"type": "integer", "type": "integer",
}, },
"fieldNumeric": { "fieldNumeric": {
"description": "Default field metadata entity description",
"type": "number", "type": "number",
}, },
"fieldPhones": { "fieldPhones": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"additionalPhones": { "additionalPhones": {
"items": { "items": {
@ -216,9 +232,11 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldPosition": { "fieldPosition": {
"description": "Default field metadata entity description",
"type": "number", "type": "number",
}, },
"fieldRating": { "fieldRating": {
"description": "Default field metadata entity description",
"enum": [ "enum": [
"RATING_1", "RATING_1",
"RATING_2", "RATING_2",
@ -226,6 +244,7 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldRawJson": { "fieldRawJson": {
"description": "Default field metadata entity description",
"type": "object", "type": "object",
}, },
"fieldRelationId": { "fieldRelationId": {
@ -233,9 +252,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldRichText": { "fieldRichText": {
"description": "Default field metadata entity description",
"type": "string", "type": "string",
}, },
"fieldSelect": { "fieldSelect": {
"description": "Default field metadata entity description",
"enum": [ "enum": [
"OPTION_1", "OPTION_1",
"OPTION_2", "OPTION_2",
@ -243,9 +264,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldText": { "fieldText": {
"description": "Default field metadata entity description",
"type": "string", "type": "string",
}, },
"fieldUuid": { "fieldUuid": {
"description": "Default field metadata entity description",
"format": "uuid", "format": "uuid",
"type": "string", "type": "string",
}, },
@ -256,9 +279,10 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"ObjectNameForResponse": { "ObjectNameForResponse": {
"description": undefined, "description": "Object description",
"properties": { "properties": {
"fieldActor": { "fieldActor": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
@ -284,6 +308,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldAddress": { "fieldAddress": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"addressCity": { "addressCity": {
"type": "string", "type": "string",
@ -313,15 +338,18 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldArray": { "fieldArray": {
"description": "Default field metadata entity description",
"items": { "items": {
"type": "string", "type": "string",
}, },
"type": "array", "type": "array",
}, },
"fieldBoolean": { "fieldBoolean": {
"description": "Default field metadata entity description",
"type": "boolean", "type": "boolean",
}, },
"fieldCurrency": { "fieldCurrency": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"amountMicros": { "amountMicros": {
"type": "number", "type": "number",
@ -333,14 +361,17 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldDate": { "fieldDate": {
"description": "Default field metadata entity description",
"format": "date", "format": "date",
"type": "string", "type": "string",
}, },
"fieldDateTime": { "fieldDateTime": {
"description": "Default field metadata entity description",
"format": "date-time", "format": "date-time",
"type": "string", "type": "string",
}, },
"fieldEmails": { "fieldEmails": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"additionalEmails": { "additionalEmails": {
"items": { "items": {
@ -356,6 +387,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldFullName": { "fieldFullName": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"firstName": { "firstName": {
"type": "string", "type": "string",
@ -367,6 +399,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldLinks": { "fieldLinks": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"primaryLinkLabel": { "primaryLinkLabel": {
"type": "string", "type": "string",
@ -394,6 +427,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldMultiSelect": { "fieldMultiSelect": {
"description": "Default field metadata entity description",
"items": { "items": {
"enum": [ "enum": [
"OPTION_1", "OPTION_1",
@ -404,12 +438,15 @@ describe('computeSchemaComponents', () => {
"type": "array", "type": "array",
}, },
"fieldNumber": { "fieldNumber": {
"description": "Default field metadata entity description",
"type": "integer", "type": "integer",
}, },
"fieldNumeric": { "fieldNumeric": {
"description": "Default field metadata entity description",
"type": "number", "type": "number",
}, },
"fieldPhones": { "fieldPhones": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"additionalPhones": { "additionalPhones": {
"items": { "items": {
@ -430,9 +467,11 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldPosition": { "fieldPosition": {
"description": "Default field metadata entity description",
"type": "number", "type": "number",
}, },
"fieldRating": { "fieldRating": {
"description": "Default field metadata entity description",
"enum": [ "enum": [
"RATING_1", "RATING_1",
"RATING_2", "RATING_2",
@ -440,9 +479,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldRawJson": { "fieldRawJson": {
"description": "Default field metadata entity description",
"type": "object", "type": "object",
}, },
"fieldRelation": { "fieldRelation": {
"description": "Default field metadata entity description",
"oneOf": [ "oneOf": [
{ {
"$ref": "#/components/schemas/RelationTargetObjectForResponse", "$ref": "#/components/schemas/RelationTargetObjectForResponse",
@ -455,9 +496,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldRichText": { "fieldRichText": {
"description": "Default field metadata entity description",
"type": "string", "type": "string",
}, },
"fieldSelect": { "fieldSelect": {
"description": "Default field metadata entity description",
"enum": [ "enum": [
"OPTION_1", "OPTION_1",
"OPTION_2", "OPTION_2",
@ -465,9 +508,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldText": { "fieldText": {
"description": "Default field metadata entity description",
"type": "string", "type": "string",
}, },
"fieldUuid": { "fieldUuid": {
"description": "Default field metadata entity description",
"format": "uuid", "format": "uuid",
"type": "string", "type": "string",
}, },
@ -475,7 +520,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"ObjectNameForUpdate": { "ObjectNameForUpdate": {
"description": undefined, "description": "Object description",
"example": { "example": {
"fieldCurrency": { "fieldCurrency": {
"amountMicros": 253000000, "amountMicros": 253000000,
@ -504,10 +549,13 @@ describe('computeSchemaComponents', () => {
"primaryPhoneCountryCode": "FR", "primaryPhoneCountryCode": "FR",
"primaryPhoneNumber": "06 10 20 30 40", "primaryPhoneNumber": "06 10 20 30 40",
}, },
"fieldSelect": "OPTION_1", "fieldSelect": [
"OPTION_1",
],
}, },
"properties": { "properties": {
"fieldActor": { "fieldActor": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"source": { "source": {
"enum": [ "enum": [
@ -526,6 +574,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldAddress": { "fieldAddress": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"addressCity": { "addressCity": {
"type": "string", "type": "string",
@ -555,15 +604,18 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldArray": { "fieldArray": {
"description": "Default field metadata entity description",
"items": { "items": {
"type": "string", "type": "string",
}, },
"type": "array", "type": "array",
}, },
"fieldBoolean": { "fieldBoolean": {
"description": "Default field metadata entity description",
"type": "boolean", "type": "boolean",
}, },
"fieldCurrency": { "fieldCurrency": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"amountMicros": { "amountMicros": {
"type": "number", "type": "number",
@ -575,14 +627,17 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldDate": { "fieldDate": {
"description": "Default field metadata entity description",
"format": "date", "format": "date",
"type": "string", "type": "string",
}, },
"fieldDateTime": { "fieldDateTime": {
"description": "Default field metadata entity description",
"format": "date-time", "format": "date-time",
"type": "string", "type": "string",
}, },
"fieldEmails": { "fieldEmails": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"additionalEmails": { "additionalEmails": {
"items": { "items": {
@ -598,6 +653,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldFullName": { "fieldFullName": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"firstName": { "firstName": {
"type": "string", "type": "string",
@ -609,6 +665,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldLinks": { "fieldLinks": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"primaryLinkLabel": { "primaryLinkLabel": {
"type": "string", "type": "string",
@ -636,6 +693,7 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldMultiSelect": { "fieldMultiSelect": {
"description": "Default field metadata entity description",
"items": { "items": {
"enum": [ "enum": [
"OPTION_1", "OPTION_1",
@ -646,12 +704,15 @@ describe('computeSchemaComponents', () => {
"type": "array", "type": "array",
}, },
"fieldNumber": { "fieldNumber": {
"description": "Default field metadata entity description",
"type": "integer", "type": "integer",
}, },
"fieldNumeric": { "fieldNumeric": {
"description": "Default field metadata entity description",
"type": "number", "type": "number",
}, },
"fieldPhones": { "fieldPhones": {
"description": "Default field metadata entity description",
"properties": { "properties": {
"additionalPhones": { "additionalPhones": {
"items": { "items": {
@ -672,9 +733,11 @@ describe('computeSchemaComponents', () => {
"type": "object", "type": "object",
}, },
"fieldPosition": { "fieldPosition": {
"description": "Default field metadata entity description",
"type": "number", "type": "number",
}, },
"fieldRating": { "fieldRating": {
"description": "Default field metadata entity description",
"enum": [ "enum": [
"RATING_1", "RATING_1",
"RATING_2", "RATING_2",
@ -682,6 +745,7 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldRawJson": { "fieldRawJson": {
"description": "Default field metadata entity description",
"type": "object", "type": "object",
}, },
"fieldRelationId": { "fieldRelationId": {
@ -689,9 +753,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldRichText": { "fieldRichText": {
"description": "Default field metadata entity description",
"type": "string", "type": "string",
}, },
"fieldSelect": { "fieldSelect": {
"description": "Default field metadata entity description",
"enum": [ "enum": [
"OPTION_1", "OPTION_1",
"OPTION_2", "OPTION_2",
@ -699,9 +765,11 @@ describe('computeSchemaComponents', () => {
"type": "string", "type": "string",
}, },
"fieldText": { "fieldText": {
"description": "Default field metadata entity description",
"type": "string", "type": "string",
}, },
"fieldUuid": { "fieldUuid": {
"description": "Default field metadata entity description",
"format": "uuid", "format": "uuid",
"type": "string", "type": "string",
}, },

View File

@ -81,11 +81,19 @@ export const generateRandomFieldValue = ({
} }
case FieldMetadataType.SELECT: { case FieldMetadataType.SELECT: {
return isDefined(field.options[0].value) ? field.options[0].value : []; if (!isDefined(field.options) || !isDefined(field.options[0].value)) {
return [];
}
return [field.options[0].value];
} }
case FieldMetadataType.MULTI_SELECT: { case FieldMetadataType.MULTI_SELECT: {
return isDefined(field.options[0].value) ? [field.options[0].value] : []; if (!isDefined(field.options) || !isDefined(field.options[0].value)) {
return [];
}
return [field.options[0].value];
} }
case FieldMetadataType.RELATION: case FieldMetadataType.RELATION:

View File

@ -14,6 +14,7 @@ import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dto
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service'; import { FieldMetadataMorphRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-morph-relation.service';
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service'; import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util';
import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util'; import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util';
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto'; import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto'; import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
@ -211,40 +212,41 @@ export class DataloaderService {
return []; return [];
} }
const fields = Object.values(objectMetadata.fieldsById).map( const fields = Object.values(
// TODO: fix this as we should merge FieldMetadataEntity and FieldMetadataInterface objectMetadata.fieldsById,
(fieldMetadata) => { ).map<FieldMetadataDTO>((fieldMetadata) => {
const overridesFieldToCompute = [ const overridesFieldToCompute = [
'icon', 'icon',
'label', 'label',
'description', 'description',
] as const satisfies (keyof FieldMetadataInterface)[]; ] as const satisfies (keyof FieldMetadataInterface)[];
const overrides = overridesFieldToCompute.reduce< const overrides = overridesFieldToCompute.reduce<
Partial< Partial<Record<(typeof overridesFieldToCompute)[number], string>>
Record<(typeof overridesFieldToCompute)[number], string> >(
> (acc, field) => ({
>( ...acc,
(acc, field) => ({ [field]: resolveFieldMetadataStandardOverride(
...acc, {
[field]: resolveFieldMetadataStandardOverride( label: fieldMetadata.label,
fieldMetadata, description: fieldMetadata.description ?? undefined,
field, icon: fieldMetadata.icon ?? undefined,
dataLoaderParams[0].locale, isCustom: fieldMetadata.isCustom,
), standardOverrides:
}), fieldMetadata.standardOverrides ?? undefined,
{}, },
); field,
dataLoaderParams[0].locale,
),
}),
{},
);
return { return fromFieldMetadataEntityToFieldMetadataDto({
...fieldMetadata, ...fieldMetadata,
createdAt: new Date(fieldMetadata.createdAt), ...overrides,
updatedAt: new Date(fieldMetadata.updatedAt), });
workspaceId: workspaceId, });
...overrides,
};
},
);
return filterMorphRelationDuplicateFieldsDTO(fields); return filterMorphRelationDuplicateFieldsDTO(fields);
}); });

View File

@ -56,6 +56,7 @@ registerEnumType(FieldMetadataType, {
@Relation('object', () => ObjectMetadataDTO, { @Relation('object', () => ObjectMetadataDTO, {
nullable: true, nullable: true,
}) })
// TODO refactor nullable fields to be typed as nullable and not optional
export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> { export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
@IsUUID() @IsUUID()
@IsNotEmpty() @IsNotEmpty()
@ -132,7 +133,7 @@ export class FieldMetadataDTO<T extends FieldMetadataType = FieldMetadataType> {
// @Validate(IsFieldMetadataOptions) // @Validate(IsFieldMetadataOptions)
@IsOptional() @IsOptional()
@Field(() => GraphQLJSON, { nullable: true }) @Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions<T>; options?: FieldMetadataOptions<T> | null;
@IsOptional() @IsOptional()
@Field(() => GraphQLJSON, { nullable: true }) @Field(() => GraphQLJSON, { nullable: true })

View File

@ -1,4 +1,4 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType, IsExactly } from 'twenty-shared/types';
import { import {
Column, Column,
CreateDateColumn, CreateDateColumn,
@ -22,8 +22,16 @@ import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-meta
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity'; import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.entity';
type IsRelationType<Ttype, T extends FieldMetadataType = FieldMetadataType> =
IsExactly<T, FieldMetadataType> extends true
? null | Ttype
: T extends FieldMetadataType.RELATION
? Ttype
: T extends FieldMetadataType.MORPH_RELATION
? Ttype
: never;
@Entity('fieldMetadata') @Entity('fieldMetadata')
// max length of index is 63 characters
@Index( @Index(
'IDX_FIELD_METADATA_NAME_OBJMID_WORKSPACE_ID_EXCEPT_MORPH_UNIQUE', 'IDX_FIELD_METADATA_NAME_OBJMID_WORKSPACE_ID_EXCEPT_MORPH_UNIQUE',
['name', 'objectMetadataId', 'workspaceId'], ['name', 'objectMetadataId', 'workspaceId'],
@ -42,6 +50,7 @@ import { FieldPermissionEntity } from 'src/engine/metadata-modules/object-permis
'objectMetadataId', 'objectMetadataId',
'workspaceId', 'workspaceId',
]) ])
// TODO add some documentation about this entity
export class FieldMetadataEntity< export class FieldMetadataEntity<
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = FieldMetadataType,
> { > {
@ -56,6 +65,7 @@ export class FieldMetadataEntity<
@ManyToOne(() => ObjectMetadataEntity, (object) => object.fields, { @ManyToOne(() => ObjectMetadataEntity, (object) => object.fields, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
nullable: false,
}) })
@JoinColumn({ name: 'objectMetadataId' }) @JoinColumn({ name: 'objectMetadataId' })
@Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID', ['objectMetadataId']) @Index('IDX_FIELD_METADATA_OBJECT_METADATA_ID', ['objectMetadataId'])
@ -74,22 +84,22 @@ export class FieldMetadataEntity<
label: string; label: string;
@Column({ nullable: true, type: 'jsonb' }) @Column({ nullable: true, type: 'jsonb' })
defaultValue: FieldMetadataDefaultValue<T>; defaultValue: FieldMetadataDefaultValue<T> | null;
@Column({ nullable: true, type: 'text' }) @Column({ nullable: true, type: 'text' })
description: string; description: string | null;
@Column({ nullable: true }) @Column({ nullable: true, type: 'varchar' })
icon: string; icon: string | null;
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', nullable: true })
standardOverrides?: FieldStandardOverridesDTO; standardOverrides?: FieldStandardOverridesDTO | null;
@Column('jsonb', { nullable: true }) @Column('jsonb', { nullable: true })
options: FieldMetadataOptions<T>; options: FieldMetadataOptions<T> | null;
@Column('jsonb', { nullable: true }) @Column('jsonb', { nullable: true })
settings?: FieldMetadataSettings<T>; settings?: FieldMetadataSettings<T> | null;
@Column({ default: false }) @Column({ default: false })
isCustom: boolean; isCustom: boolean;
@ -100,11 +110,13 @@ export class FieldMetadataEntity<
@Column({ default: false }) @Column({ default: false })
isSystem: boolean; isSystem: boolean;
@Column({ nullable: true, default: true }) // Is this really nullable ?
isNullable: boolean; @Column({ nullable: true, default: true, type: 'boolean' })
isNullable: boolean | null;
@Column({ nullable: true, default: false }) // Is this really nullable ?
isUnique: boolean; @Column({ nullable: true, default: false, type: 'boolean' })
isUnique: boolean | null;
@Column({ nullable: false, type: 'uuid' }) @Column({ nullable: false, type: 'uuid' })
@Index('IDX_FIELD_METADATA_WORKSPACE_ID', ['workspaceId']) @Index('IDX_FIELD_METADATA_WORKSPACE_ID', ['workspaceId'])
@ -114,25 +126,31 @@ export class FieldMetadataEntity<
isLabelSyncedWithName: boolean; isLabelSyncedWithName: boolean;
@Column({ nullable: true, type: 'uuid' }) @Column({ nullable: true, type: 'uuid' })
relationTargetFieldMetadataId: string; relationTargetFieldMetadataId: IsRelationType<string, T>;
@OneToOne( @OneToOne(
() => FieldMetadataEntity, () => FieldMetadataEntity,
(fieldMetadata: FieldMetadataEntity) => (fieldMetadata: FieldMetadataEntity) =>
fieldMetadata.relationTargetFieldMetadataId, fieldMetadata.relationTargetFieldMetadataId,
{ nullable: true },
) )
@JoinColumn({ name: 'relationTargetFieldMetadataId' }) @JoinColumn({ name: 'relationTargetFieldMetadataId' })
relationTargetFieldMetadata: Relation<FieldMetadataEntity>; relationTargetFieldMetadata: IsRelationType<Relation<FieldMetadataEntity>, T>;
@Column({ nullable: true, type: 'uuid' }) @Column({ nullable: true, type: 'uuid' })
relationTargetObjectMetadataId: string; relationTargetObjectMetadataId: IsRelationType<string, T>;
@ManyToOne( @ManyToOne(
() => ObjectMetadataEntity, () => ObjectMetadataEntity,
(objectMetadata: ObjectMetadataEntity) => (objectMetadata: ObjectMetadataEntity) =>
objectMetadata.targetRelationFields, objectMetadata.targetRelationFields,
{ onDelete: 'CASCADE' }, { onDelete: 'CASCADE', nullable: true },
) )
@JoinColumn({ name: 'relationTargetObjectMetadataId' }) @JoinColumn({ name: 'relationTargetObjectMetadataId' })
relationTargetObjectMetadata: Relation<ObjectMetadataEntity>; relationTargetObjectMetadata: IsRelationType<
Relation<ObjectMetadataEntity>,
T
>;
@OneToMany( @OneToMany(
() => IndexFieldMetadataEntity, () => IndexFieldMetadataEntity,

View File

@ -39,6 +39,7 @@ import {
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util'; import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
import { fromFieldMetadataEntityToFieldMetadataDto } from 'src/engine/metadata-modules/field-metadata/utils/from-field-metadata-entity-to-fieldMetadata-dto.util';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util'; import { isMorphRelationFieldMetadataType } from 'src/engine/utils/is-morph-relation-field-metadata-type.util';
@ -152,7 +153,7 @@ export class FieldMetadataResolver {
workspaceId: workspace.id, workspaceId: workspace.id,
}); });
if (!fieldMetadata.settings) { if (!isDefined(fieldMetadata.settings)) {
throw new FieldMetadataException( throw new FieldMetadataException(
'Relation settings are required', 'Relation settings are required',
FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED, FieldMetadataExceptionCode.FIELD_METADATA_RELATION_MALFORMED,
@ -163,8 +164,10 @@ export class FieldMetadataResolver {
type: fieldMetadata.settings.relationType, type: fieldMetadata.settings.relationType,
sourceObjectMetadata, sourceObjectMetadata,
targetObjectMetadata, targetObjectMetadata,
sourceFieldMetadata, sourceFieldMetadata:
targetFieldMetadata, fromFieldMetadataEntityToFieldMetadataDto(sourceFieldMetadata),
targetFieldMetadata:
fromFieldMetadataEntityToFieldMetadataDto(targetFieldMetadata),
}; };
} catch (error) { } catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error); fieldMetadataGraphqlApiExceptionHandler(error);
@ -198,12 +201,16 @@ export class FieldMetadataResolver {
); );
} }
return morphRelations.map((morphRelation) => ({ return morphRelations.map<RelationDTO>((morphRelation) => ({
type: settings.relationType, type: settings.relationType,
sourceObjectMetadata: morphRelation.sourceObjectMetadata, sourceObjectMetadata: morphRelation.sourceObjectMetadata,
targetObjectMetadata: morphRelation.targetObjectMetadata, targetObjectMetadata: morphRelation.targetObjectMetadata,
sourceFieldMetadata: morphRelation.sourceFieldMetadata, sourceFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto(
targetFieldMetadata: morphRelation.targetFieldMetadata, morphRelation.sourceFieldMetadata,
),
targetFieldMetadata: fromFieldMetadataEntityToFieldMetadataDto(
morphRelation.targetFieldMetadata,
),
})); }));
} catch (error) { } catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error); fieldMetadataGraphqlApiExceptionHandler(error);

View File

@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import { UpdateOneInputType } from '@ptc-org/nestjs-query-graphql'; import { UpdateOneInputType } from '@ptc-org/nestjs-query-graphql';
import { FieldMetadataType } from 'twenty-shared/types';
import { import {
ForbiddenError, ForbiddenError,
@ -11,6 +12,7 @@ import { UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dto
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook'; import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
jest.mock('@lingui/core', () => ({ jest.mock('@lingui/core', () => ({
i18n: { i18n: {
@ -96,14 +98,23 @@ describe('BeforeUpdateOneField', () => {
}, },
}; };
const mockField: Partial<FieldMetadataEntity> = { const mockField = getMockFieldMetadataEntity({
workspaceId: mockWorkspaceId,
objectMetadataId: '20202020-0000-0000-0000-000000000002',
id: mockFieldId, id: mockFieldId,
type: FieldMetadataType.TEXT,
name: 'oldName',
label: 'Old Name',
isNullable: true,
isCustom: true, isCustom: true,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
jest jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace') .spyOn(fieldMetadataService, 'findOneWithinWorkspace')
.mockResolvedValue(mockField as FieldMetadataEntity); .mockResolvedValue(mockField);
const result = await hook.run( const result = await hook.run(
instance as UpdateOneInputType<UpdateFieldInput>, instance as UpdateOneInputType<UpdateFieldInput>,
@ -124,14 +135,23 @@ describe('BeforeUpdateOneField', () => {
}, },
}; };
const mockField: Partial<FieldMetadataEntity> = { const mockField = getMockFieldMetadataEntity({
workspaceId: mockWorkspaceId,
objectMetadataId: '20202020-0000-0000-0000-000000000002',
id: mockFieldId, id: mockFieldId,
type: FieldMetadataType.TEXT,
name: 'oldName',
label: 'Old Name',
isNullable: true,
isCustom: false, isCustom: false,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
jest jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace') .spyOn(fieldMetadataService, 'findOneWithinWorkspace')
.mockResolvedValue(mockField as FieldMetadataEntity); .mockResolvedValue(mockField);
await expect( await expect(
hook.run(instance as UpdateOneInputType<UpdateFieldInput>, { hook.run(instance as UpdateOneInputType<UpdateFieldInput>, {
@ -149,15 +169,24 @@ describe('BeforeUpdateOneField', () => {
}, },
}; };
const mockField: Partial<FieldMetadataEntity> = { const mockField = getMockFieldMetadataEntity({
workspaceId: mockWorkspaceId,
objectMetadataId: '20202020-0000-0000-0000-000000000002',
id: mockFieldId, id: mockFieldId,
type: FieldMetadataType.TEXT,
name: 'oldName',
label: 'Old Name',
isNullable: true,
isCustom: false, isCustom: false,
isActive: true, isActive: true,
}; isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
jest jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace') .spyOn(fieldMetadataService, 'findOneWithinWorkspace')
.mockResolvedValue(mockField as FieldMetadataEntity); .mockResolvedValue(mockField);
const result = await hook.run( const result = await hook.run(
instance as UpdateOneInputType<UpdateFieldInput>, instance as UpdateOneInputType<UpdateFieldInput>,
@ -186,18 +215,26 @@ describe('BeforeUpdateOneField', () => {
}, },
}; };
const mockField: Partial<FieldMetadataEntity> = { const mockField = getMockFieldMetadataEntity({
workspaceId: mockWorkspaceId,
objectMetadataId: '20202020-0000-0000-0000-000000000002',
id: mockFieldId, id: mockFieldId,
type: FieldMetadataType.TEXT,
name: 'oldName',
label: 'Old Name',
isNullable: true,
isCustom: false, isCustom: false,
isLabelSyncedWithName: false, isLabelSyncedWithName: false,
standardOverrides: { standardOverrides: {
label: 'Custom Label', label: 'Custom Label',
}, },
}; createdAt: new Date(),
updatedAt: new Date(),
}) as FieldMetadataEntity;
jest jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace') .spyOn(fieldMetadataService, 'findOneWithinWorkspace')
.mockResolvedValue(mockField as FieldMetadataEntity); .mockResolvedValue(mockField);
const result = await hook.run( const result = await hook.run(
instance as UpdateOneInputType<UpdateFieldInput>, instance as UpdateOneInputType<UpdateFieldInput>,
@ -228,16 +265,23 @@ describe('BeforeUpdateOneField', () => {
}, },
}; };
const mockField: Partial<FieldMetadataEntity> = { const mockField = getMockFieldMetadataEntity({
workspaceId: mockWorkspaceId,
objectMetadataId: '20202020-0000-0000-0000-000000000002',
id: mockFieldId, id: mockFieldId,
type: FieldMetadataType.TEXT,
name: 'oldName',
label: 'Default Label',
isNullable: true,
isCustom: false, isCustom: false,
isLabelSyncedWithName: false, isLabelSyncedWithName: false,
label: 'Default Label', createdAt: new Date(),
}; updatedAt: new Date(),
}) as FieldMetadataEntity;
jest jest
.spyOn(fieldMetadataService, 'findOneWithinWorkspace') .spyOn(fieldMetadataService, 'findOneWithinWorkspace')
.mockResolvedValue(mockField as FieldMetadataEntity); .mockResolvedValue(mockField);
const result = await hook.run( const result = await hook.run(
instance as UpdateOneInputType<UpdateFieldInput>, instance as UpdateOneInputType<UpdateFieldInput>,

View File

@ -207,7 +207,7 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
update: StandardFieldUpdate; update: StandardFieldUpdate;
overrideKey: 'label' | 'description' | 'icon'; overrideKey: 'label' | 'description' | 'icon';
newValue: string; newValue: string;
originalValue: string; originalValue: string | null;
locale?: keyof typeof APP_LOCALES | undefined; locale?: keyof typeof APP_LOCALES | undefined;
}): boolean { }): boolean {
// Handle localized overrides // Handle localized overrides
@ -238,7 +238,7 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
update: StandardFieldUpdate, update: StandardFieldUpdate,
overrideKey: 'label' | 'description' | 'icon', overrideKey: 'label' | 'description' | 'icon',
newValue: string, newValue: string,
originalValue: string, originalValue: string | null,
locale: keyof typeof APP_LOCALES, locale: keyof typeof APP_LOCALES,
): boolean { ): boolean {
const messageId = generateMessageId(originalValue ?? ''); const messageId = generateMessageId(originalValue ?? '');
@ -268,7 +268,7 @@ export class BeforeUpdateOneField<T extends UpdateFieldInput>
update: StandardFieldUpdate, update: StandardFieldUpdate,
overrideKey: 'label' | 'description' | 'icon', overrideKey: 'label' | 'description' | 'icon',
newValue: string, newValue: string,
originalValue: string, originalValue: string | null,
): boolean { ): boolean {
if (newValue !== originalValue) { if (newValue !== originalValue) {
return false; return false;

View File

@ -1,39 +1,7 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto'; export type FieldMetadataInterface<
export interface FieldMetadataInterface<
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = FieldMetadataType,
> { > = FieldMetadataEntity<T>;
id: string;
type: T;
name: string;
label: string;
defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;
objectMetadataId: string;
workspaceId?: string;
description?: string;
icon?: string;
isNullable: boolean;
isUnique?: boolean;
relationTargetFieldMetadataId?: string;
relationTargetFieldMetadata?: FieldMetadataInterface;
relationTargetObjectMetadataId?: string;
relationTargetObjectMetadata?: ObjectMetadataInterface;
relation?: RelationDTO;
isCustom?: boolean;
isSystem?: boolean;
isActive?: boolean;
generatedType?: 'STORED' | 'VIRTUAL';
asExpression?: string;
isLabelSyncedWithName: boolean;
createdAt: Date;
updatedAt: Date;
}

View File

@ -57,8 +57,8 @@ export class FieldMetadataRelatedRecordsService {
); );
const { created, updated, deleted } = this.getOptionsDifferences( const { created, updated, deleted } = this.getOptionsDifferences(
oldFieldMetadata.options, oldFieldMetadata.options ?? [],
newFieldMetadata.options, newFieldMetadata.options ?? [],
); );
const viewGroupRepository = const viewGroupRepository =
@ -175,9 +175,15 @@ export class FieldMetadataRelatedRecordsService {
} }
const viewFilterOptions = viewFilterValue const viewFilterOptions = viewFilterValue
.map((value) => .map((value) => {
oldFieldMetadata.options.find((option) => option.value === value), if (!isDefined(oldFieldMetadata.options)) {
) return undefined;
}
return oldFieldMetadata.options.find(
(option) => option.value === value,
);
})
.filter(isDefined); .filter(isDefined);
const afterDeleteViewFilterOptions = viewFilterOptions.filter( const afterDeleteViewFilterOptions = viewFilterOptions.filter(
@ -247,8 +253,12 @@ export class FieldMetadataRelatedRecordsService {
} }
public getOptionsDifferences( public getOptionsDifferences(
oldOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], rawOldOptions:
newOptions: (FieldMetadataDefaultOption | FieldMetadataComplexOption)[], | (FieldMetadataDefaultOption | FieldMetadataComplexOption)[]
| null,
rawNewOptions:
| (FieldMetadataDefaultOption | FieldMetadataComplexOption)[]
| null,
compareLabel = false, compareLabel = false,
): GetOptionsDifferences { ): GetOptionsDifferences {
const differences: Differences< const differences: Differences<
@ -259,6 +269,9 @@ export class FieldMetadataRelatedRecordsService {
deleted: [], deleted: [],
}; };
const oldOptions = rawOldOptions ?? [];
const newOptions = rawNewOptions ?? [];
const oldOptionsMap = new Map(oldOptions.map((opt) => [opt.id, opt])); const oldOptionsMap = new Map(oldOptions.map((opt) => [opt.id, opt]));
for (const newOption of newOptions) { for (const newOption of newOptions) {

View File

@ -91,6 +91,7 @@ export class FieldMetadataRelationService {
label: relationCreationPayload.targetFieldLabel, label: relationCreationPayload.targetFieldLabel,
icon: relationCreationPayload.targetFieldIcon, icon: relationCreationPayload.targetFieldIcon,
workspaceId: fieldMetadataInput.workspaceId, workspaceId: fieldMetadataInput.workspaceId,
defaultValue: fieldMetadataInput.defaultValue,
}); });
const targetFieldMetadataToCreateWithRelation = const targetFieldMetadataToCreateWithRelation =

View File

@ -39,6 +39,7 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-
import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util'; import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util';
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util'; import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
@ -190,8 +191,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const fieldMetadataForUpdate = { const fieldMetadataForUpdate = {
...updatableFieldInput, ...updatableFieldInput,
defaultValue: defaultValueForUpdate,
...optionsForUpdate, ...optionsForUpdate,
defaultValue: defaultValueForUpdate,
}; };
await this.fieldMetadataValidationService.validateFieldMetadata({ await this.fieldMetadataValidationService.validateFieldMetadata({
@ -379,7 +380,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
name: isManyToOneRelation name: isManyToOneRelation
? computeObjectTargetTable(fieldMetadata.object) ? computeObjectTargetTable(fieldMetadata.object)
: computeObjectTargetTable( : computeObjectTargetTable(
fieldMetadata.relationTargetObjectMetadata, fieldMetadata.relationTargetObjectMetadata as ObjectMetadataEntity,
), ),
action: WorkspaceMigrationTableActionType.ALTER, action: WorkspaceMigrationTableActionType.ALTER,
columns: [ columns: [

View File

@ -0,0 +1,90 @@
import { Expect, HasAllProperties } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { Relation as TypeOrmRelation } from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
type DefinedRelationRecord = {
relationTargetFieldMetadataId: string;
relationTargetFieldMetadata: TypeOrmRelation<FieldMetadataEntity>;
relationTargetObjectMetadataId: string;
relationTargetObjectMetadata: TypeOrmRelation<ObjectMetadataEntity>;
};
type NotDefinedRelationRecord = {
relationTargetFieldMetadataId: never;
relationTargetFieldMetadata: never;
relationTargetObjectMetadataId: never;
relationTargetObjectMetadata: never;
};
type UUIDFieldMetadata = FieldMetadataEntity<FieldMetadataType.UUID>;
type TextFieldMetadata = FieldMetadataEntity<FieldMetadataType.TEXT>;
type NumberFieldMetadata = FieldMetadataEntity<FieldMetadataType.NUMBER>;
type BooleanFieldMetadata = FieldMetadataEntity<FieldMetadataType.BOOLEAN>;
type DateFieldMetadata = FieldMetadataEntity<FieldMetadataType.DATE>;
type DateTimeFieldMetadata = FieldMetadataEntity<FieldMetadataType.DATE_TIME>;
type CurrencyFieldMetadata = FieldMetadataEntity<FieldMetadataType.CURRENCY>;
type FullNameFieldMetadata = FieldMetadataEntity<FieldMetadataType.FULL_NAME>;
type RatingFieldMetadata = FieldMetadataEntity<FieldMetadataType.RATING>;
type SelectFieldMetadata = FieldMetadataEntity<FieldMetadataType.SELECT>;
type MultiSelectFieldMetadata =
FieldMetadataEntity<FieldMetadataType.MULTI_SELECT>;
type PositionFieldMetadata = FieldMetadataEntity<FieldMetadataType.POSITION>;
type RawJsonFieldMetadata = FieldMetadataEntity<FieldMetadataType.RAW_JSON>;
type RichTextFieldMetadata = FieldMetadataEntity<FieldMetadataType.RICH_TEXT>;
type ActorFieldMetadata = FieldMetadataEntity<FieldMetadataType.ACTOR>;
type ArrayFieldMetadata = FieldMetadataEntity<FieldMetadataType.ARRAY>;
type PhonesFieldMetadata = FieldMetadataEntity<FieldMetadataType.PHONES>;
type EmailsFieldMetadata = FieldMetadataEntity<FieldMetadataType.EMAILS>;
type LinksFieldMetadata = FieldMetadataEntity<FieldMetadataType.LINKS>;
type RelationFieldMetadata = FieldMetadataEntity<FieldMetadataType.RELATION>;
type MorphRelationFieldMetadata =
FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type Assertions = [
Expect<HasAllProperties<UUIDFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<TextFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<NumberFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<BooleanFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<DateFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<DateTimeFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<CurrencyFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<FullNameFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<RatingFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<SelectFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<MultiSelectFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<RichTextFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<ArrayFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<PhonesFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<EmailsFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<LinksFieldMetadata, NotDefinedRelationRecord>>,
Expect<HasAllProperties<RelationFieldMetadata, DefinedRelationRecord>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, DefinedRelationRecord>>,
];

View File

@ -7,7 +7,7 @@ export const assertDoesNotNullifyDefaultValueForNonNullableField = ({
isNullable, isNullable,
defaultValueFromUpdate, defaultValueFromUpdate,
}: { }: {
isNullable: boolean; isNullable: boolean | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultValueFromUpdate?: any; defaultValueFromUpdate?: any;
}) => { }) => {

View File

@ -0,0 +1,31 @@
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const fromFieldMetadataEntityToFieldMetadataDto = (
fieldMetadataEntity: FieldMetadataEntity,
): FieldMetadataDTO => {
const {
createdAt,
updatedAt,
description,
icon,
standardOverrides,
isNullable,
isUnique,
settings,
...rest
} = fieldMetadataEntity;
return {
...rest,
// Should we ? seems to be typed a dateString from classValidator, should be typed as string in TypeScript ?
createdAt: new Date(createdAt),
updatedAt: new Date(updatedAt),
description: description ?? undefined,
icon: icon ?? undefined,
standardOverrides: standardOverrides ?? undefined,
isNullable: isNullable ?? false,
isUnique: isUnique ?? false,
settings: settings ?? undefined,
};
};

View File

@ -9,7 +9,8 @@ export type SelectOrMultiSelectFieldMetadataEntity = FieldMetadataEntity<
>; >;
export const isSelectOrMultiSelectFieldMetadata = ( export const isSelectOrMultiSelectFieldMetadata = (
fieldMetadata: FieldMetadataInterface, fieldMetadata: FieldMetadataInterface,
): fieldMetadata is SelectOrMultiSelectFieldMetadataEntity => { ): fieldMetadata is FieldMetadataInterface &
SelectOrMultiSelectFieldMetadataEntity => {
return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes( return [FieldMetadataType.SELECT, FieldMetadataType.MULTI_SELECT].includes(
fieldMetadata.type, fieldMetadata.type,
); );

View File

@ -11,6 +11,7 @@ import {
QueryRunner, QueryRunner,
Repository, Repository,
} from 'typeorm'; } from 'typeorm';
import { FieldMetadataType } from 'twenty-shared/types';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -46,6 +47,7 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util'; import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { ObjectMetadataEntity } from './object-metadata.entity'; import { ObjectMetadataEntity } from './object-metadata.entity';
@ -471,9 +473,15 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
); );
const fieldMetadataIds = objectMetadata.fields.map((field) => field.id); const fieldMetadataIds = objectMetadata.fields.map((field) => field.id);
const relationMetadataIds = objectMetadata.fields const relationMetadataIds = objectMetadata.fields.flatMap((field) => {
.map((field) => field.relationTargetFieldMetadata?.id) if (
.filter(isDefined); isFieldMetadataEntityOfType(field, FieldMetadataType.MORPH_RELATION)
) {
return field.relationTargetFieldMetadata.id;
}
return [];
});
await fieldMetadataRepository.delete({ await fieldMetadataRepository.delete({
id: In(fieldMetadataIds.concat(relationMetadataIds)), id: In(fieldMetadataIds.concat(relationMetadataIds)),

View File

@ -4,12 +4,11 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { import {
fieldTextMock, fieldTextMock,
objectMetadataItemMock, objectMetadataItemMock,
} from 'src/engine/api/__mocks__/object-metadata-item.mock'; } 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 { 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 { 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'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
@ -21,6 +20,7 @@ import {
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('FieldPermissionService', () => { describe('FieldPermissionService', () => {
let service: FieldPermissionService; let service: FieldPermissionService;
@ -31,9 +31,9 @@ describe('FieldPermissionService', () => {
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>; let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
let workspaceCacheStorageService: jest.Mocked<WorkspaceCacheStorageService>; let workspaceCacheStorageService: jest.Mocked<WorkspaceCacheStorageService>;
const testWorkspaceId = 'test-workspace-id'; const testWorkspaceId = '20202020-0000-0000-0000-000000000000';
const testRoleId = 'test-role-id'; const testRoleId = '20202020-0000-0000-0000-000000000001';
const testObjectMetadataId = 'test-object-metadata-id'; const testObjectMetadataId = '20202020-0000-0000-0000-000000000002';
const testFieldMetadataId = fieldTextMock.id; const testFieldMetadataId = fieldTextMock.id;
const mockRole: RoleEntity = { const mockRole: RoleEntity = {
@ -121,11 +121,13 @@ describe('FieldPermissionService', () => {
[testObjectMetadataId]: { [testObjectMetadataId]: {
...objectMetadataItemMock, ...objectMetadataItemMock,
fieldsById: { fieldsById: {
[fieldTextMock.id]: { [fieldTextMock.id]: getMockFieldMetadataEntity({
...fieldTextMock, ...fieldTextMock,
label: 'Test Field', label: 'Test Field',
objectMetadataId: testObjectMetadataId, objectMetadataId: testObjectMetadataId,
} as FieldMetadataInterface, workspaceId: testWorkspaceId,
id: '20202020-0000-0000-0000-000000000003',
}) as FieldMetadataEntity,
}, },
fieldIdByJoinColumnName: {}, fieldIdByJoinColumnName: {},
fieldIdByName: {}, fieldIdByName: {},

View File

@ -5,8 +5,6 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { QueryRunner, Repository } from 'typeorm'; import { QueryRunner, Repository } from 'typeorm';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
@ -14,7 +12,10 @@ import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/inde
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import {
TsVectorColumnActionFactory,
TsVectorFieldMetadata,
} from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { import {
WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnActionType,
@ -23,6 +24,7 @@ import {
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { import {
FieldTypeAndNameMetadata, FieldTypeAndNameMetadata,
@ -66,6 +68,17 @@ export class SearchVectorService {
isNullable: true, isNullable: true,
}); });
if (
!isFieldMetadataEntityOfType(
searchVectorFieldMetadata,
FieldMetadataType.TS_VECTOR,
)
) {
throw new Error(
'Should never occur, created searchVectorFieldMetadata is not a TS_VECTOR field',
);
}
const searchableFieldForCustomObject = const searchableFieldForCustomObject =
createdObjectMetadata.labelIdentifierFieldMetadataId createdObjectMetadata.labelIdentifierFieldMetadataId
? createdObjectMetadata.fields.find( ? createdObjectMetadata.fields.find(
@ -94,7 +107,6 @@ export class SearchVectorService {
action: WorkspaceMigrationTableActionType.ALTER, action: WorkspaceMigrationTableActionType.ALTER,
columns: this.tsVectorColumnActionFactory.handleCreateAction({ columns: this.tsVectorColumnActionFactory.handleCreateAction({
...searchVectorFieldMetadata, ...searchVectorFieldMetadata,
defaultValue: undefined,
generatedType: 'STORED', generatedType: 'STORED',
asExpression: getTsVectorColumnExpressionFromFields([ asExpression: getTsVectorColumnExpressionFromFields([
{ {
@ -102,8 +114,7 @@ export class SearchVectorService {
name: searchableFieldForCustomObject.name, name: searchableFieldForCustomObject.name,
}, },
]), ]),
options: undefined, }),
} as FieldMetadataInterface<FieldMetadataType.TS_VECTOR>),
}, },
], ],
queryRunner, queryRunner,
@ -159,8 +170,8 @@ export class SearchVectorService {
fieldMetadataNameAndTypeForSearch, fieldMetadataNameAndTypeForSearch,
), ),
generatedType: 'STORED', // Not stored on fieldMetadata generatedType: 'STORED', // Not stored on fieldMetadata
options: undefined, options: null,
}, } as TsVectorFieldMetadata,
), ),
}, },
], ],

View File

@ -72,7 +72,7 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
columnType: fieldMetadataTypeToColumnType(property.type), columnType: fieldMetadataTypeToColumnType(property.type),
enum: enumOptions, enum: enumOptions,
isNullable: fieldMetadata.isNullable || !property.isRequired, isNullable: fieldMetadata.isNullable || !property.isRequired,
isUnique: fieldMetadata.isUnique, isUnique: fieldMetadata.isUnique ?? undefined,
defaultValue: serializedDefaultValue, defaultValue: serializedDefaultValue,
isArray: isArray:
property.type === FieldMetadataType.MULTI_SELECT || property.isArray, property.type === FieldMetadataType.MULTI_SELECT || property.isArray,

View File

@ -13,14 +13,19 @@ import {
WorkspaceMigrationColumnCreate, WorkspaceMigrationColumnCreate,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
export type TsVectorFieldMetadataType = FieldMetadataType.TS_VECTOR; export type TsVectorFieldMetadata =
FieldMetadataInterface<FieldMetadataType.TS_VECTOR> & {
generatedType?: 'STORED' | 'VIRTUAL';
asExpression?: string;
};
export type TsVectorFieldMetadataType = FieldMetadataType.TS_VECTOR;
@Injectable() @Injectable()
export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory<TsVectorFieldMetadataType> { export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory<TsVectorFieldMetadataType> {
protected readonly logger = new Logger(TsVectorColumnActionFactory.name); protected readonly logger = new Logger(TsVectorColumnActionFactory.name);
handleCreateAction( handleCreateAction(
fieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>, fieldMetadata: TsVectorFieldMetadata,
): WorkspaceMigrationColumnCreate[] { ): WorkspaceMigrationColumnCreate[] {
return [ return [
{ {
@ -37,8 +42,8 @@ export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory<TsV
} }
handleAlterAction( handleAlterAction(
currentFieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>, currentFieldMetadata: TsVectorFieldMetadata,
alteredFieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>, alteredFieldMetadata: TsVectorFieldMetadata,
): WorkspaceMigrationColumnAlter[] { ): WorkspaceMigrationColumnAlter[] {
return [ return [
{ {

View File

@ -11,7 +11,10 @@ import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/worksp
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
import { MorphRelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory'; import { MorphRelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/morph-relation-column-action.factory';
import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory'; import { RelationColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/relation-column-action.factory';
import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import {
TsVectorColumnActionFactory,
TsVectorFieldMetadata,
} from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory';
import { import {
WorkspaceMigrationColumnAction, WorkspaceMigrationColumnAction,
WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnActionType,
@ -115,9 +118,9 @@ export class WorkspaceMigrationFactory {
]); ]);
} }
createColumnActions( createColumnActions<T extends FieldMetadataType = FieldMetadataType>(
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
fieldMetadata: FieldMetadataInterface, fieldMetadata: FieldMetadataInterface<T>,
): WorkspaceMigrationColumnAction[]; ): WorkspaceMigrationColumnAction[];
createColumnActions( createColumnActions(
@ -126,6 +129,12 @@ export class WorkspaceMigrationFactory {
alteredFieldMetadata: FieldMetadataInterface, alteredFieldMetadata: FieldMetadataInterface,
): WorkspaceMigrationColumnAction[]; ): WorkspaceMigrationColumnAction[];
createColumnActions(
action: WorkspaceMigrationColumnActionType.ALTER,
currentFieldMetadata: FieldMetadataInterface,
alteredFieldMetadata: TsVectorFieldMetadata,
): WorkspaceMigrationColumnAction[];
createColumnActions( createColumnActions(
action: action:
| WorkspaceMigrationColumnActionType.CREATE | WorkspaceMigrationColumnActionType.CREATE

View File

@ -7,6 +7,7 @@ import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/works
import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils'; import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { WorkspaceEntityManager } from './workspace-entity-manager'; import { WorkspaceEntityManager } from './workspace-entity-manager';
@ -88,7 +89,7 @@ describe('WorkspaceEntityManager', () => {
}, },
fieldIdByName: { fieldName: 'field-id' }, fieldIdByName: { fieldName: 'field-id' },
fieldIdByJoinColumnName: {}, fieldIdByJoinColumnName: {},
}, } as unknown as ObjectMetadataItemWithFieldMaps,
}, },
idByNameSingular: { idByNameSingular: {
'test-entity': 'test-entity-id', 'test-entity': 'test-entity-id',

View File

@ -67,7 +67,7 @@ export class EntitySchemaColumnFactory {
entitySchemaColumnMap[joinColumnName] = { entitySchemaColumnMap[joinColumnName] = {
name: joinColumnName, name: joinColumnName,
type: 'uuid', type: 'uuid',
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable ?? false,
}; };
continue; continue;
@ -92,7 +92,7 @@ export class EntitySchemaColumnFactory {
type: columnType as ColumnType, type: columnType as ColumnType,
// TODO: We should double check that // TODO: We should double check that
primary: key === 'id', primary: key === 'id',
nullable: fieldMetadata.isNullable, nullable: fieldMetadata.isNullable ?? false,
createDate: key === 'createdAt', createDate: key === 'createdAt',
updateDate: key === 'updatedAt', updateDate: key === 'updatedAt',
deleteDate: key === 'deletedAt', deleteDate: key === 'deletedAt',

View File

@ -56,7 +56,7 @@ export class WorkspaceDeleteQueryBuilder<
) as this; ) as this;
} }
override async execute(): Promise<DeleteResult> { override async execute(): Promise<DeleteResult & { generatedMaps: T[] }> {
validateQueryIsPermittedOrThrow( validateQueryIsPermittedOrThrow(
this.expressionMap, this.expressionMap,
this.objectRecordsPermissions, this.objectRecordsPermissions,

View File

@ -130,7 +130,7 @@ export const convertObjectMetadataToSchemaProperties = ({
type: 'array', type: 'array',
items: { items: {
type: 'string', type: 'string',
enum: field.options.map( enum: (field.options ?? []).map(
(option: { value: string }) => option.value, (option: { value: string }) => option.value,
), ),
}, },
@ -139,7 +139,9 @@ export const convertObjectMetadataToSchemaProperties = ({
case FieldMetadataType.SELECT: case FieldMetadataType.SELECT:
itemProperty = { itemProperty = {
type: 'string', type: 'string',
enum: field.options.map((option: { value: string }) => option.value), enum: (field.options ?? []).map(
(option: { value: string }) => option.value,
),
}; };
break; break;
case FieldMetadataType.ARRAY: case FieldMetadataType.ARRAY:
@ -153,7 +155,9 @@ export const convertObjectMetadataToSchemaProperties = ({
case FieldMetadataType.RATING: case FieldMetadataType.RATING:
itemProperty = { itemProperty = {
type: 'string', type: 'string',
enum: field.options.map((option: { value: string }) => option.value), enum: (field.options ?? []).map(
(option: { value: string }) => option.value,
),
}; };
break; break;
case FieldMetadataType.LINKS: case FieldMetadataType.LINKS:

View File

@ -172,7 +172,16 @@ export class FieldMetadataHealthService {
serializeDefaultValue(`'${option.value}'`), serializeDefaultValue(`'${option.value}'`),
); );
if (!enumValues.includes(columnDefaultValue)) { if (!isDefined(enumValues)) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID,
fieldMetadata,
columnStructure,
message: `Column options of ${fieldMetadata.name} are not defined`,
});
}
if (isDefined(enumValues) && !enumValues.includes(columnDefaultValue)) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
fieldMetadata, fieldMetadata,
@ -247,8 +256,17 @@ export class FieldMetadataHealthService {
}); });
} }
if (!isDefined(fieldMetadata.options)) {
issues.push({
type: WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID,
fieldMetadata,
message: `Column options of ${fieldMetadata.name} are not defined`,
});
}
if ( if (
isEnumFieldMetadataType(fieldMetadata.type) && isEnumFieldMetadataType(fieldMetadata.type) &&
isDefined(fieldMetadata.options) &&
!validateOptionsForType(fieldMetadata.type, fieldMetadata.options) !validateOptionsForType(fieldMetadata.type, fieldMetadata.options)
) { ) {
issues.push({ issues.push({
@ -300,7 +318,7 @@ export class FieldMetadataHealthService {
}); });
} else { } else {
metadataDefaultValue.forEach((value) => { metadataDefaultValue.forEach((value) => {
if (!enumValues.includes(value)) { if (isDefined(enumValues) && !enumValues.includes(value)) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
fieldMetadata, fieldMetadata,
@ -309,7 +327,10 @@ export class FieldMetadataHealthService {
} }
}); });
} }
} else if (enumValues.includes(metadataDefaultValue as string)) { } else if (
isDefined(enumValues) &&
!enumValues.includes(metadataDefaultValue as string)
) {
issues.push({ issues.push({
type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID, type: WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID,
fieldMetadata, fieldMetadata,

View File

@ -2,17 +2,30 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.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 { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { createIndexMigration } from 'src/engine/workspace-manager/workspace-migration-builder/factories/utils/workspace-migration-index.factory.utils'; import { createIndexMigration } from 'src/engine/workspace-manager/workspace-migration-builder/factories/utils/workspace-migration-index.factory.utils';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('WorkspaceMigrationIndexFactory', () => { describe('WorkspaceMigrationIndexFactory', () => {
it('should create index migrations for simple fields', async () => { it('should create index migrations for simple fields', async () => {
const simpleField = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: 'f1',
type: FieldMetadataType.TEXT,
name: 'simpleField',
label: 'Simple Field',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
});
const objectMetadata = { const objectMetadata = {
id: 'obj1', id: '20202020-0000-0000-0000-000000000002',
workspaceId: 'ws1', workspaceId: '20202020-0000-0000-0000-000000000000',
nameSingular: 'Test', nameSingular: 'Test',
fields: [{ id: 'f1', name: 'simpleField', type: FieldMetadataType.TEXT }], fields: [simpleField],
isCustom: false, isCustom: false,
}; };
const indexMetadata = { const indexMetadata = {
@ -39,25 +52,28 @@ describe('WorkspaceMigrationIndexFactory', () => {
}); });
it('should create index migrations for relation fields', async () => { it('should create index migrations for relation fields', async () => {
const fieldMetadata: Pick< const relationField = getMockFieldMetadataEntity({
FieldMetadataEntity<FieldMetadataType.RELATION>, workspaceId: '20202020-0000-0000-0000-000000000000',
'id' | 'name' | 'type' | 'settings' | 'isCustom' objectMetadataId: '20202020-0000-0000-0000-000000000001',
> = {
id: 'f2', id: 'f2',
name: 'author',
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'author',
label: 'Author',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(),
updatedAt: new Date(),
settings: { settings: {
relationType: RelationType.MANY_TO_ONE, relationType: RelationType.MANY_TO_ONE,
joinColumnName: 'authorId', joinColumnName: 'authorId',
}, },
isCustom: false, });
};
const objectMetadata = { const objectMetadata = {
id: 'obj2', id: '20202020-0000-0000-0000-000000000003',
workspaceId: 'ws1', workspaceId: '20202020-0000-0000-0000-000000000000',
nameSingular: 'Attachment', nameSingular: 'Attachment',
fields: [fieldMetadata], fields: [relationField],
isCustom: false, isCustom: false,
}; };
const indexMetadata = { const indexMetadata = {

View File

@ -11,13 +11,14 @@ type FlatFieldMetadataOverrides<
Partial<FlatFieldMetadata<T>>; Partial<FlatFieldMetadata<T>>;
export const getFlatFieldMetadataMock = < export const getFlatFieldMetadataMock = <
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = FieldMetadataType.TEXT,
>( >(
overrides: FlatFieldMetadataOverrides<T>, overrides: FlatFieldMetadataOverrides<T>,
): FlatFieldMetadata<T> => { ): FlatFieldMetadata<T> => {
const createdAt = faker.date.anytime(); const createdAt = faker.date.anytime();
return { return {
type: FieldMetadataType.TEXT as T,
createdAt, createdAt,
description: 'default flat field metadata description', description: 'default flat field metadata description',
icon: 'icon', icon: 'icon',
@ -28,15 +29,18 @@ export const getFlatFieldMetadataMock = <
label: 'flat field metadata label', label: 'flat field metadata label',
isNullable: true, isNullable: true,
isUnique: false, isUnique: false,
relationTargetFieldMetadataId: undefined,
relationTargetObjectMetadataId: undefined,
type: FieldMetadataType.TEXT as T,
isLabelSyncedWithName: false, isLabelSyncedWithName: false,
isSystem: false, isSystem: false,
standardId: undefined, standardId: null,
standardOverrides: undefined, standardOverrides: undefined,
updatedAt: createdAt, updatedAt: createdAt,
workspaceId: faker.string.uuid(), workspaceId: faker.string.uuid(),
defaultValue: null,
options: null,
relationTargetFieldMetadata: undefined as never,
relationTargetFieldMetadataId: undefined as never,
relationTargetObjectMetadata: undefined as never,
relationTargetObjectMetadataId: undefined as never,
...overrides, ...overrides,
}; };
}; };

View File

@ -11,8 +11,6 @@ type FieldMetadataEntityRelationProperties =
>; >;
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> = export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
Partial< Omit<FieldMetadataEntity<T>, FieldMetadataEntityRelationProperties> & {
Omit<FieldMetadataEntity<T>, FieldMetadataEntityRelationProperties>
> & {
uniqueIdentifier: string; uniqueIdentifier: string;
}; };

View File

@ -104,20 +104,19 @@ const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [
context: { context: {
input: () => { input: () => {
const objectMetadataId = faker.string.uuid(); const objectMetadataId = faker.string.uuid();
const updatedFieldMetadata = const updatedFieldMetadata = getFlatFieldMetadataMock({
getFlatFieldMetadataMock<FieldMetadataType.RELATION>({ uniqueIdentifier: 'field-metadata-unique-identifier-1',
uniqueIdentifier: 'field-metadata-unique-identifier-1', objectMetadataId,
objectMetadataId, type: FieldMetadataType.RELATION,
type: FieldMetadataType.RELATION, settings: {
settings: { relationType: RelationType.MANY_TO_ONE,
relationType: RelationType.MANY_TO_ONE, isForeignKey: true,
isForeignKey: true, joinColumnName: 'column-name',
joinColumnName: 'column-name', onDelete: undefined,
onDelete: undefined, },
}, relationTargetFieldMetadataId: faker.string.uuid(),
relationTargetFieldMetadataId: faker.string.uuid(), relationTargetObjectMetadataId: faker.string.uuid(),
relationTargetObjectMetadataId: faker.string.uuid(), });
});
const flatObjectMetadata = getFlatObjectMetadataMock({ const flatObjectMetadata = getFlatObjectMetadataMock({
uniqueIdentifier: 'object-metadata-unique-identifier-1', uniqueIdentifier: 'object-metadata-unique-identifier-1',
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
@ -130,7 +129,7 @@ const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [
{ {
...flatObjectMetadata, ...flatObjectMetadata,
flatFieldMetadatas: [ flatFieldMetadatas: [
{ getFlatFieldMetadataMock({
...updatedFieldMetadata, ...updatedFieldMetadata,
settings: { settings: {
relationType: RelationType.ONE_TO_MANY, relationType: RelationType.ONE_TO_MANY,
@ -140,7 +139,7 @@ const relationTestCases: WorkspaceMigrationBuilderTestCase[] = [
}, },
relationTargetFieldMetadataId: faker.string.uuid(), relationTargetFieldMetadataId: faker.string.uuid(),
relationTargetObjectMetadataId: faker.string.uuid(), relationTargetObjectMetadataId: faker.string.uuid(),
}, }),
], ],
}, },
], ],

View File

@ -195,6 +195,8 @@ export class WorkspaceFieldRelationComparator {
throw new Error(`Field ${fieldId} not found in standardObjectMetadata`); throw new Error(`Field ${fieldId} not found in standardObjectMetadata`);
} }
const relationFieldMetadata = propertiesMap[fieldId];
if (relationTypeChange) { if (relationTypeChange) {
result.push({ result.push({
action: ComparatorAction.DELETE, action: ComparatorAction.DELETE,
@ -204,9 +206,11 @@ export class WorkspaceFieldRelationComparator {
result.push({ result.push({
action: ComparatorAction.CREATE, action: ComparatorAction.CREATE,
object: { object: {
...propertiesMap[fieldId], ...relationFieldMetadata,
id: originalFieldMetadata.id, id: originalFieldMetadata.id,
standardId: standardFieldMetadata.standardId ?? undefined, standardId: standardFieldMetadata.standardId ?? undefined,
description: relationFieldMetadata.description ?? undefined,
icon: relationFieldMetadata.icon ?? undefined,
}, },
}); });
} else if (allOldPropertiesAreNull) { } else if (allOldPropertiesAreNull) {
@ -216,6 +220,8 @@ export class WorkspaceFieldRelationComparator {
...propertiesMap[fieldId], ...propertiesMap[fieldId],
id: originalFieldMetadata.id, id: originalFieldMetadata.id,
standardId: standardFieldMetadata.standardId ?? undefined, standardId: standardFieldMetadata.standardId ?? undefined,
description: relationFieldMetadata.description ?? undefined,
icon: relationFieldMetadata.icon ?? undefined,
}, },
}); });
} else if (allNewPropertiesAreNull) { } else if (allNewPropertiesAreNull) {
@ -230,6 +236,8 @@ export class WorkspaceFieldRelationComparator {
...propertiesMap[fieldId], ...propertiesMap[fieldId],
id: originalFieldMetadata.id, id: originalFieldMetadata.id,
standardId: standardFieldMetadata.standardId ?? undefined, standardId: standardFieldMetadata.standardId ?? undefined,
description: relationFieldMetadata.description ?? undefined,
icon: relationFieldMetadata.icon ?? undefined,
}, },
}); });
} }

View File

@ -143,8 +143,8 @@ export class StandardFieldFactory {
icon: workspaceFieldMetadataArgs.icon, icon: workspaceFieldMetadataArgs.icon,
label: workspaceFieldMetadataArgs.label, label: workspaceFieldMetadataArgs.label,
description: workspaceFieldMetadataArgs.description, description: workspaceFieldMetadataArgs.description,
defaultValue: workspaceFieldMetadataArgs.defaultValue, defaultValue: workspaceFieldMetadataArgs.defaultValue ?? null,
options: workspaceFieldMetadataArgs.options, options: workspaceFieldMetadataArgs.options ?? null,
settings: workspaceFieldMetadataArgs.settings, settings: workspaceFieldMetadataArgs.settings,
workspaceId: context.workspaceId, workspaceId: context.workspaceId,
isNullable: workspaceFieldMetadataArgs.isNullable, isNullable: workspaceFieldMetadataArgs.isNullable,
@ -155,6 +155,10 @@ export class StandardFieldFactory {
asExpression: workspaceFieldMetadataArgs.asExpression, asExpression: workspaceFieldMetadataArgs.asExpression,
generatedType: workspaceFieldMetadataArgs.generatedType, generatedType: workspaceFieldMetadataArgs.generatedType,
isLabelSyncedWithName: workspaceFieldMetadataArgs.isLabelSyncedWithName, isLabelSyncedWithName: workspaceFieldMetadataArgs.isLabelSyncedWithName,
relationTargetFieldMetadata: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadata: null,
relationTargetObjectMetadataId: null,
}, },
]; ];
} }
@ -195,6 +199,12 @@ export class StandardFieldFactory {
isActive: workspaceRelationMetadataArgs.isActive ?? true, isActive: workspaceRelationMetadataArgs.isActive ?? true,
isLabelSyncedWithName: isLabelSyncedWithName:
workspaceRelationMetadataArgs.isLabelSyncedWithName, workspaceRelationMetadataArgs.isLabelSyncedWithName,
defaultValue: null,
options: null,
relationTargetFieldMetadata: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadata: null,
relationTargetObjectMetadataId: null,
}); });
return fieldMetadataCollection; return fieldMetadataCollection;

View File

@ -5,6 +5,7 @@ import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-o
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
// Should get deprecated in favor of the FlatFieldMetadata
export type PartialFieldMetadata< export type PartialFieldMetadata<
T extends FieldMetadataType = FieldMetadataType, T extends FieldMetadataType = FieldMetadataType,
> = Omit< > = Omit<
@ -15,6 +16,15 @@ export type PartialFieldMetadata<
| 'objectMetadataId' | 'objectMetadataId'
| 'createdAt' | 'createdAt'
| 'updatedAt' | 'updatedAt'
| 'standardId'
| 'icon'
| 'isSystem'
| 'workspaceId'
| 'isActive'
| 'asExpression'
| 'indexFieldMetadatas'
| 'fieldPermissions'
| 'object'
> & { > & {
standardId: string; standardId: string;
label: string | ((objectMetadata: ObjectMetadataEntity) => string); label: string | ((objectMetadata: ObjectMetadataEntity) => string);
@ -24,8 +34,8 @@ export type PartialFieldMetadata<
workspaceId: string; workspaceId: string;
objectMetadataId?: string; objectMetadataId?: string;
isActive?: boolean; isActive?: boolean;
asExpression?: string; asExpression?: string; // not accurate
generatedType?: 'STORED' | 'VIRTUAL'; generatedType?: 'STORED' | 'VIRTUAL'; // not accurate
}; };
export type PartialComputedFieldMetadata = { export type PartialComputedFieldMetadata = {

View File

@ -48,6 +48,12 @@ export const computeStandardFields = (
defaultValue: null, defaultValue: null,
isNullable: true, isNullable: true,
isLabelSyncedWithName: true, isLabelSyncedWithName: true,
isUnique: null,
options: null,
relationTargetFieldMetadata: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadata: null,
relationTargetObjectMetadataId: null,
}); });
} }
} else { } else {

View File

@ -13,17 +13,27 @@ export class TimelineActivityRepository {
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {} ) {}
async upsertOne( async upsertOne({
name: string, name,
properties: Partial<ObjectRecord>, objectName,
objectName: string, properties,
recordId: string, recordId,
workspaceId: string, workspaceId,
workspaceMemberId?: string, linkedObjectMetadataId,
linkedRecordCachedName?: string, linkedRecordCachedName,
linkedRecordId?: string, linkedRecordId,
linkedObjectMetadataId?: string, workspaceMemberId,
) { }: {
name: string;
properties: Partial<ObjectRecord>;
objectName: string;
recordId: string;
workspaceId: string;
workspaceMemberId?: string;
linkedRecordCachedName?: string;
linkedRecordId?: string;
linkedObjectMetadataId: string | null;
}) {
const recentTimelineActivity = await this.findRecentTimelineActivity( const recentTimelineActivity = await this.findRecentTimelineActivity(
name, name,
objectName, objectName,
@ -58,17 +68,17 @@ export class TimelineActivityRepository {
); );
} }
return this.insertTimelineActivity( return this.insertTimelineActivity({
name, name,
properties, properties,
objectName, objectName,
recordId, recordId,
workspaceMemberId, workspaceMemberId,
linkedRecordCachedName ?? '', linkedRecordCachedName: linkedRecordCachedName ?? '',
linkedRecordId, linkedRecordId,
linkedObjectMetadataId, linkedObjectMetadataId,
workspaceId, workspaceId,
); });
} }
private async findRecentTimelineActivity( private async findRecentTimelineActivity(
@ -131,17 +141,27 @@ export class TimelineActivityRepository {
}); });
} }
private async insertTimelineActivity( private async insertTimelineActivity({
name: string, linkedObjectMetadataId,
properties: Partial<ObjectRecord>, linkedRecordCachedName,
objectName: string, linkedRecordId,
recordId: string, name,
workspaceMemberId: string | undefined, objectName,
linkedRecordCachedName: string, properties,
linkedRecordId: string | undefined, recordId,
linkedObjectMetadataId: string | undefined, workspaceId,
workspaceId: string, workspaceMemberId,
) { }: {
name: string;
properties: Partial<ObjectRecord>;
objectName: string;
recordId: string;
workspaceMemberId: string | undefined;
linkedRecordCachedName: string;
linkedRecordId: string | undefined;
linkedObjectMetadataId: string | null;
workspaceId: string;
}) {
const timelineActivityTypeORMRepository = const timelineActivityTypeORMRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace( await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId, workspaceId,

View File

@ -14,7 +14,7 @@ type TimelineActivity = Omit<ObjectRecordNonDestructiveEvent, 'properties'> & {
objectName?: string; objectName?: string;
linkedRecordCachedName?: string; linkedRecordCachedName?: string;
linkedRecordId?: string; linkedRecordId?: string;
linkedObjectMetadataId?: string; linkedObjectMetadataId?: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
properties: Record<string, any>; // more relaxed conditions than for internal events properties: Record<string, any>; // more relaxed conditions than for internal events
}; };
@ -50,17 +50,28 @@ export class TimelineActivityService {
if (!timelineActivities || timelineActivities.length === 0) return; if (!timelineActivities || timelineActivities.length === 0) return;
for (const timelineActivity of timelineActivities) { for (const timelineActivity of timelineActivities) {
await this.timelineActivityRepository.upsertOne( const {
timelineActivity.name, name,
timelineActivity.properties, properties,
timelineActivity.objectName ?? event.objectMetadata.nameSingular, recordId,
timelineActivity.recordId, linkedObjectMetadataId,
linkedRecordCachedName,
linkedRecordId,
objectName,
workspaceMemberId,
} = timelineActivity;
await this.timelineActivityRepository.upsertOne({
linkedObjectMetadataId: linkedObjectMetadataId ?? null,
name,
objectName: objectName ?? event.objectMetadata.nameSingular,
properties,
recordId,
workspaceId, workspaceId,
timelineActivity.workspaceMemberId, linkedRecordCachedName,
timelineActivity.linkedRecordCachedName, linkedRecordId,
timelineActivity.linkedRecordId, workspaceMemberId,
timelineActivity.linkedObjectMetadataId, });
);
} }
} }

View File

@ -56,44 +56,80 @@ describe('generateFakeFormResponse', () => {
objectMetadataMaps: mockObjectMetadataMaps, objectMetadataMaps: mockObjectMetadataMaps,
}); });
expect(result).toEqual({ expect(result).toMatchInlineSnapshot(`
name: { {
isLeaf: true, "age": {
label: 'Name', "icon": undefined,
type: FieldMetadataType.TEXT, "isLeaf": true,
value: 'My text', "label": "Age",
icon: undefined, "type": "NUMBER",
}, "value": 20,
age: { },
isLeaf: true, "company": {
label: 'Age', "isLeaf": false,
type: FieldMetadataType.NUMBER, "label": "Company",
value: 20, "value": {
icon: undefined, "_outputSchemaType": "RECORD",
}, "fields": {
company: { "domainName": {
isLeaf: false, "icon": "test-field-icon",
label: 'Company', "isLeaf": false,
value: { "label": "Domain Name",
_outputSchemaType: 'RECORD', "type": "LINKS",
fields: {}, "value": {
object: { "primaryLinkLabel": {
isLeaf: true, "isLeaf": true,
label: 'Company', "label": "Primary Link Label",
fieldIdName: 'id', "type": "TEXT",
icon: 'test-company-icon', "value": "My text",
nameSingular: 'company', },
value: 'A company', "primaryLinkUrl": {
"isLeaf": true,
"label": "Primary Link Url",
"type": "TEXT",
"value": "My text",
},
"secondaryLinks": {
"isLeaf": true,
"label": "Secondary Links",
"type": "RAW_JSON",
"value": null,
},
}, },
}, },
"name": {
"icon": "test-field-icon",
"isLeaf": true,
"label": "Name",
"type": "TEXT",
"value": "My text",
},
}, },
date: { "object": {
isLeaf: true, "fieldIdName": "id",
label: 'Date', "icon": "test-company-icon",
type: FieldMetadataType.DATE, "isLeaf": true,
value: 'mm/dd/yyyy', "label": "Company",
icon: undefined, "nameSingular": "company",
"value": "A company",
}, },
}); },
},
"date": {
"icon": undefined,
"isLeaf": true,
"label": "Date",
"type": "DATE",
"value": "mm/dd/yyyy",
},
"name": {
"icon": undefined,
"isLeaf": true,
"label": "Name",
"type": "TEXT",
"value": "My text",
},
}
`);
}); });
}); });

View File

@ -1,71 +1,100 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value'; import { shouldGenerateFieldFakeValue } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/should-generate-field-fake-value';
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
describe('shouldGenerateFieldFakeValue', () => { describe('shouldGenerateFieldFakeValue', () => {
it('should return true for active non-system fields', () => { it('should return true for active non-system fields', () => {
const field = { const field = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000002',
isSystem: false, isSystem: false,
isActive: true, isActive: true,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'testField', name: 'testField',
label: 'Test Field',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} as FieldMetadataInterface; });
expect(shouldGenerateFieldFakeValue(field)).toBe(true); expect(shouldGenerateFieldFakeValue(field)).toBe(true);
}); });
it('should return true for system id field', () => { it('should return true for system id field', () => {
const field = { const field = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000003',
isSystem: true, isSystem: true,
isActive: true, isActive: true,
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
name: 'id', name: 'id',
label: 'ID',
isNullable: false,
isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} as FieldMetadataInterface; });
expect(shouldGenerateFieldFakeValue(field)).toBe(true); expect(shouldGenerateFieldFakeValue(field)).toBe(true);
}); });
it('should return false for inactive fields', () => { it('should return false for inactive fields', () => {
const field = { const field = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000004',
isSystem: false, isSystem: false,
isActive: false, isActive: false,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'testField', name: 'testField',
label: 'Test Field',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} as FieldMetadataInterface; });
expect(shouldGenerateFieldFakeValue(field)).toBe(false); expect(shouldGenerateFieldFakeValue(field)).toBe(false);
}); });
it('should return false for system fields (except id)', () => { it('should return false for system fields (except id)', () => {
const field = { const field = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000005',
isSystem: true, isSystem: true,
isActive: true, isActive: true,
type: FieldMetadataType.TEXT, type: FieldMetadataType.TEXT,
name: 'testField', name: 'testField',
label: 'Test Field',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} as FieldMetadataInterface; });
expect(shouldGenerateFieldFakeValue(field)).toBe(false); expect(shouldGenerateFieldFakeValue(field)).toBe(false);
}); });
it('should return false for relation fields', () => { it('should return false for relation fields', () => {
const field = { const field = getMockFieldMetadataEntity({
workspaceId: '20202020-0000-0000-0000-000000000000',
objectMetadataId: '20202020-0000-0000-0000-000000000001',
id: '20202020-0000-0000-0000-000000000006',
isSystem: false, isSystem: false,
isActive: true, isActive: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
name: 'testField', name: 'testField',
label: 'Test Field',
isNullable: true,
isLabelSyncedWithName: true,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
} as FieldMetadataInterface; });
expect(shouldGenerateFieldFakeValue(field)).toBe(false); expect(shouldGenerateFieldFakeValue(field)).toBe(false);
}); });

View File

@ -28,7 +28,7 @@ export const generateObjectRecordFields = ({
acc[field.name] = generateFakeField({ acc[field.name] = generateFakeField({
type: field.type, type: field.type,
label: field.label, label: field.label,
icon: field.icon, icon: field.icon ?? undefined,
}); });
return acc; return acc;
@ -49,7 +49,7 @@ export const generateObjectRecordFields = ({
acc[field.name] = { acc[field.name] = {
isLeaf: false, isLeaf: false,
icon: field.icon, icon: field.icon ?? undefined,
label: field.label, label: field.label,
value: generateFakeObjectRecord({ value: generateFakeObjectRecord({
objectMetadataInfo: { objectMetadataInfo: {

View File

@ -1,18 +1,19 @@
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
const isManyToOneRelationField = (field: FieldMetadataInterface) => const isManyToOneRelationField = (field: FieldMetadataEntity) =>
(field as FieldMetadataEntity<FieldMetadataType.RELATION>).settings (field as FieldMetadataEntity<FieldMetadataType.RELATION>).settings
?.relationType === 'MANY_TO_ONE'; ?.relationType === 'MANY_TO_ONE';
export const shouldGenerateFieldFakeValue = (field: FieldMetadataInterface) => { // TODO refactor
export const shouldGenerateFieldFakeValue = <T extends FieldMetadataType>(
field: FieldMetadataEntity<T>,
) => {
return ( return (
field.isActive && field.isActive &&
(!field.isSystem || field.name === 'id' || field.name === 'userEmail') && (!field.isSystem || field.name === 'id' || field.name === 'userEmail') &&
(field.type !== FieldMetadataType.RELATION || (field.type !== FieldMetadataType.RELATION ||
isManyToOneRelationField(field)) isManyToOneRelationField(field as unknown as FieldMetadataEntity))
); );
}; };

View File

@ -0,0 +1,47 @@
import { faker } from '@faker-js/faker';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
type GetMockFieldMetadataEntityOverride<
T extends FieldMetadataType = FieldMetadataType,
> = Partial<FieldMetadataEntity<T>> &
Required<Pick<FieldMetadataEntity<T>, 'workspaceId' | 'objectMetadataId'>>;
export const getMockFieldMetadataEntity = <
T extends FieldMetadataType = FieldMetadataType.TEXT,
>(
overrides: GetMockFieldMetadataEntityOverride<T>,
): FieldMetadataEntity<T> => {
return {
type: FieldMetadataType.TEXT as T,
fieldPermissions: [],
icon: null,
indexFieldMetadatas: {} as IndexFieldMetadataEntity,
isCustom: true,
isLabelSyncedWithName: false,
isNullable: null,
isSystem: false,
isUnique: null,
object: {} as ObjectMetadataEntity,
relationTargetFieldMetadata: null as never,
relationTargetFieldMetadataId: null as never,
relationTargetObjectMetadata: null as never,
relationTargetObjectMetadataId: null as never,
standardId: null,
standardOverrides: null,
id: faker.string.uuid(),
name: 'defaultFieldMetadataName',
label: 'Default field metadata entity label',
description: 'Default field metadata entity description',
defaultValue: null,
options: null,
settings: null,
createdAt: new Date(),
updatedAt: new Date(),
isActive: true,
...overrides,
};
};

View File

@ -71,7 +71,7 @@ describe.each(fieldMetadataEnumTypes)(
expect(data.createOneField.type).toEqual(testedFieldMetadataType); expect(data.createOneField.type).toEqual(testedFieldMetadataType);
const createdOptions = data.createOneField.options; const createdOptions = data.createOneField.options;
const optionsToCompare = expectedOptions ?? input.options; const optionsToCompare = expectedOptions ?? input.options ?? [];
expect(errors).toBeUndefined(); expect(errors).toBeUndefined();
expect(createdOptions?.length).toBe(optionsToCompare.length); expect(createdOptions?.length).toBe(optionsToCompare.length);

View File

@ -168,12 +168,19 @@ describe.each(fieldMetadataEnumTypes)(
expect(data.updateOneField).toBeDefined(); expect(data.updateOneField).toBeDefined();
const updatedOptions: const updatedOptions:
| FieldMetadataComplexOption[] | FieldMetadataComplexOption[]
| FieldMetadataDefaultOption[] = data.updateOneField.options; | FieldMetadataDefaultOption[]
| null = data.updateOneField.options;
expect(updatedOptions).toBeDefined();
if (!isDefined(updatedOptions))
throw new Error(
'Should never occur, type invariant post test assertion',
);
expect(errors).toBeUndefined(); expect(errors).toBeUndefined();
updatedOptions.forEach((option) => expect(option.id).toBeDefined()); updatedOptions.forEach((option) => expect(option.id).toBeDefined());
const optionsToCompare = expectedOptions ?? input.options; const optionsToCompare = expectedOptions ?? input.options ?? [];
expect(updatedOptions.length).toBe(optionsToCompare.length); expect(updatedOptions.length).toBe(optionsToCompare.length);
expect(updatedOptions).toMatchObject(optionsToCompare); expect(updatedOptions).toMatchObject(optionsToCompare);

View File

@ -8,14 +8,14 @@ import { PerformMetadataQueryParams } from 'test/integration/metadata/types/perf
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util'; import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
export const createOneFieldMetadata = async <T extends FieldMetadataType>({ export const createOneFieldMetadata = async <T extends FieldMetadataType>({
input, input,
gqlFields, gqlFields,
expectToFail = false, expectToFail = false,
}: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{ }: PerformMetadataQueryParams<CreateOneFieldFactoryInput>): CommonResponseBody<{
createOneField: FieldMetadataInterface<T>; createOneField: FieldMetadataDTO<T>;
}> => { }> => {
const graphqlOperation = createOneFieldMetadataQueryFactory({ const graphqlOperation = createOneFieldMetadataQueryFactory({
input, input,

View File

@ -1,16 +1,16 @@
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util'; import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util'; import { createRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-relation-between-objects.util';
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { EachTestingContext } from 'twenty-shared/testing'; import { EachTestingContext } from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
type DeleteOneObjectMetadataItemTestingContext = EachTestingContext< type DeleteOneObjectMetadataItemTestingContext = EachTestingContext<
(args: { objectMetadataIdToDelete: string; relationFieldId: string }) => { (args: { objectMetadataIdToDelete: string; relationFieldId: string }) => {
objectMetadataIdToDelete: string; objectMetadataIdToDelete: string;
@ -44,11 +44,12 @@ const successfulDeleteTargetUseCase: DeleteOneObjectMetadataItemTestingContext =
describe('Delete Object metadata with relation should succeed', () => { describe('Delete Object metadata with relation should succeed', () => {
let createdObjectMetadataPersonId = ''; let createdObjectMetadataPersonId = '';
let createdObjectMetadataOpportunityId = ''; let createdObjectMetadataOpportunityId = '';
let createdRelationField: FieldMetadataInterface<FieldMetadataType.RELATION>;
let globalTestContext: { let globalTestContext: {
opportunityMetadataId: string; opportunityMetadataId: string;
personMetadataId: string; personMetadataId: string;
relationField: FieldMetadataInterface<FieldMetadataType.RELATION>; relationField: FieldMetadataDTO & {
relation: RelationDTO;
};
}; };
beforeEach(async () => { beforeEach(async () => {
@ -56,7 +57,7 @@ describe('Delete Object metadata with relation should succeed', () => {
data: { data: {
createOneObject: { id: objectMetadataPersonId }, createOneObject: { id: objectMetadataPersonId },
}, },
} = await createOneObjectMetadata({ } = await forceCreateOneObjectMetadata({
input: { input: {
nameSingular: 'personForRelation', nameSingular: 'personForRelation',
namePlural: 'peopleForRelation', namePlural: 'peopleForRelation',
@ -72,7 +73,7 @@ describe('Delete Object metadata with relation should succeed', () => {
data: { data: {
createOneObject: { id: objectMetadataOpportunityId }, createOneObject: { id: objectMetadataOpportunityId },
}, },
} = await createOneObjectMetadata({ } = await forceCreateOneObjectMetadata({
input: { input: {
nameSingular: 'opportunityForRelation', nameSingular: 'opportunityForRelation',
namePlural: 'opportunitiesForRelation', namePlural: 'opportunitiesForRelation',
@ -84,24 +85,20 @@ describe('Delete Object metadata with relation should succeed', () => {
createdObjectMetadataOpportunityId = objectMetadataOpportunityId; createdObjectMetadataOpportunityId = objectMetadataOpportunityId;
createdRelationField =
await createRelationBetweenObjects<FieldMetadataType.RELATION>({
objectMetadataId: createdObjectMetadataOpportunityId,
targetObjectMetadataId: createdObjectMetadataPersonId,
type: FieldMetadataType.RELATION,
relationType: RelationType.MANY_TO_ONE,
});
globalTestContext = { globalTestContext = {
opportunityMetadataId: createdObjectMetadataOpportunityId, opportunityMetadataId: createdObjectMetadataOpportunityId,
personMetadataId: createdObjectMetadataPersonId, personMetadataId: createdObjectMetadataPersonId,
relationField: createdRelationField, relationField:
await createRelationBetweenObjects<FieldMetadataType.RELATION>({
objectMetadataId: createdObjectMetadataOpportunityId,
targetObjectMetadataId: createdObjectMetadataPersonId,
type: FieldMetadataType.RELATION,
relationType: RelationType.MANY_TO_ONE,
}),
}; };
}); });
afterEach(async () => { afterEach(async () => {
await deleteOneFieldMetadata({
input: { idToDelete: createdRelationField.id },
});
await deleteOneObjectMetadata({ await deleteOneObjectMetadata({
input: { idToDelete: createdObjectMetadataPersonId }, input: { idToDelete: createdObjectMetadataPersonId },
}); });
@ -128,14 +125,9 @@ describe('Delete Object metadata with relation should succeed', () => {
}); });
it.each(successfulDeleteTargetUseCase)('$title', async ({ context }) => { it.each(successfulDeleteTargetUseCase)('$title', async ({ context }) => {
if (!isDefined(globalTestContext.relationField.relation)) {
throw new Error('Relation field relation is undefined');
}
const computedContext = context({ const computedContext = context({
objectMetadataIdToDelete: globalTestContext.opportunityMetadataId, objectMetadataIdToDelete: globalTestContext.opportunityMetadataId,
relationFieldId: relationFieldId: globalTestContext.relationField.id,
globalTestContext.relationField.relation.targetFieldMetadata.id,
}); });
await deleteOneObjectMetadata({ await deleteOneObjectMetadata({

View File

@ -4,6 +4,9 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
export const createRelationBetweenObjects = async < export const createRelationBetweenObjects = async <
T extends FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION, T extends FieldMetadataType.RELATION | FieldMetadataType.MORPH_RELATION,
>({ >({
@ -41,22 +44,35 @@ export const createRelationBetweenObjects = async <
}, },
}; };
const { const { data } = await createOneFieldMetadata<typeof type>({
data: { createOneField: createdFieldPerson },
} = await createOneFieldMetadata<typeof type>({
input: createFieldInput, input: createFieldInput,
gqlFields: ` gqlFields: `
id id
name name
label label
isLabelSyncedWithName isLabelSyncedWithName
settings
relation { relation {
type type
sourceObjectMetadata {
id
nameSingular
namePlural
}
targetObjectMetadata {
id
nameSingular
namePlural
}
sourceFieldMetadata {
id
name
}
targetFieldMetadata { targetFieldMetadata {
id id
name
} }
} }
settings
object { object {
id id
nameSingular nameSingular
@ -65,5 +81,7 @@ export const createRelationBetweenObjects = async <
expectToFail: false, expectToFail: false,
}); });
return createdFieldPerson; return data.createOneField as unknown as FieldMetadataDTO<T> & {
relation: RelationDTO;
};
}; };

View File

@ -6,6 +6,7 @@ export const eachTestingContextFilter = <T>(
const onlyTestsCases = testCases.filter((testCase) => testCase.only === true); const onlyTestsCases = testCases.filter((testCase) => testCase.only === true);
if (process.env.CI && onlyTestsCases.length > 0) { if (process.env.CI && onlyTestsCases.length > 0) {
// eslint-disable-next-line no-console
console.warn( console.warn(
'Should never push tests cases with an only to true, only to use in dev env\n returning the whole test suite anyway', 'Should never push tests cases with an only to true, only to use in dev env\n returning the whole test suite anyway',
); );

View File

@ -10,3 +10,8 @@
export { eachTestingContextFilter } from './EachTestingContextFilter'; export { eachTestingContextFilter } from './EachTestingContextFilter';
export type { EachTestingContext } from './types/EachTestingContext.type'; export type { EachTestingContext } from './types/EachTestingContext.type';
export type { SuccessfulAndFailingTestCases } from './types/SuccessfulAndFailingTestCases'; export type { SuccessfulAndFailingTestCases } from './types/SuccessfulAndFailingTestCases';
export type {
Expect,
Equal,
HasAllProperties,
} from './types/TestingGenerics.type';

View File

@ -0,0 +1,59 @@
/**
* Type testing utilities for TypeScript
* @module TypeTesting
*/
/**
* A type utility for testing TypeScript type assertions
*
* @template T - The actual type to test
*/
export type Expect<T extends true> = T;
/**
* Tests if two types are exactly equal
*
* @template A - First type to compare
* @template B - Second type to compare
*/
export type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
? true
: false;
/**
* Tests if a type has all required properties of another type
* Works with both class types and regular object types
*
* @template T - Type that should contain the properties
* @template U - Type whose properties should be contained in T
*/
export type HasAllProperties<T, U> = [T] extends [new (...args: any[]) => any]
? HasAllProperties<InstanceType<T>, U>
: [U] extends [new (...args: any[]) => any]
? HasAllProperties<T, InstanceType<U>>
: {
[K in keyof U]: K extends keyof T ? Equal<T[K], U[K]> : false;
}[keyof U] extends true
? true
: false;
class TestClass {
id!: string;
name!: string;
}
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type BasicTests = [
Expect<Equal<string, string>>,
Expect<Equal<number, number>>,
Expect<Equal<{ a: string }, { a: string }>>,
Expect<HasAllProperties<{ a: string; b: number }, { a: string }>>,
Expect<HasAllProperties<{ a: string; b: number }, { a: string; b: number }>>,
Expect<HasAllProperties<{ a: never; b: never }, { a: never }>>,
Expect<HasAllProperties<TestClass, { id: string }>>,
Expect<HasAllProperties<{ id: string; name: string }, { id: string }>>,
];