# 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
66 lines
2.7 KiB
TypeScript
66 lines
2.7 KiB
TypeScript
import { type MessageDescriptor } from '@lingui/core';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { assertUnreachable } from 'twenty-shared/utils';
|
|
|
|
import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant';
|
|
import { CustomException } from 'src/utils/custom-exception';
|
|
|
|
export enum ObjectMetadataExceptionCode {
|
|
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
|
INVALID_OBJECT_INPUT = 'INVALID_OBJECT_INPUT',
|
|
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
|
|
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
|
|
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
|
|
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
|
|
INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT',
|
|
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
|
NAME_CONFLICT = 'NAME_CONFLICT',
|
|
MISSING_SYSTEM_FIELD = 'MISSING_SYSTEM_FIELD',
|
|
INVALID_SYSTEM_FIELD = 'INVALID_SYSTEM_FIELD',
|
|
}
|
|
|
|
const getObjectMetadataExceptionUserFriendlyMessage = (
|
|
code: ObjectMetadataExceptionCode,
|
|
) => {
|
|
switch (code) {
|
|
case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
|
return msg`Object not found.`;
|
|
case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT:
|
|
return msg`Invalid object input.`;
|
|
case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED:
|
|
return msg`This object cannot be modified.`;
|
|
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
|
|
return msg`An object with this name already exists.`;
|
|
case ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND:
|
|
return msg`Application not found.`;
|
|
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
|
return msg`Custom object is missing a label identifier field.`;
|
|
case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT:
|
|
return msg`Invalid data format.`;
|
|
case ObjectMetadataExceptionCode.INTERNAL_SERVER_ERROR:
|
|
return STANDARD_ERROR_MESSAGE;
|
|
case ObjectMetadataExceptionCode.NAME_CONFLICT:
|
|
return msg`A name conflict occurred.`;
|
|
case ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD:
|
|
return msg`A system field is missing.`;
|
|
case ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD:
|
|
return msg`A system field has invalid properties.`;
|
|
default:
|
|
assertUnreachable(code);
|
|
}
|
|
};
|
|
|
|
export class ObjectMetadataException extends CustomException<ObjectMetadataExceptionCode> {
|
|
constructor(
|
|
message: string,
|
|
code: ObjectMetadataExceptionCode,
|
|
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
|
) {
|
|
super(message, code, {
|
|
userFriendlyMessage:
|
|
userFriendlyMessage ??
|
|
getObjectMetadataExceptionUserFriendlyMessage(code),
|
|
});
|
|
}
|
|
}
|