Add default fields on object manifest (#17977)
as title <img width="830" height="227" alt="image" src="https://github.com/user-attachments/assets/8cbf4a14-5d1d-496f-a146-f31079e6e602" />
This commit is contained in:
@@ -277,7 +277,11 @@ Key points:
|
||||
- You can scaffold new objects using `yarn twenty entity:add`, which guides you through naming, fields, and relationships.
|
||||
|
||||
<Note>
|
||||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
|
||||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields
|
||||
such as `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` and `deletedAt`.
|
||||
You don't need to define these in your `fields` array — only add your custom fields.
|
||||
You can override default fields by defining a field with the same name in your `fields` array,
|
||||
but this is not recommended.
|
||||
</Note>
|
||||
|
||||
|
||||
|
||||
+153
@@ -1,6 +1,7 @@
|
||||
import { FieldType } from '@/sdk';
|
||||
import type { Manifest } from 'twenty-shared/application';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
export const EXPECTED_MANIFEST: Manifest = {
|
||||
publicAssets: [
|
||||
@@ -121,6 +122,82 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldType.TEXT,
|
||||
universalIdentifier: 'b0b1b2b3-b4b5-4000-8000-000000000003',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: 'b5830f92-1abe-5ae7-b689-7821dda00b15',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'ae2b2e5a-6c8b-5324-b639-8f17e3bc0420',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '7c84a501-41a6-531e-bc5e-8062843f6cba',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '72c2903a-91c9-5f36-8668-b53b083ae4cf',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '007f5d8e-d376-519e-bd07-aea41a53f7d9',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '44053354-0d8b-5d66-bfa8-7e7d221de3db',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '38e2c671-cbf5-5773-a04a-e5a2d6c323c8',
|
||||
},
|
||||
],
|
||||
icon: 'IconNote',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
@@ -199,6 +276,82 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
type: FieldType.DATE_TIME,
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '56e652a3-fac2-54fd-a379-f01b72758268',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: 'c59a149f-5861-51a3-a924-2bd2fa31daaa',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '236e2ec5-3636-5b97-b19d-f7799cb88d99',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '2cf640eb-09a7-52f2-820b-419071c5ec24',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '2eb62b4a-6888-58ab-9887-a879321fff31',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: '36e95d21-9c86-5b6f-8133-a66d09fc8952',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'dc5231a4-e128-57b0-ac9d-723678d9b239',
|
||||
},
|
||||
],
|
||||
icon: 'IconMail',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
|
||||
+77
@@ -1,5 +1,6 @@
|
||||
import { FieldType } from '@/sdk';
|
||||
import type { Manifest } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
export const EXPECTED_MANIFEST: Manifest = {
|
||||
application: {
|
||||
@@ -31,6 +32,82 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
label: 'Title',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
defaultValue: 'uuid',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
label: 'Id',
|
||||
name: 'id',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: '5bdb4f57-a349-56d0-b421-845fd55b3026',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
label: 'Name',
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: '86cc7b06-26de-5b7b-bf4a-31fa246dfd90',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
label: 'Creation date',
|
||||
name: 'createdAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'abe3bc69-1a51-5814-a722-74f5a025e92a',
|
||||
},
|
||||
{
|
||||
defaultValue: 'now',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
label: 'Last update',
|
||||
name: 'updatedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: 'c296d408-9720-5a86-8586-3490fbf04b86',
|
||||
},
|
||||
{
|
||||
defaultValue: null,
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
label: 'Deleted at',
|
||||
name: 'deletedAt',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: '488333a2-f4ff-5790-88ef-21f07f8f1985',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
label: 'Created by',
|
||||
name: 'createdBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'fba6e01b-6579-554e-ad64-a676003c8e68',
|
||||
},
|
||||
{
|
||||
defaultValue: {
|
||||
name: "''",
|
||||
source: "'MANUAL'",
|
||||
},
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
label: 'Updated by',
|
||||
name: 'updatedBy',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: 'fa60f87d-15c8-5ff7-9da0-191fa14a922f',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -205,7 +205,10 @@ export class ApiService {
|
||||
try {
|
||||
const mutation = `
|
||||
mutation SyncApplication($manifest: JSON!) {
|
||||
syncApplication(manifest: $manifest)
|
||||
syncApplication(manifest: $manifest) {
|
||||
applicationUniversalIdentifier
|
||||
actions
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from 'twenty-shared/application';
|
||||
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
|
||||
|
||||
const loadSources = async (appPath: string): Promise<string[]> => {
|
||||
return await glob(['**/*.ts', '**/*.tsx'], {
|
||||
@@ -104,14 +105,26 @@ export const buildManifest = async (
|
||||
filePath,
|
||||
});
|
||||
|
||||
const { labelIdentifierFieldMetadataUniversalIdentifier, ...rest } =
|
||||
extract.config;
|
||||
const objectFieldsWithDefaultFields = injectDefaultFieldsInObjectFields(
|
||||
extract.config,
|
||||
);
|
||||
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
extract.config.labelIdentifierFieldMetadataUniversalIdentifier ??
|
||||
objectFieldsWithDefaultFields.find((field) => field.name === 'name')
|
||||
?.universalIdentifier;
|
||||
|
||||
if (!labelIdentifierFieldMetadataUniversalIdentifier) {
|
||||
errors.push(
|
||||
`No label identifier field found for object ${extract.config.nameSingular}. Please add a field with name "name" to your object.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const objectManifest: ObjectManifest = {
|
||||
...rest,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier:
|
||||
// TODO replace by system id universal identifier once we've refactored it
|
||||
labelIdentifierFieldMetadataUniversalIdentifier ?? '',
|
||||
...extract.config,
|
||||
fields: objectFieldsWithDefaultFields,
|
||||
labelIdentifierFieldMetadataUniversalIdentifier,
|
||||
};
|
||||
|
||||
objects.push(objectManifest);
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
|
||||
import { type ObjectConfig } from '@/sdk/objects/object-config';
|
||||
|
||||
describe('generateDefaultFieldUniversalIdentifier', () => {
|
||||
it('should generate a unique universal identifier', () => {
|
||||
const objectConfig = {
|
||||
universalIdentifier: '55b79f88-4094-4b3f-a0ac-1a91a55714f2',
|
||||
} as ObjectConfig;
|
||||
|
||||
const uId1 = generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'id',
|
||||
});
|
||||
const uId2 = generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'id',
|
||||
});
|
||||
|
||||
const anotherUId = generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'name',
|
||||
});
|
||||
|
||||
expect(uId1).toEqual(uId2);
|
||||
expect(uId1).not.toEqual(anotherUId);
|
||||
});
|
||||
});
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
|
||||
const mockObjectConfig: ObjectConfig = {
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
fields: [],
|
||||
};
|
||||
|
||||
const expectedUniversalId = (fieldName: string) =>
|
||||
generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig: mockObjectConfig,
|
||||
fieldName,
|
||||
});
|
||||
|
||||
describe('getDefaultObjectFields', () => {
|
||||
it('should return an array of 9 default fields', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
expect(fields).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('should include an id field with UUID type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const idField = fields.find((field) => field.name === 'id');
|
||||
|
||||
expect(idField).toBeDefined();
|
||||
expect(idField).toEqual({
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
defaultValue: 'uuid',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: expectedUniversalId('id'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include a name field with TEXT type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const nameField = fields.find((field) => field.name === 'name');
|
||||
|
||||
expect(nameField).toBeDefined();
|
||||
expect(nameField).toEqual({
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: expectedUniversalId('name'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include updatedAt and deletedAt fields with DATE_TIME type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const updatedAtFields = fields.filter(
|
||||
(field) => field.name === 'updatedAt',
|
||||
);
|
||||
const deletedAtField = fields.find((field) => field.name === 'deletedAt');
|
||||
|
||||
expect(updatedAtFields).toHaveLength(1);
|
||||
expect(updatedAtFields[0]).toEqual({
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: expectedUniversalId('updatedAt'),
|
||||
});
|
||||
|
||||
expect(deletedAtField).toBeDefined();
|
||||
expect(deletedAtField).toEqual({
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: expectedUniversalId('deletedAt'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include createdBy and updatedBy fields with ACTOR type', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const createdByField = fields.find((field) => field.name === 'createdBy');
|
||||
const updatedByField = fields.find((field) => field.name === 'updatedBy');
|
||||
|
||||
expect(createdByField).toBeDefined();
|
||||
expect(createdByField).toEqual({
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: expectedUniversalId('createdBy'),
|
||||
});
|
||||
|
||||
expect(updatedByField).toBeDefined();
|
||||
expect(updatedByField).toEqual({
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: expectedUniversalId('updatedBy'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate deterministic universalIdentifiers based on objectConfig', () => {
|
||||
const firstResult = getDefaultObjectFields(mockObjectConfig);
|
||||
const secondResult = getDefaultObjectFields(mockObjectConfig);
|
||||
|
||||
firstResult.forEach((field, index) => {
|
||||
expect(field.universalIdentifier).toBe(
|
||||
secondResult[index].universalIdentifier,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate different universalIdentifiers for different objectConfigs', () => {
|
||||
const otherObjectConfig: ObjectConfig = {
|
||||
...mockObjectConfig,
|
||||
universalIdentifier: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||
};
|
||||
|
||||
const fieldsA = getDefaultObjectFields(mockObjectConfig);
|
||||
const fieldsB = getDefaultObjectFields(otherObjectConfig);
|
||||
|
||||
fieldsA.forEach((fieldA, index) => {
|
||||
expect(fieldA.universalIdentifier).not.toBe(
|
||||
fieldsB[index].universalIdentifier,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return fields in the expected order', () => {
|
||||
const fields = getDefaultObjectFields(mockObjectConfig);
|
||||
const fieldNames = fields.map((field) => field.name);
|
||||
|
||||
expect(fieldNames).toEqual([
|
||||
'id',
|
||||
'name',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
|
||||
const baseObjectConfig: ObjectConfig = {
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
nameSingular: 'testObject',
|
||||
namePlural: 'testObjects',
|
||||
labelSingular: 'Test Object',
|
||||
labelPlural: 'Test Objects',
|
||||
fields: [],
|
||||
};
|
||||
|
||||
describe('injectDefaultFieldsInObjectFields', () => {
|
||||
it('should return all default fields when objectConfig has no fields', () => {
|
||||
const result = injectDefaultFieldsInObjectFields(baseObjectConfig);
|
||||
const defaultFields = getDefaultObjectFields(baseObjectConfig);
|
||||
|
||||
expect(result).toEqual(defaultFields);
|
||||
});
|
||||
|
||||
it('should preserve custom fields and append missing default fields', () => {
|
||||
const customField = {
|
||||
universalIdentifier: '11111111-1111-1111-1111-111111111111',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getDefaultObjectFields(objectConfig);
|
||||
|
||||
expect(result[0]).toEqual(customField);
|
||||
expect(result).toHaveLength(1 + defaultFields.length);
|
||||
});
|
||||
|
||||
it('should not inject a default field when a field with the same name exists', () => {
|
||||
const customIdField = {
|
||||
universalIdentifier: '22222222-2222-2222-2222-222222222222',
|
||||
name: 'id',
|
||||
label: 'Custom Id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customIdField],
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
const idFields = result.filter((f) => f.name === 'id');
|
||||
|
||||
expect(idFields).toHaveLength(1);
|
||||
expect(idFields[0]).toEqual(customIdField);
|
||||
});
|
||||
|
||||
it('should skip multiple default fields when overridden by custom fields', () => {
|
||||
const customFields = [
|
||||
{
|
||||
universalIdentifier: '33333333-3333-3333-3333-333333333333',
|
||||
name: 'id',
|
||||
label: 'Custom Id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '44444444-4444-4444-4444-444444444444',
|
||||
name: 'name',
|
||||
label: 'Custom Name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '55555555-5555-5555-5555-555555555555',
|
||||
name: 'position',
|
||||
label: 'Custom Position',
|
||||
type: FieldMetadataType.NUMBER,
|
||||
},
|
||||
];
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: customFields,
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
const defaultFields = getDefaultObjectFields(objectConfig);
|
||||
const overriddenCount = defaultFields.filter((df) =>
|
||||
customFields.some((cf) => cf.name === df.name),
|
||||
).length;
|
||||
|
||||
expect(result).toHaveLength(
|
||||
customFields.length + defaultFields.length - overriddenCount,
|
||||
);
|
||||
|
||||
expect(result.filter((f) => f.name === 'id')).toHaveLength(1);
|
||||
expect(result.filter((f) => f.name === 'name')).toHaveLength(1);
|
||||
expect(result.filter((f) => f.name === 'position')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should place custom fields before default fields in the result', () => {
|
||||
const customField = {
|
||||
universalIdentifier: '66666666-6666-6666-6666-666666666666',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const result = injectDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(result[0]).toEqual(customField);
|
||||
});
|
||||
|
||||
it('should not mutate the original objectConfig fields array', () => {
|
||||
const customField = {
|
||||
universalIdentifier: '77777777-7777-7777-7777-777777777777',
|
||||
name: 'customField',
|
||||
label: 'Custom Field',
|
||||
type: FieldMetadataType.TEXT,
|
||||
};
|
||||
|
||||
const objectConfig: ObjectConfig = {
|
||||
...baseObjectConfig,
|
||||
fields: [customField],
|
||||
};
|
||||
|
||||
const originalLength = objectConfig.fields.length;
|
||||
|
||||
injectDefaultFieldsInObjectFields(objectConfig);
|
||||
|
||||
expect(objectConfig.fields).toHaveLength(originalLength);
|
||||
});
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { v5 } from 'uuid';
|
||||
|
||||
const UNIVERSAL_IDENTIFIER_NAMESPACE = '142046f0-4d80-48b5-ad56-26ad410e895c';
|
||||
|
||||
export const generateDefaultFieldUniversalIdentifier = ({
|
||||
objectConfig,
|
||||
fieldName,
|
||||
}: {
|
||||
objectConfig: ObjectConfig;
|
||||
fieldName: string;
|
||||
}) => {
|
||||
const seed = `${objectConfig.universalIdentifier}-${fieldName}`;
|
||||
|
||||
return v5(seed, UNIVERSAL_IDENTIFIER_NAMESPACE);
|
||||
};
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { type ObjectManifest } from 'twenty-shared/application';
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { generateDefaultFieldUniversalIdentifier } from '@/cli/utilities/build/manifest/utils/generate-default-field-universal-identifier';
|
||||
|
||||
export const getDefaultObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
): ObjectManifest['fields'] => {
|
||||
const idField = {
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
description: 'Id',
|
||||
icon: 'Icon123',
|
||||
isNullable: false,
|
||||
defaultValue: 'uuid',
|
||||
type: FieldMetadataType.UUID,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'id',
|
||||
}),
|
||||
};
|
||||
|
||||
const nameField = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name',
|
||||
icon: 'IconAbc',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.TEXT,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'name',
|
||||
}),
|
||||
};
|
||||
|
||||
const createdAtField = {
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
description: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'createdAt',
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedAtField = {
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
description: 'Last time the record was changed',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: false,
|
||||
defaultValue: 'now',
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'updatedAt',
|
||||
}),
|
||||
};
|
||||
|
||||
const deletedAtField = {
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
description: 'Deletion date',
|
||||
icon: 'IconCalendarClock',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'deletedAt',
|
||||
}),
|
||||
};
|
||||
|
||||
const createdByField = {
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
description: 'The creator of the record',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'createdBy',
|
||||
}),
|
||||
};
|
||||
|
||||
const updatedByField = {
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
description: 'The workspace member who last updated the record',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
type: FieldMetadataType.ACTOR,
|
||||
universalIdentifier: generateDefaultFieldUniversalIdentifier({
|
||||
objectConfig,
|
||||
fieldName: 'updatedBy',
|
||||
}),
|
||||
};
|
||||
|
||||
return [
|
||||
idField,
|
||||
nameField,
|
||||
createdAtField,
|
||||
updatedAtField,
|
||||
deletedAtField,
|
||||
createdByField,
|
||||
updatedByField,
|
||||
];
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import type { ObjectConfig } from '@/sdk/objects/object-config';
|
||||
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
|
||||
|
||||
export const injectDefaultFieldsInObjectFields = (
|
||||
objectConfig: ObjectConfig,
|
||||
) => {
|
||||
const defaultObjectFields = getDefaultObjectFields(objectConfig);
|
||||
|
||||
const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name);
|
||||
|
||||
const fieldsWithDefaultFields = [...objectConfig.fields];
|
||||
|
||||
for (const defaultField of defaultObjectFields) {
|
||||
if (!objectConfigFieldNames.includes(defaultField.name)) {
|
||||
fieldsWithDefaultFields.push(defaultField);
|
||||
}
|
||||
}
|
||||
|
||||
return fieldsWithDefaultFields;
|
||||
};
|
||||
Reference in New Issue
Block a user