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
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
|
||||
|
||||
export type FindManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsArgs<
|
||||
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
|
||||
> = {
|
||||
flatEntityMaps: UniversalFlatEntityMaps<T>;
|
||||
universalIdentifiers: string[];
|
||||
};
|
||||
|
||||
export const findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps =
|
||||
<T extends SyncableFlatEntity | UniversalSyncableFlatEntity>({
|
||||
flatEntityMaps,
|
||||
universalIdentifiers,
|
||||
}: FindManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsArgs<T>): T[] =>
|
||||
universalIdentifiers
|
||||
.map((universalIdentifier) =>
|
||||
findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps,
|
||||
universalIdentifier,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
+1
-1
@@ -7,5 +7,5 @@ export const FLAT_FIELD_METADATA_RELATION_PROPERTIES_TO_COMPARE = [
|
||||
'standardOverrides',
|
||||
'icon',
|
||||
'name',
|
||||
'universalSettings', // TODO prastoin VERY IMPORTANT
|
||||
'universalSettings',
|
||||
] as const satisfies (typeof ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY.fieldMetadata.propertiesToCompare)[number][];
|
||||
|
||||
+4
-10
@@ -14,6 +14,8 @@ import { validateEnumSelectFlatFieldMetadata } from 'src/engine/metadata-modules
|
||||
import { validateFilesFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-files-flat-field-metadata.util';
|
||||
import { validateMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-or-relation-flat-field-metadata.util';
|
||||
import { validateMorphRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-morph-relation-flat-field-metadata.util';
|
||||
import { validatePositionFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-position-flat-field-metadata.util';
|
||||
import { validateTsVectorFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-ts-vector-flat-field-metadata.util';
|
||||
|
||||
const DEFAULT_NO_VALIDATION = (): FlatFieldMetadataValidationError[] => [];
|
||||
|
||||
@@ -67,20 +69,12 @@ export class FlatFieldMetadataTypeValidatorService {
|
||||
msg`Field type NUMERIC is not supported. Use Number instead.`,
|
||||
),
|
||||
PHONES: DEFAULT_NO_VALIDATION,
|
||||
POSITION: rejectUserCreation(
|
||||
FieldMetadataType.POSITION,
|
||||
'Field type POSITION is a system type and cannot be created manually.',
|
||||
msg`Field type POSITION is a system type and cannot be created manually.`,
|
||||
),
|
||||
POSITION: validatePositionFlatFieldMetadata,
|
||||
RAW_JSON: DEFAULT_NO_VALIDATION,
|
||||
RICH_TEXT: DEFAULT_NO_VALIDATION,
|
||||
RICH_TEXT_V2: DEFAULT_NO_VALIDATION,
|
||||
TEXT: DEFAULT_NO_VALIDATION,
|
||||
TS_VECTOR: rejectUserCreation(
|
||||
FieldMetadataType.TS_VECTOR,
|
||||
'Field type TS_VECTOR is a system type and cannot be created manually.',
|
||||
msg`Field type TS_VECTOR is a system type and cannot be created manually.`,
|
||||
),
|
||||
TS_VECTOR: validateTsVectorFlatFieldMetadata,
|
||||
UUID: DEFAULT_NO_VALIDATION,
|
||||
MORPH_RELATION: validateMorphRelationFlatFieldMetadata,
|
||||
MULTI_SELECT: validateEnumSelectFlatFieldMetadata,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { RelationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps.util';
|
||||
import { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
@@ -21,7 +21,7 @@ export const getObjectFieldNamesAndJoinColumnNames = ({
|
||||
objectFieldNamesAndJoinColumnNames: ObjectFieldNamesAndJoinColumnNames;
|
||||
} => {
|
||||
const objectUniversalFlatFieldMetadatas =
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps({
|
||||
flatEntityMaps: universalFlatFieldMetadataMaps,
|
||||
universalIdentifiers:
|
||||
universalFlatObjectMetadata.fieldUniversalIdentifiers,
|
||||
|
||||
+5
-3
@@ -1,20 +1,22 @@
|
||||
import { computeMetadataNameFromLabel } from 'twenty-shared/metadata';
|
||||
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const isFlatFieldMetadataNameSyncedWithLabel = ({
|
||||
flatFieldMetadata,
|
||||
isSystemBuild,
|
||||
buildOptions,
|
||||
}: {
|
||||
flatFieldMetadata: Pick<
|
||||
FlatFieldMetadata,
|
||||
'name' | 'isLabelSyncedWithName' | 'label'
|
||||
>;
|
||||
isSystemBuild: boolean;
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
}) => {
|
||||
const computedName = computeMetadataNameFromLabel({
|
||||
label: flatFieldMetadata.label,
|
||||
applyCustomSuffix: !isSystemBuild,
|
||||
applyCustomSuffix: !isCallerTwentyStandardApp(buildOptions),
|
||||
});
|
||||
|
||||
return flatFieldMetadata.name === computedName;
|
||||
|
||||
+4
-3
@@ -5,9 +5,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { getObjectFieldNamesAndJoinColumnNames } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util';
|
||||
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
|
||||
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
@@ -51,7 +52,7 @@ export const validateFlatFieldMetadataNameAvailability = ({
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
const objectUniversalFlatFieldMetadatas =
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMaps({
|
||||
flatEntityMaps: universalFlatFieldMetadataMaps,
|
||||
universalIdentifiers:
|
||||
universalFlatObjectMetadata.fieldUniversalIdentifiers,
|
||||
@@ -61,7 +62,7 @@ export const validateFlatFieldMetadataNameAvailability = ({
|
||||
);
|
||||
|
||||
if (
|
||||
!buildOptions.isSystemBuild &&
|
||||
!isCallerTwentyStandardApp(buildOptions) &&
|
||||
reservedCompositeFieldsNames.includes(name)
|
||||
) {
|
||||
errors.push({
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-me
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants';
|
||||
import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants';
|
||||
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
|
||||
@@ -51,7 +52,7 @@ export const validateFlatFieldMetadataName = ({
|
||||
}
|
||||
|
||||
if (
|
||||
!buildOptions.isSystemBuild &&
|
||||
!isCallerTwentyStandardApp(buildOptions) &&
|
||||
RESERVED_METADATA_NAME_KEYWORDS.includes(name)
|
||||
) {
|
||||
errors.push({
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
|
||||
export const validatePositionFlatFieldMetadata = ({
|
||||
flatEntityToValidate,
|
||||
}: FlatFieldMetadataTypeValidationArgs<FieldMetadataType.POSITION>): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (flatEntityToValidate.name !== 'position') {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Field type POSITION must be named "position", got "${flatEntityToValidate.name}"`,
|
||||
value: flatEntityToValidate.name,
|
||||
userFriendlyMessage: msg`Field type POSITION must be named "position"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!flatEntityToValidate.isSystem) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'Field type POSITION must be a system field',
|
||||
value: flatEntityToValidate.isSystem,
|
||||
userFriendlyMessage: msg`Field type POSITION must be a system field`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
|
||||
export const validateTsVectorFlatFieldMetadata = ({
|
||||
flatEntityToValidate,
|
||||
}: FlatFieldMetadataTypeValidationArgs<FieldMetadataType.TS_VECTOR>): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (flatEntityToValidate.name !== 'searchVector') {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Field type TS_VECTOR must be named "searchVector", got "${flatEntityToValidate.name}"`,
|
||||
value: flatEntityToValidate.name,
|
||||
userFriendlyMessage: msg`Field type TS_VECTOR must be named "searchVector"`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!flatEntityToValidate.isSystem) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: 'Field type TS_VECTOR must be a system field',
|
||||
value: flatEntityToValidate.isSystem,
|
||||
userFriendlyMessage: msg`Field type TS_VECTOR must be a system field`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
+5
-3
@@ -1,12 +1,14 @@
|
||||
import { computeMetadataNameFromLabel } from 'twenty-shared/metadata';
|
||||
|
||||
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const areFlatObjectMetadataNamesSyncedWithLabels = ({
|
||||
flatObjectdMetadata,
|
||||
isSystemBuild,
|
||||
buildOptions,
|
||||
}: {
|
||||
isSystemBuild: boolean;
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
flatObjectdMetadata: Pick<
|
||||
UniversalFlatObjectMetadata,
|
||||
'namePlural' | 'nameSingular' | 'labelPlural' | 'labelSingular'
|
||||
@@ -18,7 +20,7 @@ export const areFlatObjectMetadataNamesSyncedWithLabels = ({
|
||||
].map((label) =>
|
||||
computeMetadataNameFromLabel({
|
||||
label,
|
||||
applyCustomSuffix: !isSystemBuild,
|
||||
applyCustomSuffix: !isCallerTwentyStandardApp(buildOptions),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
const nameField = defaultFlatFieldForCustomObjectMaps.fields.nameField;
|
||||
const labelIdentifierFieldMetadataUniversalIdentifier =
|
||||
nameField?.universalIdentifier ??
|
||||
defaultFlatFieldForCustomObjectMaps.fields.idField.universalIdentifier;
|
||||
defaultFlatFieldForCustomObjectMaps.fields.id.universalIdentifier;
|
||||
|
||||
const universalFlatObjectMetadataToCreate: UniversalFlatObjectMetadata & {
|
||||
id: string;
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export const validateFlatObjectMetadataNameAndLabels = ({
|
||||
universalFlatObjectMetadataToValidate.isLabelSyncedWithName &&
|
||||
!areFlatObjectMetadataNamesSyncedWithLabels({
|
||||
flatObjectdMetadata: universalFlatObjectMetadataToValidate,
|
||||
isSystemBuild: buildOptions.isSystemBuild,
|
||||
buildOptions,
|
||||
})
|
||||
) {
|
||||
errors.push({
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-universal-identifier-in-universal-flat-entity-maps-or-throw.util';
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import {
|
||||
type OrchestratorActionsReport,
|
||||
type OrchestratorFailureReport,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
|
||||
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
|
||||
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
|
||||
import { buildUniversalFlatObjectFieldByNameAndJoinColumnMaps } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/build-universal-flat-object-field-by-name-and-join-column-maps.util';
|
||||
|
||||
type ValidateObjectMetadataSystemFieldsIntegrityArgs = {
|
||||
orchestratorActionsReport: Pick<
|
||||
OrchestratorActionsReport,
|
||||
'fieldMetadata' | 'objectMetadata'
|
||||
>;
|
||||
optimisticUniversalFlatMaps: Pick<
|
||||
AllUniversalFlatEntityMaps,
|
||||
'flatFieldMetadataMaps' | 'flatObjectMetadataMaps'
|
||||
>;
|
||||
};
|
||||
export const validateObjectMetadataSystemFieldsIntegrity = ({
|
||||
optimisticUniversalFlatMaps,
|
||||
orchestratorActionsReport,
|
||||
}: ValidateObjectMetadataSystemFieldsIntegrityArgs): Pick<
|
||||
OrchestratorFailureReport,
|
||||
'objectMetadata'
|
||||
> => {
|
||||
const metadataValidationErrors: Pick<
|
||||
OrchestratorFailureReport,
|
||||
'objectMetadata'
|
||||
> = {
|
||||
objectMetadata: [],
|
||||
};
|
||||
|
||||
const createdObjectMetadatas =
|
||||
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({
|
||||
universalIdentifiers: orchestratorActionsReport.objectMetadata.create.map(
|
||||
(createObjectAction) =>
|
||||
createObjectAction.flatEntity.universalIdentifier,
|
||||
),
|
||||
flatEntityMaps: optimisticUniversalFlatMaps.flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
for (const createdObjectMetadata of createdObjectMetadatas) {
|
||||
const createdFailedFlatEntityValidations: FailedFlatEntityValidation<
|
||||
'objectMetadata',
|
||||
'create'
|
||||
> = getEmptyFlatEntityValidationError({
|
||||
flatEntityMinimalInformation: {
|
||||
universalIdentifier: createdObjectMetadata.universalIdentifier,
|
||||
namePlural: createdObjectMetadata.namePlural,
|
||||
nameSingular: createdObjectMetadata.nameSingular,
|
||||
},
|
||||
metadataName: 'objectMetadata',
|
||||
type: 'create',
|
||||
});
|
||||
|
||||
const { fieldUniversalIdentifierByName } =
|
||||
buildUniversalFlatObjectFieldByNameAndJoinColumnMaps({
|
||||
flatFieldMetadataMaps:
|
||||
optimisticUniversalFlatMaps.flatFieldMetadataMaps,
|
||||
flatObjectMetadata: createdObjectMetadata,
|
||||
});
|
||||
|
||||
for (const expectedSystemField of Object.values(
|
||||
PARTIAL_SYSTEM_FLAT_FIELD_METADATAS,
|
||||
)) {
|
||||
const matchingFieldUniversalIdentifier =
|
||||
fieldUniversalIdentifierByName[expectedSystemField.name];
|
||||
|
||||
const expectedFieldName = expectedSystemField.name;
|
||||
|
||||
if (!isDefined(matchingFieldUniversalIdentifier)) {
|
||||
createdFailedFlatEntityValidations.errors.push({
|
||||
code: ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD,
|
||||
message: `System field ${expectedFieldName} is missing`,
|
||||
userFriendlyMessage: msg`System field ${expectedFieldName} is missing`,
|
||||
value: expectedFieldName,
|
||||
});
|
||||
} else {
|
||||
const universalFlatFieldMetadata =
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps: optimisticUniversalFlatMaps.flatFieldMetadataMaps,
|
||||
universalIdentifier: matchingFieldUniversalIdentifier,
|
||||
});
|
||||
|
||||
const propertiesToValidate = [
|
||||
'type',
|
||||
'isSystem',
|
||||
] as const satisfies (keyof UniversalFlatFieldMetadata)[];
|
||||
|
||||
for (const property of propertiesToValidate) {
|
||||
const expectedValue = expectedSystemField[property];
|
||||
const actualValue = universalFlatFieldMetadata[property];
|
||||
|
||||
if (actualValue !== expectedValue) {
|
||||
createdFailedFlatEntityValidations.errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD,
|
||||
message: `System field ${expectedFieldName} has invalid ${property}: expected ${String(expectedValue)}, got ${String(actualValue)}`,
|
||||
userFriendlyMessage: msg`System field ${expectedFieldName} has invalid ${property}`,
|
||||
value: actualValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (createdFailedFlatEntityValidations.errors.length > 0) {
|
||||
metadataValidationErrors.objectMetadata.push(
|
||||
createdFailedFlatEntityValidations,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return metadataValidationErrors;
|
||||
};
|
||||
+2
-2
@@ -4,6 +4,7 @@ import { type GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs } from 's
|
||||
import { type FlatPageLayoutWidgetValidationError } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-validation-error.type';
|
||||
import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum';
|
||||
import { PageLayoutWidgetExceptionCode } from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
|
||||
|
||||
export const rejectWidgetType = (
|
||||
widgetType: WidgetType,
|
||||
@@ -13,8 +14,7 @@ export const rejectWidgetType = (
|
||||
return (
|
||||
args: GenericValidateFlatPageLayoutWidgetTypeSpecificitiesArgs,
|
||||
): FlatPageLayoutWidgetValidationError[] => {
|
||||
// Allow standard widgets to use widget types that are not yet supported for user creation
|
||||
if (args.buildOptions.isSystemBuild) {
|
||||
if (isCallerTwentyStandardApp(args.buildOptions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export class NavigationMenuItemDeletionService {
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
|
||||
type PartialSystemFlatFieldMetadata<
|
||||
T extends FieldMetadataType = FieldMetadataType,
|
||||
> = Omit<
|
||||
UniversalFlatFieldMetadata<T>,
|
||||
| 'applicationUniversalIdentifier'
|
||||
| 'universalIdentifier'
|
||||
| 'objectMetadataUniversalIdentifier'
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
>;
|
||||
|
||||
const PARTIAL_ID_FIELD = {
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
icon: 'Icon123',
|
||||
description: 'Id',
|
||||
isNullable: false,
|
||||
isUnique: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: 'uuid',
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.UUID>;
|
||||
|
||||
const PARTIAL_CREATED_AT_FIELD = {
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
description: 'Creation date',
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: 'now',
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
|
||||
|
||||
const PARTIAL_UPDATED_AT_FIELD = {
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
icon: 'IconCalendarClock',
|
||||
description: 'Last time the record was changed',
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: 'now',
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
|
||||
|
||||
const PARTIAL_DELETED_AT_FIELD = {
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
icon: 'IconCalendarClock',
|
||||
description: 'Deletion date',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.DATE_TIME>;
|
||||
|
||||
const PARTIAL_CREATED_BY_FIELD = {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
description: 'The creator of the record',
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
|
||||
|
||||
const PARTIAL_UPDATED_BY_FIELD = {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
icon: 'IconUserCircle',
|
||||
description: 'The workspace member who last updated the record',
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.ACTOR>;
|
||||
|
||||
const PARTIAL_POSITION_FIELD = {
|
||||
type: FieldMetadataType.POSITION,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
description: 'Position',
|
||||
isNullable: false,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: 0,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.POSITION>;
|
||||
|
||||
const PARTIAL_SEARCH_VECTOR_FIELD = {
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
description: 'Search vector',
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
isLabelSyncedWithName: false,
|
||||
defaultValue: null,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
// universalSettings for searchVector is computed at runtime
|
||||
// based on the name field (getTsVectorColumnExpressionFromFields)
|
||||
universalSettings: null,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
} as const satisfies PartialSystemFlatFieldMetadata<FieldMetadataType.TS_VECTOR>;
|
||||
|
||||
export const PARTIAL_SYSTEM_FLAT_FIELD_METADATAS = {
|
||||
id: PARTIAL_ID_FIELD,
|
||||
createdAt: PARTIAL_CREATED_AT_FIELD,
|
||||
updatedAt: PARTIAL_UPDATED_AT_FIELD,
|
||||
deletedAt: PARTIAL_DELETED_AT_FIELD,
|
||||
createdBy: PARTIAL_CREATED_BY_FIELD,
|
||||
updatedBy: PARTIAL_UPDATED_BY_FIELD,
|
||||
position: PARTIAL_POSITION_FIELD,
|
||||
searchVector: PARTIAL_SEARCH_VECTOR_FIELD,
|
||||
} as const satisfies Record<string, PartialSystemFlatFieldMetadata>;
|
||||
+6
@@ -15,6 +15,8 @@ export enum ObjectMetadataExceptionCode {
|
||||
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 = (
|
||||
@@ -39,6 +41,10 @@ const getObjectMetadataExceptionUserFriendlyMessage = (
|
||||
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);
|
||||
}
|
||||
|
||||
-28
@@ -931,34 +931,6 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
}, authContext);
|
||||
}
|
||||
|
||||
public async deleteWorkspaceAllObjectMetadata({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const deleteObjectInputs = Object.keys(
|
||||
flatObjectMetadataMaps.universalIdentifierById,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.map<DeleteOneObjectInput>((id) => ({
|
||||
id,
|
||||
}));
|
||||
|
||||
await this.deleteManyObjectMetadatas({
|
||||
deleteObjectInputs,
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
}
|
||||
|
||||
public async findOneWithinWorkspace(
|
||||
workspaceId: string,
|
||||
options: FindOneOptions<ObjectMetadataEntity>,
|
||||
|
||||
+108
-279
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
type FieldMetadataSettingsMapping,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
|
||||
import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/utils/get-ts-vector-column-expression.util';
|
||||
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
|
||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||
@@ -19,6 +17,98 @@ type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
|
||||
export type DefaultFlatFieldForCustomObjectMaps = ReturnType<
|
||||
typeof buildDefaultFlatFieldMetadatasForCustomObject
|
||||
>;
|
||||
|
||||
const buildObjectSystemFlatFieldMetadatas = ({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
now,
|
||||
searchVectorUniversalSettings,
|
||||
}: {
|
||||
applicationUniversalIdentifier: string;
|
||||
objectMetadataUniversalIdentifier: string;
|
||||
now: string;
|
||||
searchVectorUniversalSettings: UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR>['universalSettings'];
|
||||
}) => {
|
||||
const {
|
||||
createdAt,
|
||||
createdBy,
|
||||
deletedAt,
|
||||
id,
|
||||
position,
|
||||
searchVector,
|
||||
updatedAt,
|
||||
updatedBy,
|
||||
} = PARTIAL_SYSTEM_FLAT_FIELD_METADATAS;
|
||||
|
||||
return {
|
||||
id: {
|
||||
...id,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
createdAt: {
|
||||
...createdAt,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
createdBy: {
|
||||
...createdBy,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
deletedAt: {
|
||||
...deletedAt,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
position: {
|
||||
...position,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
searchVector: {
|
||||
...searchVector,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
universalSettings: searchVectorUniversalSettings,
|
||||
},
|
||||
updatedAt: {
|
||||
...updatedAt,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
updatedBy: {
|
||||
...updatedBy,
|
||||
universalIdentifier: v4(),
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
} as const satisfies Record<string, UniversalFlatFieldMetadata>;
|
||||
};
|
||||
|
||||
// This could be replaced totally by an import schema + its transpilation when it's ready
|
||||
export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
flatObjectMetadata: {
|
||||
@@ -27,39 +117,7 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
},
|
||||
skipNameField = false,
|
||||
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
const idField: UniversalFlatFieldMetadata<FieldMetadataType.UUID> = {
|
||||
type: FieldMetadataType.UUID,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: true,
|
||||
universalIdentifier: v4(),
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
icon: 'Icon123',
|
||||
description: 'Id',
|
||||
isNullable: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 'uuid',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const nameField: UniversalFlatFieldMetadata<FieldMetadataType.TEXT> | null =
|
||||
skipNameField
|
||||
@@ -79,8 +137,8 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
isSystem: false,
|
||||
isUIReadOnly: false,
|
||||
defaultValue: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
@@ -96,252 +154,23 @@ export const buildDefaultFlatFieldMetadatasForCustomObject = ({
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const createdAtField: UniversalFlatFieldMetadata<FieldMetadataType.DATE_TIME> =
|
||||
const searchVectorUniversalSettings: UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR>['universalSettings'] =
|
||||
{
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
icon: 'IconCalendar',
|
||||
description: 'Creation date',
|
||||
isNullable: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 'now',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const updatedAtField: UniversalFlatFieldMetadata<FieldMetadataType.DATE_TIME> =
|
||||
{
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'updatedAt',
|
||||
label: 'Last update',
|
||||
icon: 'IconCalendarClock',
|
||||
description: 'Last time the record was changed',
|
||||
isNullable: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 'now',
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const deletedAtField: UniversalFlatFieldMetadata<FieldMetadataType.DATE_TIME> =
|
||||
{
|
||||
type: FieldMetadataType.DATE_TIME,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'deletedAt',
|
||||
label: 'Deleted at',
|
||||
icon: 'IconCalendarClock',
|
||||
description: 'Deletion date',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const createdByField: UniversalFlatFieldMetadata<FieldMetadataType.ACTOR> = {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'createdBy',
|
||||
label: 'Created by',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
description: 'The creator of the record',
|
||||
isNullable: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const updatedByField: UniversalFlatFieldMetadata<FieldMetadataType.ACTOR> = {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'updatedBy',
|
||||
label: 'Updated by',
|
||||
icon: 'IconUserCircle',
|
||||
description: 'The workspace member who last updated the record',
|
||||
isNullable: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: false,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: { name: "''", source: "'MANUAL'" },
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const positionField: UniversalFlatFieldMetadata<FieldMetadataType.POSITION> =
|
||||
{
|
||||
type: FieldMetadataType.POSITION,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
icon: 'IconHierarchy2',
|
||||
description: 'Position',
|
||||
isNullable: false,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: 0,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: null,
|
||||
};
|
||||
|
||||
const searchVectorSettings: FieldMetadataSettingsMapping['TS_VECTOR'] = {
|
||||
asExpression: getTsVectorColumnExpressionFromFields(
|
||||
nameField ? [nameField] : [],
|
||||
),
|
||||
generatedType: 'STORED',
|
||||
};
|
||||
const searchVectorField: UniversalFlatFieldMetadata<FieldMetadataType.TS_VECTOR> =
|
||||
{
|
||||
type: FieldMetadataType.TS_VECTOR,
|
||||
isLabelSyncedWithName: false,
|
||||
isUnique: false,
|
||||
universalIdentifier: v4(),
|
||||
name: 'searchVector',
|
||||
label: 'Search vector',
|
||||
icon: 'IconSearch',
|
||||
description: 'Search vector',
|
||||
isNullable: true,
|
||||
isActive: true,
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
isUIReadOnly: true,
|
||||
defaultValue: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
options: null,
|
||||
standardOverrides: null,
|
||||
morphId: null,
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
relationTargetObjectMetadataUniversalIdentifier: null,
|
||||
relationTargetFieldMetadataUniversalIdentifier: null,
|
||||
viewFilterUniversalIdentifiers: [],
|
||||
viewFieldUniversalIdentifiers: [],
|
||||
kanbanAggregateOperationViewUniversalIdentifiers: [],
|
||||
calendarViewUniversalIdentifiers: [],
|
||||
mainGroupByFieldMetadataViewUniversalIdentifiers: [],
|
||||
universalSettings: searchVectorSettings,
|
||||
asExpression: getTsVectorColumnExpressionFromFields(
|
||||
nameField ? [nameField] : [],
|
||||
),
|
||||
generatedType: 'STORED',
|
||||
};
|
||||
|
||||
return {
|
||||
fields: {
|
||||
idField,
|
||||
...(nameField && { nameField }),
|
||||
createdAtField,
|
||||
updatedAtField,
|
||||
updatedByField,
|
||||
deletedAtField,
|
||||
createdByField,
|
||||
positionField,
|
||||
searchVectorField,
|
||||
...buildObjectSystemFlatFieldMetadatas({
|
||||
applicationUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
now,
|
||||
searchVectorUniversalSettings,
|
||||
}),
|
||||
},
|
||||
} as const satisfies {
|
||||
fields: Record<string, UniversalFlatFieldMetadata>;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const buildDefaultIndexesForCustomObject = ({
|
||||
{
|
||||
createdAt: createdAt.toISOString(),
|
||||
fieldMetadataUniversalIdentifier:
|
||||
defaultFlatFieldForCustomObjectMaps.fields.searchVectorField
|
||||
defaultFlatFieldForCustomObjectMaps.fields.searchVector
|
||||
.universalIdentifier,
|
||||
indexMetadataUniversalIdentifier:
|
||||
tsFlatVectorIndexUniversalIdentifier,
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ export const buildDefaultRelationFlatFieldMetadatasForCustomObject = ({
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: targetFlatObjectMetadata.namePlural,
|
||||
label: capitalize(targetFlatObjectMetadata.labelPlural),
|
||||
isSystem: true,
|
||||
isSystem: false,
|
||||
relationCreationPayload: {
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
targetObjectMetadataId: targetFlatObjectMetadata.id,
|
||||
|
||||
+3
@@ -45,6 +45,9 @@ export const objectMetadataGraphqlApiExceptionHandler = (
|
||||
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
||||
case ObjectMetadataExceptionCode.APPLICATION_NOT_FOUND:
|
||||
throw error;
|
||||
case ObjectMetadataExceptionCode.MISSING_SYSTEM_FIELD:
|
||||
case ObjectMetadataExceptionCode.INVALID_SYSTEM_FIELD:
|
||||
throw new UserInputError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const isCallerTwentyStandardApp = (
|
||||
buildOptions: WorkspaceMigrationBuilderOptions,
|
||||
) =>
|
||||
buildOptions.applicationUniversalIdentifier ===
|
||||
TWENTY_STANDARD_APPLICATION.universalIdentifier;
|
||||
Reference in New Issue
Block a user