Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ac4e5cc
Add missing fields
martmull Feb 16, 2026
ea71d29
Reorganize files
martmull Feb 16, 2026
369ee28
Fix typing
martmull Feb 16, 2026
d5dac06
Add default relation fields
martmull Feb 16, 2026
b00c7e0
Remove TS_VECTOR and POSITION validation
martmull Feb 17, 2026
a889b3b
WIP
martmull Feb 17, 2026
cec2a65
Merge branch 'main' into fix-sync-2
martmull Feb 17, 2026
0c6e16e
WIP
martmull Feb 17, 2026
c99a876
Merge branch 'main' into fix-sync-2
martmull Feb 17, 2026
6d9dc34
WIP
martmull Feb 17, 2026
51003c8
Merge branch 'main' into fix-sync-2
martmull Feb 17, 2026
7a0d274
refactor(server): manifest sync build on dependency all flat entity maps
prastoin Feb 18, 2026
959ee56
Set proper default relation fields
martmull Feb 18, 2026
a7b9f33
Merge branch 'main' into fix-sync-2
martmull Feb 19, 2026
8c0b024
Merge branch 'main' into fix-sync-2
martmull Feb 19, 2026
654d4bd
fix(server): dependency flat maps as from
prastoin Feb 19, 2026
bb586ac
fix
prastoin Feb 19, 2026
0e3b13a
Fix wrong handlerName check
martmull Feb 19, 2026
ca8631e
fix(server): dependency flat entity maps
prastoin Feb 19, 2026
f4facfe
Merge branch 'main' into fix-sync-2
martmull Feb 19, 2026
841afe3
fix(server): restore validators of fields position and ts_vector
prastoin Feb 19, 2026
a76ca26
refactor(server): simplify computeApplicationManifestAllUniversalFlat…
prastoin Feb 19, 2026
9041474
refacotr(server): fieldManifest type
prastoin Feb 19, 2026
7e26a7d
Merge branch 'main' into fix-sync-2
martmull Feb 19, 2026
c34db90
Fix test
martmull Feb 19, 2026
4558f30
Merge branch 'main' into fix-sync-2
martmull Feb 19, 2026
00bcb0c
Merge branch 'main' into fix-sync-2
martmull Feb 20, 2026
d9b7460
Fix test
martmull Feb 20, 2026
a5da69b
Refactor constants
martmull Feb 20, 2026
e50bfa4
Fix test
martmull Feb 20, 2026
6830738
Fix lint
martmull Feb 20, 2026
f1629d1
Fix ci
martmull Feb 20, 2026
bfa862a
Merge branch 'main' into fix-sync-2
martmull Feb 20, 2026
705ca73
Merge branch 'main' into fix-sync-2
martmull Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
TARGET_FUNCTION_TO_ENTITY_KEY_MAPPING,
} from '@/cli/utilities/build/manifest/manifest-extract-config';
import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file';
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
import {
type ApplicationConfig,
type FrontComponentConfig,
Expand Down Expand Up @@ -33,6 +32,7 @@ import {
} from 'twenty-shared/application';
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
import { assertUnreachable } from 'twenty-shared/utils';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-fields-in-object-fields';

const loadSources = async (appPath: string): Promise<string[]> => {
return await glob(['**/*.ts', '**/*.tsx'], {
Expand Down Expand Up @@ -117,13 +117,14 @@ export const buildManifest = async (
filePath,
});

const objectFieldsWithDefaultFields = injectDefaultFieldsInObjectFields(
extract.config,
);
const {
objectFields: objectFieldsWithDefaults,
fields: reverseRelationFields,
} = getDefaultFieldsInObjectFields(extract.config);

const labelIdentifierFieldMetadataUniversalIdentifier =
extract.config.labelIdentifierFieldMetadataUniversalIdentifier ??
objectFieldsWithDefaultFields.find((field) => field.name === 'name')
objectFieldsWithDefaults.find((field) => field.name === 'name')
?.universalIdentifier;

if (!labelIdentifierFieldMetadataUniversalIdentifier) {
Expand All @@ -135,11 +136,12 @@ export const buildManifest = async (

const objectManifest: ObjectManifest = {
...extract.config,
fields: objectFieldsWithDefaultFields,
fields: objectFieldsWithDefaults,
labelIdentifierFieldMetadataUniversalIdentifier,
};

objects.push(objectManifest);
fields.push(...reverseRelationFields);

errors.push(...extract.errors);
objectsFilePaths.push(relativePath);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { injectDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/inject-default-fields-in-object-fields';
import { getDefaultFieldsInObjectFields } from '@/cli/utilities/build/manifest/utils/get-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';
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
import { type ObjectFieldManifest } from 'twenty-shared/application';

const baseObjectConfig: ObjectConfig = {
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
Expand All @@ -12,16 +14,25 @@
fields: [],
};

describe('injectDefaultFieldsInObjectFields', () => {
describe('getDefaultFieldsInObjectFields', () => {
it('should return all default fields when objectConfig has no fields', () => {
const result = injectDefaultFieldsInObjectFields(baseObjectConfig);
const { objectFields, fields } =
getDefaultFieldsInObjectFields(baseObjectConfig);
const defaultFields = getDefaultObjectFields(baseObjectConfig);

expect(result).toEqual(defaultFields);
const {
objectFields: defaultRelationObjectFields,
fields: expectedReverseFields,
} = getDefaultRelationObjectFields(baseObjectConfig);

expect(objectFields).toEqual([
...defaultFields,
...defaultRelationObjectFields,
]);
expect(fields).toEqual(expectedReverseFields);
});

it('should preserve custom fields and append missing default fields', () => {
const customField = {
const customField: ObjectFieldManifest = {
universalIdentifier: '11111111-1111-1111-1111-111111111111',
name: 'customField',
label: 'Custom Field',
Expand All @@ -33,15 +44,19 @@
fields: [customField],
};

const result = injectDefaultFieldsInObjectFields(objectConfig);
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
const defaultFields = getDefaultObjectFields(objectConfig);
const { objectFields: defaultRelationObjectFields } =
getDefaultRelationObjectFields(objectConfig);

expect(result[0]).toEqual(customField);
expect(result).toHaveLength(1 + defaultFields.length);
expect(objectFields[0]).toEqual(customField);
expect(objectFields).toHaveLength(
1 + defaultFields.length + defaultRelationObjectFields.length,
);
});

it('should not inject a default field when a field with the same name exists', () => {
const customIdField = {
const customIdField: ObjectFieldManifest = {
universalIdentifier: '22222222-2222-2222-2222-222222222222',
name: 'id',
label: 'Custom Id',
Expand All @@ -53,16 +68,16 @@
fields: [customIdField],
};

const result = injectDefaultFieldsInObjectFields(objectConfig);
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);

const idFields = result.filter((f) => f.name === 'id');
const idFields = objectFields.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 = [
const customFields: ObjectFieldManifest[] = [
{
universalIdentifier: '33333333-3333-3333-3333-333333333333',
name: 'id',
Expand All @@ -88,23 +103,28 @@
fields: customFields,
};

const result = injectDefaultFieldsInObjectFields(objectConfig);
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);
const defaultFields = getDefaultObjectFields(objectConfig);
const { objectFields: defaultRelationObjectFields } =
getDefaultRelationObjectFields(objectConfig);
const overriddenCount = defaultFields.filter((df) =>
customFields.some((cf) => cf.name === df.name),
).length;

expect(result).toHaveLength(
customFields.length + defaultFields.length - overriddenCount,
expect(objectFields).toHaveLength(
customFields.length +
defaultFields.length +
defaultRelationObjectFields.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);
expect(objectFields.filter((f) => f.name === 'id')).toHaveLength(1);
expect(objectFields.filter((f) => f.name === 'name')).toHaveLength(1);
expect(objectFields.filter((f) => f.name === 'position')).toHaveLength(1);
});

it('should place custom fields before default fields in the result', () => {
const customField = {
const customField: ObjectFieldManifest = {
universalIdentifier: '66666666-6666-6666-6666-666666666666',
name: 'customField',
label: 'Custom Field',
Expand All @@ -116,13 +136,13 @@
fields: [customField],
};

const result = injectDefaultFieldsInObjectFields(objectConfig);
const { objectFields } = getDefaultFieldsInObjectFields(objectConfig);

expect(result[0]).toEqual(customField);
expect(objectFields[0]).toEqual(customField);
});

it('should not mutate the original objectConfig fields array', () => {
const customField = {
const customField: ObjectFieldManifest = {
universalIdentifier: '77777777-7777-7777-7777-777777777777',
name: 'customField',
label: 'Custom Field',
Expand All @@ -136,8 +156,19 @@

const originalLength = objectConfig.fields.length;

injectDefaultFieldsInObjectFields(objectConfig);
getDefaultFieldsInObjectFields(objectConfig);

expect(objectConfig.fields).toHaveLength(originalLength);
});

it('should return reverse relation fields for each default relation', () => {
const { fields } = getDefaultFieldsInObjectFields(baseObjectConfig);

expect(fields).toHaveLength(5);

const fieldNames = fields.map((f) => f.name);

expect(fieldNames).toContain('targetTestObject');
expect(fieldNames).toContain('testObject');

Check failure on line 172 in packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts

View workflow job for this annotation

GitHub Actions / sdk-test (test:unit)

[twenty-sdk-unit] src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts > getDefaultFieldsInObjectFields > should return reverse relation fields for each default relation

AssertionError: expected [ 'targetTestObject', …(4) ] to include 'testObject' ❯ src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts:172:24

Check failure on line 172 in packages/twenty-sdk/src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts

View workflow job for this annotation

GitHub Actions / sdk-test (test:unit)

[twenty-sdk-unit] src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts > getDefaultFieldsInObjectFields > should return reverse relation fields for each default relation

AssertionError: expected [ 'targetTestObject', …(4) ] to include 'testObject' ❯ src/cli/utilities/build/manifest/utils/__tests__/get-default-fields-in-object-fields.spec.ts:172:24
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('getDefaultObjectFields', () => {
it('should return an array of 9 default fields', () => {
const fields = getDefaultObjectFields(mockObjectConfig);

expect(fields).toHaveLength(7);
expect(fields).toHaveLength(9);
});

it('should include an id field with UUID type', () => {
Expand Down Expand Up @@ -59,15 +59,26 @@ describe('getDefaultObjectFields', () => {
});
});

it('should include updatedAt and deletedAt fields with DATE_TIME type', () => {
it('should include createdAt, updatedAt and deletedAt fields with DATE_TIME type', () => {
const fields = getDefaultObjectFields(mockObjectConfig);
const updatedAtFields = fields.filter(
(field) => field.name === 'updatedAt',
);
const createdAtField = fields.find((field) => field.name === 'createdAt');
const updatedAtField = fields.find((field) => field.name === 'updatedAt');
const deletedAtField = fields.find((field) => field.name === 'deletedAt');

expect(updatedAtFields).toHaveLength(1);
expect(updatedAtFields[0]).toEqual({
expect(createdAtField).toBeDefined();
expect(createdAtField).toEqual({
name: 'createdAt',
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isNullable: false,
defaultValue: 'now',
type: FieldMetadataType.DATE_TIME,
universalIdentifier: expectedUniversalId('createdAt'),
});

expect(updatedAtField).toBeDefined();
expect(updatedAtField).toEqual({
name: 'updatedAt',
label: 'Last update',
description: 'Last time the record was changed',
Expand Down Expand Up @@ -121,6 +132,42 @@ describe('getDefaultObjectFields', () => {
});
});

it('should include a position field with POSITION type', () => {
const fields = getDefaultObjectFields(mockObjectConfig);
const positionField = fields.find((field) => field.name === 'position');

expect(positionField).toBeDefined();
expect(positionField).toEqual({
name: 'position',
label: 'Position',
description: 'Position',
icon: 'IconHierarchy2',
isNullable: false,
defaultValue: 0,
type: FieldMetadataType.POSITION,
universalIdentifier: expectedUniversalId('position'),
});
});

it('should include a searchVector field with TS_VECTOR type', () => {
const fields = getDefaultObjectFields(mockObjectConfig);
const searchVectorField = fields.find(
(field) => field.name === 'searchVector',
);

expect(searchVectorField).toBeDefined();
expect(searchVectorField).toEqual({
name: 'searchVector',
label: 'Search vector',
description: 'Search vector',
icon: 'IconSearch',
isNullable: true,
defaultValue: null,
type: FieldMetadataType.TS_VECTOR,
universalIdentifier: expectedUniversalId('searchVector'),
});
});

it('should generate deterministic universalIdentifiers based on objectConfig', () => {
const firstResult = getDefaultObjectFields(mockObjectConfig);
const secondResult = getDefaultObjectFields(mockObjectConfig);
Expand Down Expand Up @@ -160,6 +207,8 @@ describe('getDefaultObjectFields', () => {
'deletedAt',
'createdBy',
'updatedBy',
'position',
'searchVector',
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ObjectConfig } from '@/sdk/objects/object-config';
import { getDefaultObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-object-fields';
import { getDefaultRelationObjectFields } from '@/cli/utilities/build/manifest/utils/get-default-relation-object-fields';
import {
type FieldManifest,
type ObjectFieldManifest,
} from 'twenty-shared/application';

export const getDefaultFieldsInObjectFields = (
objectConfig: ObjectConfig,
): { objectFields: ObjectFieldManifest[]; fields: FieldManifest[] } => {
const defaultObjectFields = getDefaultObjectFields(objectConfig);
const { objectFields: defaultRelationObjectFields, fields: reverseFields } =
getDefaultRelationObjectFields(objectConfig);

const objectConfigFieldNames = (objectConfig.fields ?? []).map((f) => f.name);

const objectFieldsWithDefaults = [...objectConfig.fields];

for (const defaultField of defaultObjectFields) {
if (!objectConfigFieldNames.includes(defaultField.name)) {
objectFieldsWithDefaults.push(defaultField);
}
}

for (const defaultRelationField of defaultRelationObjectFields) {
if (!objectConfigFieldNames.includes(defaultRelationField.name)) {
objectFieldsWithDefaults.push(defaultRelationField);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remark: We should allow the user to configure an object without either any default relation field or default field
For example will be required in order to declare twenty-standard as an sdk app


return { objectFields: objectFieldsWithDefaults, fields: reverseFields };
};
Loading
Loading