Files
twenty/packages/twenty-server/test/integration/metadata/suites/application/failing-sync-application-object-system-fields.integration-spec.ts
T
Paul Rastoin 88424611ec Refactor and standardize isSystem field and object (#17992)
# Introduction

## Centralize system field definitions
- Extract a single `PARTIAL_SYSTEM_FLAT_FIELD_METADATAS` constant as the
source of truth for all 8 system fields (`id`, `createdAt`, `updatedAt`,
`deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`),
eliminating duplication across custom object and standard app field
builders
- Refactor `buildDefaultFlatFieldMetadatasForCustomObject` to use the
shared constant via a new `buildObjectSystemFlatFieldMetadatas` helper

## Mark system fields as `isSystem: true`
- Fields `id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`,
`updatedBy`, `position`, `searchVector` are now properly flagged as
system fields across all standard objects and custom object creation
- Standard app field builders for all ~30 standard objects updated to
set `isSystem: true` on `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`
- System-only standard objects (blocklist, calendar channels, message
threads, etc.) now also include `createdBy`, `updatedBy`, `position`,
`searchVector` field definitions that were previously missing

## Validate system fields on object creation
- New transversal validation (`crossEntityTransversalValidation`) runs
after all atomic entity validations in the build orchestrator, ensuring
all 8 system fields are present with correct `type` and `isSystem: true`
when an object is created
- New `buildUniversalFlatObjectFieldByNameAndJoinColumnMaps` utility to
resolve field names to universal identifiers for a given object
- New exception codes: `MISSING_SYSTEM_FIELD` and `INVALID_SYSTEM_FIELD`
on `ObjectMetadataExceptionCode`

## Protect system fields and objects from mutation
- Field validators now block update/delete of `isSystem` fields by
non-system callers (`FIELD_MUTATION_NOT_ALLOWED`)
- Object validators now block update/delete of `isSystem` objects by
non-system callers
- `POSITION` and `TS_VECTOR` field type validators replaced: instead of
rejecting creation outright, they now validate that the field is named
correctly (`position` / `searchVector`) and has `isSystem: true`

## Distinguish `isSystemBuild` from `isCallerTwentyStandardApp`
- New `isCallerTwentyStandardApp` utility checks whether the caller's
`applicationUniversalIdentifier` matches the twenty standard app
- Name-sync logic (`isFlatFieldMetadataNameSyncedWithLabel`,
`areFlatObjectMetadataNamesSyncedWithLabels`) refactored to use
`isCallerTwentyStandardApp` for custom suffix decisions, keeping
`isSystemBuild` for mutation permission checks
- `WorkspaceMigrationBuilderOptions` type updated to include
`applicationUniversalIdentifier`

## Adapt frontend filtering
- New `HIDDEN_SYSTEM_FIELD_NAMES` constant (`id`, `position`,
`searchVector`) and `isHiddenSystemField` utility to only hide truly
internal fields while keeping user-facing system fields (`createdAt`,
`updatedAt`, `deletedAt`, `createdBy`, `updatedBy`) visible in the UI
- ~20 frontend files updated to replace `!field.isSystem` checks with
`!isHiddenSystemField(field)` across record index, settings, data model,
charts, workflows, spreadsheet import, aggregations, and role
permissions

## Add 1.19 upgrade commands
- **`backfill-system-fields-is-system`**: Raw SQL command to set
`isSystem = true` on existing workspace fields matching system field
names, and fix `position` field type from `NUMBER` to `POSITION` for
`favorite`/`favoriteFolder` objects. Includes proper cache invalidation.
- **`add-missing-system-fields-to-standard-objects`**: Codegen'd
workspace migration to create missing `position`, `searchVector`,
`createdBy`, `updatedBy` fields on standard objects that didn't
previously have them. Runs via `WorkspaceMigrationRunnerService` in a
single transaction with idempotency check. **Known limitation**: assumes
all standard objects exist and are valid in the target workspace.

## Add `universalIdentifier` for system fields in standard object
constants
- `standard-object.constant.ts` updated to include `universalIdentifier`
for `createdBy`, `updatedBy`, `position`, and `searchVector` across all
standard objects
- `fieldManifestType.ts` updated to support the new field manifest shape

## System relation
Completely removed and backfilled all `isSystem` relation to be false
false
As we won't require an object to have any relation system fields

## Add integration tests
- New test suite `failing-sync-application-object-system-fields`
covering: missing system fields, wrong field types (`id` as TEXT,
`createdAt` as TEXT, `position` as TEXT), system field deletion
attempts, and system field update attempts
- New test utilities: `buildDefaultObjectManifest` (builds an object
manifest with all 8 system fields) and `setupApplicationForSync`
(centralizes application setup)
- Existing successful sync test updated to verify system fields are
created with correct properties

## Next step
Make the builder scope the compared entity to be the currently built app
+ nor twenty standard app
2026-02-19 10:13:50 +00:00

311 lines
8.9 KiB
TypeScript

import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { buildDefaultObjectManifest } from 'test/integration/metadata/suites/application/utils/build-default-object-manifest.util';
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
import { uninstallApplication } from 'test/integration/metadata/suites/application/utils/uninstall-application.util';
import { type Manifest, type ObjectManifest } from 'twenty-shared/application';
import {
type EachTestingContext,
eachTestingContextFilter,
} from 'twenty-shared/testing';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
type TestContext = {
manifest: Manifest;
};
type SyncApplicationTestingContext = EachTestingContext<TestContext>[];
const buildBaseManifest = (
overrides: Pick<Manifest, 'objects' | 'fields'>,
): Manifest => ({
application: {
apiClientChecksum: '',
marketplaceData: undefined,
universalIdentifier: TEST_APP_ID,
defaultRoleUniversalIdentifier: TEST_ROLE_ID,
displayName: 'Test System Fields App',
description: 'App for testing system field validation',
icon: 'IconTestPipe',
applicationVariables: {},
packageJsonChecksum: null,
yarnLockChecksum: null,
},
roles: [
{
universalIdentifier: TEST_ROLE_ID,
label: 'Test Role',
description: 'A test role',
},
],
logicFunctions: [],
frontComponents: [],
publicAssets: [],
views: [],
navigationMenuItems: [],
pageLayouts: [],
...overrides,
});
const buildObjectWithLabelField = ({
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
additionalFields = [],
}: {
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description: string;
additionalFields?: ObjectManifest['fields'];
}): Pick<Manifest, 'objects' | 'fields'> => {
const objectId = uuidv4();
const labelFieldId = uuidv4();
return {
objects: [
{
universalIdentifier: objectId,
labelIdentifierFieldMetadataUniversalIdentifier: labelFieldId,
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon: 'IconTicket',
fields: [
{
universalIdentifier: labelFieldId,
type: FieldMetadataType.TEXT,
name: 'title',
label: 'Title',
description: 'Label identifier field',
icon: 'IconTextCaption',
},
...additionalFields,
],
},
],
fields: [],
};
};
const failingSyncApplicationSystemFieldsTestCases: SyncApplicationTestingContext =
[
{
title:
'when object is created without any system fields (missing all 8 system fields)',
context: {
manifest: buildBaseManifest(
buildObjectWithLabelField({
nameSingular: 'noSystemFieldsObject',
namePlural: 'noSystemFieldsObjects',
labelSingular: 'No System Fields Object',
labelPlural: 'No System Fields Objects',
description: 'Object with no system fields',
}),
),
},
},
{
title: 'when object has id field with wrong type (TEXT instead of UUID)',
context: {
manifest: buildBaseManifest(
buildObjectWithLabelField({
nameSingular: 'wrongIdTypeObject',
namePlural: 'wrongIdTypeObjects',
labelSingular: 'Wrong Id Type Object',
labelPlural: 'Wrong Id Type Objects',
description: 'Object with wrong id field type',
additionalFields: [
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.TEXT,
name: 'id',
label: 'Id',
description: 'Id field with wrong type',
icon: 'IconKey',
},
],
}),
),
},
},
{
title:
'when object has createdAt field with wrong type (TEXT instead of DATE_TIME)',
context: {
manifest: buildBaseManifest(
buildObjectWithLabelField({
nameSingular: 'wrongCreatedAtObject',
namePlural: 'wrongCreatedAtObjects',
labelSingular: 'Wrong CreatedAt Object',
labelPlural: 'Wrong CreatedAt Objects',
description: 'Object with wrong createdAt field type',
additionalFields: [
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.TEXT,
name: 'createdAt',
label: 'Created At',
description: 'Created at field with wrong type',
icon: 'IconCalendar',
},
],
}),
),
},
},
{
title:
'when object has position field with wrong type (TEXT instead of POSITION)',
context: {
manifest: buildBaseManifest(
buildObjectWithLabelField({
nameSingular: 'wrongPositionObject',
namePlural: 'wrongPositionObjects',
labelSingular: 'Wrong Position Object',
labelPlural: 'Wrong Position Objects',
description: 'Object with wrong position field type',
additionalFields: [
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.TEXT,
name: 'position',
label: 'Position',
description: 'Position field with wrong type',
icon: 'IconArrowsSort',
},
],
}),
),
},
},
];
describe('Sync application should fail due to object system fields integrity', () => {
let appCreated = false;
beforeAll(async () => {
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Test System Fields App',
description: 'App for testing system field validation',
sourcePath: 'test-system-fields',
});
appCreated = true;
}, 60000);
afterEach(async () => {
if (!appCreated) {
return;
}
await uninstallApplication({
universalIdentifier: TEST_APP_ID,
expectToFail: false,
});
});
it.each(
eachTestingContextFilter(failingSyncApplicationSystemFieldsTestCases),
)(
'$title',
async ({ context }) => {
const { errors } = await syncApplication({
manifest: context.manifest,
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({ errors });
},
60000,
);
it('should fail when trying to delete a system field after a successful sync', async () => {
const testObject = buildDefaultObjectManifest({
nameSingular: 'deleteSystemFieldObject',
namePlural: 'deleteSystemFieldObjects',
labelSingular: 'Delete System Field Object',
labelPlural: 'Delete System Field Objects',
description: 'Object for testing system field deletion',
});
const validManifest = buildBaseManifest({
objects: [testObject],
fields: [],
});
await syncApplication({
manifest: validManifest,
expectToFail: false,
});
const manifestWithDeletedIdField = buildBaseManifest({
objects: [
{
...testObject,
fields: testObject.fields.filter((field) => field.name !== 'id'),
},
],
fields: [],
});
const { errors } = await syncApplication({
manifest: manifestWithDeletedIdField,
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({ errors });
}, 60000);
it('should fail when trying to update a system field after a successful sync', async () => {
const testObject = buildDefaultObjectManifest({
nameSingular: 'updateSystemFieldObject',
namePlural: 'updateSystemFieldObjects',
labelSingular: 'Update System Field Object',
labelPlural: 'Update System Field Objects',
description: 'Object for testing system field update',
});
const validManifest = buildBaseManifest({
objects: [testObject],
fields: [],
});
await syncApplication({
manifest: validManifest,
expectToFail: false,
});
const manifestWithUpdatedIdField = buildBaseManifest({
objects: [
{
...testObject,
fields: testObject.fields.map((field) =>
field.name === 'id'
? { ...field, label: 'Modified Id Label' }
: field,
),
},
],
fields: [],
});
const { errors } = await syncApplication({
manifest: manifestWithUpdatedIdField,
expectToFail: true,
});
expectOneNotInternalServerErrorSnapshot({ errors });
}, 60000);
});