Compare commits

...
Author SHA1 Message Date
prastoin 33674052b6 basic-validation-system-field 2026-02-17 17:57:51 +01:00
prastoin 5d4c4daf62 fix 2026-02-17 17:57:51 +01:00
prastoin ee3ad6d807 fix 2026-02-17 17:57:51 +01:00
prastoin 9fbafc65b5 lint 2026-02-17 17:57:51 +01:00
prastoin 621531a2ca chore 2026-02-17 17:57:51 +01:00
prastoin 0d54ae28d6 review 2026-02-17 17:57:51 +01:00
prastoin 69d084c5da chore 2026-02-17 17:57:51 +01:00
prastoin 50fa0960a7 chore 2026-02-17 17:57:51 +01:00
prastoin 763467b5c7 refactor(server): standard field with is system 2026-02-17 17:57:51 +01:00
prastoin 5901f88b27 feat(server): wobbly refactor of the isSystem build 2026-02-17 17:57:51 +01:00
prastoin b78cab2470 refactor(server): system field and object cannot be updated 2026-02-17 17:57:50 +01:00
prastoin 4781eaf1f3 feat(server): backfill command 2026-02-17 17:57:50 +01:00
prastoin 146166a58b libnt 2026-02-17 17:57:50 +01:00
prastoin 7c02c53685 refactor(front): hidden system fields 2026-02-17 17:57:50 +01:00
prastoin 1021b1ca79 refactor 2026-02-17 17:57:50 +01:00
prastoin fdf67627f9 refactor(server): centralize constants 2026-02-17 17:57:50 +01:00
prastoin 8bb305cdba util 2026-02-17 17:57:50 +01:00
prastoin 3b12030917 test(server): fix successfull 2026-02-17 17:57:50 +01:00
prastoin 7d0f97bf62 setup app for sync 2026-02-17 17:57:50 +01:00
prastoin f9e2c40e4c lint 2026-02-17 17:57:50 +01:00
prastoin 049aed00ab lint 2026-02-17 17:57:50 +01:00
prastoin 25e3f468de test(server): system field missing 2026-02-17 17:57:50 +01:00
prastoin 011e8f134d feat(server): complete validation 2026-02-17 17:57:50 +01:00
prastoin 4b4672110b naming 2026-02-17 17:57:50 +01:00
prastoin 5bb1ed0e0a refactor(server): add system integrity validation 2026-02-17 17:57:50 +01:00
101 changed files with 2088 additions and 417 deletions
@@ -6,6 +6,7 @@ import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
import { msg } from '@lingui/core/macro';
import React from 'react';
@@ -35,7 +36,7 @@ export const useRelatedRecordActions = ({
(field) =>
field.type === 'RELATION' &&
field.relation?.type === 'ONE_TO_MANY' &&
!field.isSystem,
!isHiddenSystemField(field),
);
let currentPosition = startPosition;
@@ -3,6 +3,7 @@ import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pa
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
@@ -69,7 +70,7 @@ export const ChartFieldSelectionForAggregateOperationDropdownContent = () => {
searchQuery,
getSearchableValues: (item) => [item.label, item.name],
// TODO: remove the relation filter once group by is supported for relation fields
}).filter((field) => !isFieldRelation(field) && !field.isSystem);
}).filter((field) => !isFieldRelation(field) && !isHiddenSystemField(field));
const { getIcon } = useIcons();
@@ -7,6 +7,7 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/
import { buildChartGroupByFieldConfigUpdate } from '@/command-menu/pages/page-layout/utils/buildChartGroupByFieldConfigUpdate';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -83,7 +84,7 @@ export const ChartGroupByFieldSelectionDropdownContentBase = <
searchQuery,
getSearchableValues: (item) => [item.label, item.name],
}).filter((field) => {
if (field.isSystem === true) {
if (isHiddenSystemField(field)) {
return false;
}
if (isFieldRelation(field)) {
@@ -1,6 +1,7 @@
import { ChartGroupByFieldSelectionCompositeFieldView } from '@/command-menu/pages/page-layout/components/dropdown-content/ChartGroupByFieldSelectionCompositeFieldView';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
@@ -70,7 +71,7 @@ export const ChartGroupByFieldSelectionRelationFieldView = ({
return filterBySearchQuery({
items: targetObjectMetadataItem.fields.filter(
(field) => !field.isSystem && !isFieldRelation(field),
(field) => !isHiddenSystemField(field) && !isFieldRelation(field),
),
searchQuery,
getSearchableValues: (field) => [field.label, field.name],
@@ -0,0 +1,8 @@
// System fields that should be hidden from the UI by default.
// Other system fields (createdAt, updatedAt, deletedAt, createdBy, updatedBy)
// are isSystem: true but remain user-visible.
export const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
'id',
'searchVector',
'position',
]);
@@ -1,4 +1,5 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/constants/TableColumnsDenyList';
import { useMemo } from 'react';
@@ -16,7 +17,7 @@ export const useAvailableFieldMetadataItems = ({
objectMetadataItem.readableFields.filter((fieldMetadataItemToFilter) => {
return (
fieldMetadataItemToFilter.isActive &&
!fieldMetadataItemToFilter.isSystem &&
!isHiddenSystemField(fieldMetadataItemToFilter) &&
!TABLE_COLUMNS_DENY_LIST.includes(fieldMetadataItemToFilter.name)
);
}),
@@ -1,4 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
@@ -12,10 +13,11 @@ export const useColumnDefinitionsFromObjectMetadata = (
objectMetadataItem: ObjectMetadataItem,
) => {
const activeFieldMetadataItems = objectMetadataItem.readableFields.filter(
({ id, isActive, isSystem }) =>
isActive &&
// Allow label identifier field (e.g. id for junction tables) even if it's a system field
(!isSystem || id === objectMetadataItem.labelIdentifierFieldMetadataId),
(field) =>
field.isActive &&
// Allow label identifier field (e.g. id for junction tables) even if it's a hidden system field
(!isHiddenSystemField(field) ||
field.id === objectMetadataItem.labelIdentifierFieldMetadataId),
);
const filterableFieldMetadataItems = useRecoilValue(
@@ -7,6 +7,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.TEXT,
isSystem: false,
isActive: true,
name: 'text',
};
expect(filterSortableFieldMetadataItems(field)).toBe(true);
@@ -17,6 +18,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.NUMBER,
isSystem: false,
isActive: true,
name: 'number',
};
expect(filterSortableFieldMetadataItems(field)).toBe(true);
@@ -27,6 +29,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.DATE_TIME,
isSystem: false,
isActive: true,
name: 'dateTime',
};
expect(filterSortableFieldMetadataItems(field)).toBe(true);
@@ -37,6 +40,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.RELATION,
isSystem: false,
isActive: true,
name: 'relation',
relation: {
type: RelationType.MANY_TO_ONE,
},
@@ -50,6 +54,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.RELATION,
isSystem: false,
isActive: true,
name: 'relation',
relation: {
type: RelationType.ONE_TO_MANY,
},
@@ -63,6 +68,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.RELATION,
isSystem: false,
isActive: true,
name: 'relation',
};
expect(filterSortableFieldMetadataItems(field)).toBe(false);
@@ -73,6 +79,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.TEXT,
isSystem: true,
isActive: true,
name: 'id',
};
expect(filterSortableFieldMetadataItems(field)).toBe(false);
@@ -83,6 +90,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.TEXT,
isSystem: false,
isActive: false,
name: 'text',
};
expect(filterSortableFieldMetadataItems(field)).toBe(false);
@@ -93,6 +101,7 @@ describe('filterSortableFieldMetadataItems', () => {
type: FieldMetadataType.RICH_TEXT,
isSystem: false,
isActive: true,
name: 'richText',
};
expect(filterSortableFieldMetadataItems(field)).toBe(false);
@@ -1,15 +1,16 @@
import { SORTABLE_FIELD_METADATA_TYPES } from '@/object-metadata/constants/SortableFieldMetadataTypes';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
type SortableFieldInput = {
isSystem?: boolean | null;
isActive?: boolean | null;
name: string;
type: FieldMetadataType;
relation?: { type: RelationType } | null;
};
export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
const isSystemField = field.isSystem;
const isFieldActive = field.isActive;
const isFieldTypeSortable = SORTABLE_FIELD_METADATA_TYPES.includes(
@@ -21,7 +22,7 @@ export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
field.relation?.type === RelationType.MANY_TO_ONE;
return (
!isSystemField &&
!isHiddenSystemField(field) &&
isFieldActive &&
(isFieldTypeSortable || isRelationFieldSortable)
);
@@ -1,7 +1,8 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
export const filterUserFacingFieldMetadataItems = (
field: FieldMetadataItem,
) => {
return !field.isSystem;
return !isHiddenSystemField(field);
};
@@ -1,9 +1,10 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
export const getActiveFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
) =>
objectMetadataItem.readableFields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
fieldMetadataItem.isActive && !isHiddenSystemField(fieldMetadataItem),
);
@@ -1,4 +1,5 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
export const getFilterFilterableFieldMetadataItems = ({
@@ -7,7 +8,6 @@ export const getFilterFilterableFieldMetadataItems = ({
isJsonFilterEnabled: boolean;
}) => {
return (field: FieldMetadataItem) => {
const isSystemField = field.isSystem;
const isFieldActive = field.isActive;
const isIdField = field.name === 'id';
@@ -44,7 +44,7 @@ export const getFilterFilterableFieldMetadataItems = ({
].includes(field.type);
const isFieldFilterable =
(!isSystemField || isIdField || isWorkflowRelationField) &&
(!isHiddenSystemField(field) || isIdField || isWorkflowRelationField) &&
isFieldActive &&
isRelationFieldHandled &&
isFieldTypeFilterable;
@@ -1,5 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
type IsFieldMetadataAvailableForViewFieldArgs = {
objectNameSingular: string;
@@ -23,7 +24,7 @@ export const isActiveFieldMetadataItem = ({
return true;
}
if (fieldMetadata.isSystem === true) {
if (isHiddenSystemField(fieldMetadata)) {
return false;
}
@@ -0,0 +1,6 @@
import { HIDDEN_SYSTEM_FIELD_NAMES } from '@/object-metadata/constants/HiddenSystemFieldNames';
export const isHiddenSystemField = (field: {
isSystem?: boolean | null;
name: string;
}) => field.isSystem === true && HIDDEN_SYSTEM_FIELD_NAMES.has(field.name);
@@ -1,4 +1,5 @@
import { flattenedReadableFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { useRecoilValue } from 'recoil';
@@ -17,7 +18,7 @@ export const useFilterVisibleAndReadableRecordField = () => {
fieldMetadataItemToFilter.id ===
recordFieldToFilter.fieldMetadataItemId &&
fieldMetadataItemToFilter.isActive === true &&
fieldMetadataItemToFilter.isSystem !== true,
!isHiddenSystemField(fieldMetadataItemToFilter),
)
);
};
@@ -4,6 +4,7 @@ import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-met
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
@@ -69,7 +70,7 @@ export const useLoadRecordIndexStates = () => {
({ set, snapshot }) =>
(viewFields: ViewField[], objectMetadataItem: ObjectMetadataItem) => {
const activeFieldMetadataItems = objectMetadataItem.fields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
(field) => field.isActive && !isHiddenSystemField(field),
);
const filterableFieldMetadataItems = snapshot
@@ -1,4 +1,5 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
@@ -9,7 +10,8 @@ export const spreadsheetImportFilterAvailableFieldMetadataItems = (
.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive &&
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
(!isHiddenSystemField(fieldMetadataItem) ||
fieldMetadataItem.name === 'id') &&
fieldMetadataItem.name !== 'deletedAt' &&
(![
FieldMetadataType.RELATION,
@@ -1,4 +1,5 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
@@ -19,7 +20,7 @@ export const getAvailableAggregationsFromObjectFields = (
): Aggregations => {
return fields.reduce<Record<string, NameForAggregation>>(
(acc, field) => {
if (field.isSystem === true) {
if (isHiddenSystemField(field)) {
return acc;
}
@@ -1,6 +1,7 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -52,5 +53,7 @@ export const isFieldCellSupported = (
}
}
return !fieldMetadataItem.isSystem && !!fieldMetadataItem.isActive;
return (
!isHiddenSystemField(fieldMetadataItem) && !!fieldMetadataItem.isActive
);
};
@@ -4,6 +4,7 @@ import { type Node, type NodeProps } from '@xyflow/react';
import { Link } from 'react-router-dom';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -110,7 +111,7 @@ export const SettingsDataModelOverviewObject = ({
});
const fields = objectMetadataItem.fields.filter(
(x) => !x.isSystem && x.isActive,
(x) => !isHiddenSystemField(x) && x.isActive,
);
const countNonRelation = fields.filter(
@@ -4,6 +4,7 @@ import { useLingui } from '@lingui/react/macro';
import { type ReactNode } from 'react';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { SettingsItemTypeTag } from '@/settings/components/SettingsItemTypeTag';
import {
StyledActionTableCell,
@@ -84,7 +85,11 @@ export const SettingsObjectMetadataItemTableRow = ({
<SettingsItemTypeTag item={objectMetadataItem} />
</TableCell>
<TableCell align="right">
{objectMetadataItem.fields.filter((field) => !field.isSystem).length}
{
objectMetadataItem.fields.filter(
(field) => !isHiddenSystemField(field),
).length
}
</TableCell>
<TableCell align="right">{totalObjectCount}</TableCell>
<StyledActionTableCell>{action}</StyledActionTableCell>
@@ -1,5 +1,6 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -89,7 +90,7 @@ export const SettingsObjectRelationsTable = ({
const relationFields = useMemo(() => {
return objectMetadataItem.fields.filter(
(field) =>
(showSystemRelations || !field.isSystem) &&
(showSystemRelations || !isHiddenSystemField(field)) &&
(field.type === FieldMetadataType.RELATION ||
field.type === FieldMetadataType.MORPH_RELATION),
);
@@ -1,4 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { SettingsObjectRelationsTable } from '@/settings/data-model/object-details/components/SettingsObjectRelationsTable';
import styled from '@emotion/styled';
@@ -31,7 +32,7 @@ export const ObjectFields = ({ objectMetadataItem }: ObjectFieldsProps) => {
const hasRelations = objectMetadataItem.fields.some(
(field) =>
!field.isSystem &&
!isHiddenSystemField(field) &&
(field.type === FieldMetadataType.RELATION ||
field.type === FieldMetadataType.MORPH_RELATION),
);
@@ -1,4 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { useObjectPermissionDerivedStates } from '@/settings/roles/role-permissions/object-level-permissions/field-permissions/hooks/useObjectPermissionDerivedStates';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useLingui } from '@lingui/react/macro';
@@ -22,7 +23,7 @@ export const SettingsRolePermissionsObjectLevelSeeFieldsValueForObject = ({
const objectMetadataItemId = objectMetadataItem.id;
const restrictableFieldMetadataItems = objectMetadataItem.fields.filter(
(fieldMetadataItem) => !fieldMetadataItem.isSystem,
(fieldMetadataItem) => !isHiddenSystemField(fieldMetadataItem),
);
const numberOfRestrictableFieldMetadataItemsOnRead =
@@ -1,4 +1,5 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
@@ -50,7 +51,7 @@ export const WorkflowFormFieldSettingsSelect = ({
.filter(
(field) =>
field.isActive &&
!field.isSystem &&
!isHiddenSystemField(field) &&
field.type === FieldMetadataType.SELECT,
)
.map((field) => ({
@@ -1,4 +1,5 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { type WorkflowActionType } from '@/workflow/types/Workflow';
import { CustomError } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -46,21 +47,21 @@ export const shouldDisplayFormField = ({
return (
!isNotSupportedRelation &&
!fieldMetadataItem.isUIReadOnly &&
!fieldMetadataItem.isSystem &&
!isHiddenSystemField(fieldMetadataItem) &&
fieldMetadataItem.isActive
);
case 'UPSERT_RECORD':
return (
(!isNotSupportedRelation &&
!fieldMetadataItem.isUIReadOnly &&
!fieldMetadataItem.isSystem &&
!isHiddenSystemField(fieldMetadataItem) &&
fieldMetadataItem.isActive) ||
isIdField
);
case 'FIND_RECORDS':
return (
!isNotSupportedRelation &&
(!fieldMetadataItem.isSystem || isIdField) &&
(!isHiddenSystemField(fieldMetadataItem) || isIdField) &&
fieldMetadataItem.isActive
);
default:
@@ -1,4 +1,5 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { SettingsLogicFunctionsTable } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
@@ -40,7 +41,7 @@ export const SettingsApplicationDetailContentTab = ({
)
.map((objectMetadataItem) => {
const nonSystemFields = objectMetadataItem.fields.filter(
(field) => !field.isSystem,
(field) => !isHiddenSystemField(field),
);
const fields: ApplicationDataTableFieldItem[] = nonSystemFields.map(
@@ -1,4 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import {
SettingsObjectFieldItemTableRow,
StyledObjectFieldTableRow,
@@ -110,7 +111,7 @@ export const SettingsObjectFieldTable = ({
const filteredBySystem = showSystemFields
? settingsObjectFields
: settingsObjectFields?.filter(
(fieldMetadataItem) => !fieldMetadataItem.isSystem,
(fieldMetadataItem) => !isHiddenSystemField(fieldMetadataItem),
);
const fieldsToDisplay = excludeRelations
@@ -2,6 +2,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
import { SettingsObjectMetadataItemTableRow } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { StyledObjectTableRow } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
@@ -79,7 +80,7 @@ export const SettingsObjectTable = ({
currentWorkspace?.workspaceCustomApplication?.id,
}).labelText,
fieldsCount: objectMetadataItem.fields.filter(
(field) => !field.isSystem,
(field) => !isHiddenSystemField(field),
).length,
totalObjectCount:
totalCountByObjectMetadataItemNamePlural[
@@ -0,0 +1,118 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
import { getMetadataRelatedMetadataNames } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-related-metadata-names.util';
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
const SYSTEM_FIELD_NAMES = Object.keys(PARTIAL_SYSTEM_FLAT_FIELD_METADATAS);
@Command({
name: 'upgrade:1-18:backfill-system-fields-is-system',
description:
'Set isSystem to true for all field metadata matching system field names (id, createdAt, updatedAt, deletedAt, createdBy, updatedBy, position, searchVector)',
})
export class BackfillSystemFieldsIsSystemCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectDataSource()
private readonly coreDataSource: DataSource,
protected readonly twentyORMGlobalManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const dryRun = options?.dryRun ?? false;
this.logger.log(
`${dryRun ? '[DRY RUN] ' : ''}Backfilling isSystem for system fields in workspace ${workspaceId}`,
);
if (dryRun) {
this.logger.log(
`[DRY RUN] Would set isSystem=true for fields named [${SYSTEM_FIELD_NAMES.join(', ')}] in workspace ${workspaceId}. Skipping.`,
);
return;
}
const queryRunner = this.coreDataSource.createQueryRunner();
await queryRunner.connect();
try {
const result = await queryRunner.query(
`UPDATE core."fieldMetadata"
SET "isSystem" = true
WHERE "workspaceId" = $1
AND "name" = ANY($2)
AND "isSystem" = false`,
[workspaceId, SYSTEM_FIELD_NAMES],
);
const updatedCount = result?.[1] ?? 0;
if (updatedCount > 0) {
this.logger.log(
`Set isSystem=true for ${updatedCount} field(s) in workspace ${workspaceId}`,
);
await this.invalidateCaches(workspaceId);
} else {
this.logger.log(
`No fields needed updating in workspace ${workspaceId}`,
);
}
} finally {
await queryRunner.release();
}
}
private async invalidateCaches(workspaceId: string): Promise<void> {
const modifiedMetadataNames = ['fieldMetadata'] as const;
const cacheKeysToInvalidate: WorkspaceCacheKeyName[] = [
...new Set(
modifiedMetadataNames
.flatMap((name) => [name, ...getMetadataRelatedMetadataNames(name)])
.map(getMetadataFlatEntityMapsKey),
),
'ORMEntityMetadatas',
];
await this.workspaceCacheService.invalidateAndRecompute(
workspaceId,
cacheKeysToInvalidate,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
await this.workspaceCacheStorageService.flush(workspaceId);
this.logger.log(
`Cache invalidated and metadata version incremented for workspace ${workspaceId}`,
);
}
}
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command';
import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command';
import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-system-fields-is-system.command';
import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command';
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command';
@@ -62,6 +63,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
BackfillMessageChannelThrottleRetryAfterCommand,
BackfillStandardViewsAndFieldMetadataCommand,
MigrateWorkflowSendEmailAttachmentsCommand,
BackfillSystemFieldsIsSystemCommand,
],
exports: [
MigratePersonAvatarFilesCommand,
@@ -73,6 +75,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
MigrateWorkspacePicturesCommand,
BackfillFileSizeAndMimeTypeCommand,
MigrateWorkflowSendEmailAttachmentsCommand,
BackfillSystemFieldsIsSystemCommand,
],
})
export class V1_18_UpgradeVersionCommandModule {}
@@ -21,6 +21,7 @@ import { MigrateWorkflowCodeStepsCommand } from 'src/database/commands/upgrade-v
import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command';
import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command';
import { BackfillSystemFieldsIsSystemCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-system-fields-is-system.command';
import { MigrateActivityRichTextAttachmentFileIdsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-activity-rich-text-attachment-file-ids.command';
import { MigrateAttachmentFilesCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-attachment-files.command';
import { MigrateFavoritesToNavigationMenuItemsCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-migrate-favorites-to-navigation-menu-items.command';
@@ -67,6 +68,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly backfillStandardViewsAndFieldMetadataCommand: BackfillStandardViewsAndFieldMetadataCommand,
protected readonly migrateWorkspacePicturesCommand: MigrateWorkspacePicturesCommand,
protected readonly migrateWorkflowSendEmailAttachmentsCommand: MigrateWorkflowSendEmailAttachmentsCommand,
protected readonly backfillSystemFieldsIsSystemCommand: BackfillSystemFieldsIsSystemCommand,
) {
super(
workspaceRepository,
@@ -101,6 +103,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.backfillFileSizeAndMimeTypeCommand,
this.backfillMessageChannelThrottleRetryAfterCommand,
this.backfillStandardViewsAndFieldMetadataCommand,
this.backfillSystemFieldsIsSystemCommand,
];
this.allCommands = {
@@ -78,8 +78,10 @@ export class ApplicationManifestMigrationService {
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigrationFromTo(
{
buildOptions: {
isSystemBuild: true,
isSystemBuild: false,
inferDeletionFromMissingEntities: true,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
},
fromToAllFlatEntityMaps,
workspaceId,
@@ -176,6 +176,7 @@ export class ApplicationSyncService {
buildOptions: {
isSystemBuild: true,
inferDeletionFromMissingEntities: true,
applicationUniversalIdentifier,
},
fromToAllFlatEntityMaps,
workspaceId,
@@ -9,6 +9,7 @@ import {
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
import { PARTIAL_SYSTEM_FLAT_FIELD_METADATAS } from 'src/engine/metadata-modules/object-metadata/constants/partial-system-flat-field-metadatas.constant';
import { isMorphOrRelationFieldMetadataType } from 'src/engine/utils/is-morph-or-relation-field-metadata-type.util';
import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type';
@@ -79,7 +80,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({
universalSettings: fieldManifest.universalSettings ?? null,
isCustom: true,
isActive: true,
isSystem: false,
isSystem: fieldManifest.name in PARTIAL_SYSTEM_FLAT_FIELD_METADATAS,
isUIReadOnly: false,
isNullable: fieldManifest.isNullable ?? true,
isUnique: false,
@@ -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,
@@ -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;
@@ -8,6 +8,7 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-
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 { 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';
@@ -61,7 +62,7 @@ export const validateFlatFieldMetadataNameAvailability = ({
);
if (
!buildOptions.isSystemBuild &&
!isCallerTwentyStandardApp(buildOptions) &&
reservedCompositeFieldsNames.includes(name)
) {
errors.push({
@@ -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({
@@ -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;
};
@@ -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;
};
@@ -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),
}),
);
@@ -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;
@@ -67,7 +67,7 @@ export const validateFlatObjectMetadataNameAndLabels = ({
universalFlatObjectMetadataToValidate.isLabelSyncedWithName &&
!areFlatObjectMetadataNamesSyncedWithLabels({
flatObjectdMetadata: universalFlatObjectMetadataToValidate,
isSystemBuild: buildOptions.isSystemBuild,
buildOptions,
})
) {
errors.push({
@@ -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;
};
@@ -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 [];
}
@@ -68,7 +68,7 @@ export class NavigationMenuItemDeletionService {
},
},
workspaceId,
isSystemBuild: true,
isSystemBuild: false,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
},
@@ -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';
export 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>;
@@ -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);
}
@@ -934,34 +934,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>,
@@ -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 {
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,
},
id: {
...id,
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,
...buildObjectSystemFlatFieldMetadatas({
applicationUniversalIdentifier,
objectMetadataUniversalIdentifier,
now,
searchVectorUniversalSettings,
}),
...(nameField && { nameField }),
createdAtField,
updatedAtField,
updatedByField,
deletedAtField,
createdByField,
positionField,
searchVectorField,
},
} as const satisfies {
fields: Record<string, UniversalFlatFieldMetadata>;
@@ -26,7 +26,7 @@ export const buildDefaultIndexesForCustomObject = ({
{
createdAt: createdAt.toISOString(),
fieldMetadataUniversalIdentifier:
defaultFlatFieldForCustomObjectMaps.fields.searchVectorField
defaultFlatFieldForCustomObjectMaps.fields.searchVector
.universalIdentifier,
indexMetadataUniversalIdentifier:
tsFlatVectorIndexUniversalIdentifier,
@@ -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);
}
@@ -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;
@@ -113,6 +113,8 @@ export class TwentyStandardApplicationService {
buildOptions: {
isSystemBuild: true,
inferDeletionFromMissingEntities: true,
applicationUniversalIdentifier:
twentyStandardFlatApplication.universalIdentifier,
},
fromToAllFlatEntityMaps,
workspaceId,
@@ -54,6 +54,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -75,6 +76,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -96,6 +98,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -219,6 +222,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -241,6 +245,7 @@ export const buildAttachmentStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -53,6 +53,7 @@ export const buildBlocklistStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildBlocklistStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -95,6 +97,7 @@ export const buildBlocklistStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -55,6 +55,7 @@ export const buildCalendarChannelEventAssociationStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildCalendarChannelEventAssociationStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -93,6 +95,7 @@ export const buildCalendarChannelEventAssociationStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -55,6 +55,7 @@ export const buildCalendarChannelStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildCalendarChannelStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -93,6 +95,7 @@ export const buildCalendarChannelStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -55,6 +55,7 @@ export const buildCalendarEventParticipantStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildCalendarEventParticipantStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -93,6 +95,7 @@ export const buildCalendarEventParticipantStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -51,6 +51,7 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -70,6 +71,7 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -89,6 +91,7 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -55,6 +55,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -76,6 +77,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -97,6 +99,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -273,6 +276,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -295,6 +299,7 @@ export const buildCompanyStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -55,6 +55,7 @@ export const buildConnectedAccountStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildConnectedAccountStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -93,6 +95,7 @@ export const buildConnectedAccountStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -54,6 +54,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -73,6 +74,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -92,6 +94,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -163,6 +166,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -185,6 +189,7 @@ export const buildDashboardStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -55,6 +55,7 @@ export const buildFavoriteFolderStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildFavoriteFolderStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -93,6 +95,7 @@ export const buildFavoriteFolderStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -53,6 +53,7 @@ export const buildFavoriteStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -72,6 +73,7 @@ export const buildFavoriteStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -91,6 +93,7 @@ export const buildFavoriteStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -60,6 +60,7 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -79,6 +80,7 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -98,6 +100,7 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas =
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -56,6 +56,7 @@ export const buildMessageChannelStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -75,6 +76,7 @@ export const buildMessageChannelStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -94,6 +96,7 @@ export const buildMessageChannelStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -52,6 +52,7 @@ export const buildMessageFolderStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -71,6 +72,7 @@ export const buildMessageFolderStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -90,6 +92,7 @@ export const buildMessageFolderStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -56,6 +56,7 @@ export const buildMessageParticipantStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -75,6 +76,7 @@ export const buildMessageParticipantStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -94,6 +96,7 @@ export const buildMessageParticipantStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -52,6 +52,7 @@ export const buildMessageStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -71,6 +72,7 @@ export const buildMessageStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -90,6 +92,7 @@ export const buildMessageStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -51,6 +51,7 @@ export const buildMessageThreadStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -70,6 +71,7 @@ export const buildMessageThreadStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -89,6 +91,7 @@ export const buildMessageThreadStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -54,6 +54,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -75,6 +76,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -96,6 +98,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -168,6 +171,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -190,6 +194,7 @@ export const buildNoteStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -54,6 +54,7 @@ export const buildNoteTargetStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -75,6 +76,7 @@ export const buildNoteTargetStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -96,6 +98,7 @@ export const buildNoteTargetStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -55,6 +55,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -76,6 +77,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -97,6 +99,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -219,6 +222,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -241,6 +245,7 @@ export const buildOpportunityStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -55,6 +55,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -76,6 +77,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -97,6 +99,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -294,6 +297,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -316,6 +320,7 @@ export const buildPersonStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -55,6 +55,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -76,6 +77,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -97,6 +99,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -212,6 +215,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -234,6 +238,7 @@ export const buildTaskStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -54,6 +54,7 @@ export const buildTaskTargetStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -75,6 +76,7 @@ export const buildTaskTargetStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -96,6 +98,7 @@ export const buildTaskTargetStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: {
@@ -57,6 +57,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -76,6 +77,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -95,6 +97,7 @@ export const buildTimelineActivityStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -55,6 +55,7 @@ export const buildWorkflowAutomatedTriggerStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -74,6 +75,7 @@ export const buildWorkflowAutomatedTriggerStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -93,6 +95,7 @@ export const buildWorkflowAutomatedTriggerStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -54,6 +54,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -73,6 +74,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -92,6 +94,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -210,6 +213,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
label: 'Executed by',
description: 'The executor of the workflow',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: {
@@ -232,6 +236,7 @@ export const buildWorkflowRunStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -53,6 +53,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -72,6 +73,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -91,6 +93,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -187,6 +190,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
label: 'Created by',
description: 'The creator of the record',
icon: 'IconCreativeCommonsSa',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -209,6 +213,7 @@ export const buildWorkflowStandardFlatFieldMetadatas = ({
label: 'Updated by',
description: 'The workspace member who last updated the record',
icon: 'IconUserCircle',
isSystem: true,
isUIReadOnly: true,
isNullable: false,
defaultValue: {
@@ -57,6 +57,7 @@ export const buildWorkflowVersionStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -76,6 +77,7 @@ export const buildWorkflowVersionStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -95,6 +97,7 @@ export const buildWorkflowVersionStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -60,6 +60,7 @@ export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({
label: 'Creation date',
description: 'Creation date',
icon: 'IconCalendar',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -79,6 +80,7 @@ export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({
label: 'Last update',
description: 'Last time the record was changed',
icon: 'IconCalendarClock',
isSystem: true,
isNullable: false,
isUIReadOnly: true,
defaultValue: 'now',
@@ -98,6 +100,7 @@ export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({
label: 'Deleted at',
description: 'Date when the record was deleted',
icon: 'IconCalendarMinus',
isSystem: true,
isNullable: true,
isUIReadOnly: true,
settings: { displayFormat: DateDisplayFormat.RELATIVE },
@@ -14,6 +14,7 @@ import {
} from 'src/engine/workspace-manager/workspace-migration/types/workspace-migration-orchestrator.type';
import { AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { aggregateOrchestratorActionsReport } from 'src/engine/workspace-manager/workspace-migration/utils/aggregate-orchestrator-actions-report.util';
import { crossEntityTransversalValidation } from 'src/engine/workspace-manager/workspace-migration/utils/post-atomic-entity-validation-transversal-entity-validation.util';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
@@ -994,6 +995,13 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
const { objectMetadata } = crossEntityTransversalValidation({
optimisticUniversalFlatMaps: optimisticAllFlatEntityMaps,
orchestratorActionsReport,
});
orchestratorFailureReport.objectMetadata.push(...objectMetadata);
const allErrors = Object.values(orchestratorFailureReport);
if (allErrors.some((report) => report.length > 0)) {
@@ -296,6 +296,7 @@ export class WorkspaceMigrationValidateBuildAndRunService {
buildOptions: {
isSystemBuild,
inferDeletionFromMissingEntities,
applicationUniversalIdentifier,
},
fromToAllFlatEntityMaps,
workspaceId,
@@ -40,6 +40,7 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: false },
isSystemBuild: false,
applicationUniversalIdentifier: 'application-universal-identifier-1',
},
},
},
@@ -67,6 +68,7 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: true },
isSystemBuild: false,
applicationUniversalIdentifier: 'application-universal-identifier-1',
},
},
},
@@ -111,6 +113,7 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: false },
isSystemBuild: false,
applicationUniversalIdentifier: 'application-universal-identifier-1',
},
},
},
@@ -183,6 +186,7 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: true },
isSystemBuild: false,
applicationUniversalIdentifier: 'application-universal-identifier-1',
},
},
},
@@ -211,6 +215,7 @@ describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: false },
isSystemBuild: false,
applicationUniversalIdentifier: 'application-universal-identifier-1',
},
},
},
@@ -0,0 +1,21 @@
import { validateObjectMetadataSystemFieldsIntegrity } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-object-metadata-system-fields-integrity';
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';
export const crossEntityTransversalValidation = ({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
}: {
optimisticUniversalFlatMaps: AllUniversalFlatEntityMaps;
orchestratorActionsReport: OrchestratorActionsReport;
}): Pick<OrchestratorFailureReport, 'objectMetadata'> => {
const { objectMetadata } = validateObjectMetadataSystemFieldsIntegrity({
optimisticUniversalFlatMaps,
orchestratorActionsReport,
});
return { objectMetadata };
};
@@ -3,4 +3,5 @@ import { type InferDeletionFromMissingEntities } from 'src/engine/workspace-mana
export type WorkspaceMigrationBuilderOptions = {
inferDeletionFromMissingEntities?: InferDeletionFromMissingEntities;
isSystemBuild: boolean;
applicationUniversalIdentifier: string;
};
@@ -0,0 +1,40 @@
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 { isMorphOrRelationUniversalFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
import { type AllUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/all-universal-flat-entity-maps.type';
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
export const buildUniversalFlatObjectFieldByNameAndJoinColumnMaps = ({
flatFieldMetadataMaps,
flatObjectMetadata,
}: {
flatFieldMetadataMaps: AllUniversalFlatEntityMaps['flatFieldMetadataMaps'];
flatObjectMetadata: UniversalFlatObjectMetadata;
}) => {
const fieldUniversalIdentifierByName: Record<string, string> = {};
const fieldUniversalIdentifierByJoinColumnName: Record<string, string> = {};
const objectFields =
findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow({
universalIdentifiers: flatObjectMetadata.fieldUniversalIdentifiers,
flatEntityMaps: flatFieldMetadataMaps,
});
for (const field of objectFields) {
fieldUniversalIdentifierByName[field.name] = field.universalIdentifier;
if (isMorphOrRelationUniversalFlatFieldMetadata(field)) {
if (isDefined(field.universalSettings.joinColumnName)) {
fieldUniversalIdentifierByJoinColumnName[
field.universalSettings.joinColumnName
] = field.universalIdentifier;
}
}
}
return {
fieldUniversalIdentifierByName,
fieldUniversalIdentifierByJoinColumnName,
};
};
@@ -8,6 +8,7 @@ import { AgentExceptionCode } from 'src/engine/metadata-modules/ai/ai-agent/agen
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
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 { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@@ -97,7 +98,7 @@ export class FlatAgentValidatorService {
}
if (
!buildOptions.isSystemBuild &&
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: existingAgent.universalIdentifier,
applicationUniversalIdentifier:
@@ -148,7 +149,7 @@ export class FlatAgentValidatorService {
}
if (
!buildOptions.isSystemBuild &&
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: fromFlatAgent.universalIdentifier,
applicationUniversalIdentifier:
@@ -75,6 +75,17 @@ export class FlatFieldMetadataValidatorService {
flatFieldMetadataToValidate.objectMetadataUniversalIdentifier,
};
if (
!buildOptions.isSystemBuild &&
existingFlatFieldMetadataToUpdate.isSystem
) {
validationResult.errors.push({
code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
message: 'System fields cannot be updated',
userFriendlyMessage: msg`System fields cannot be updated`,
});
}
const flatObjectMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatFieldMetadataToValidate.objectMetadataUniversalIdentifier,
@@ -155,7 +166,7 @@ export class FlatFieldMetadataValidatorService {
flatFieldMetadataToValidate.isLabelSyncedWithName &&
!isFlatFieldMetadataNameSyncedWithLabel({
flatFieldMetadata: flatFieldMetadataToValidate,
isSystemBuild: buildOptions.isSystemBuild,
buildOptions,
})
) {
validationResult.errors.push({
@@ -205,6 +216,7 @@ export class FlatFieldMetadataValidatorService {
flatFieldMetadataMaps: optimisticFlatFieldMetadataMaps,
flatObjectMetadataMaps,
},
buildOptions,
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.fieldMetadata
>): FailedFlatEntityValidation<'fieldMetadata', 'delete'> {
@@ -238,6 +250,14 @@ export class FlatFieldMetadataValidatorService {
flatFieldMetadataToDelete.objectMetadataUniversalIdentifier,
};
if (!buildOptions.isSystemBuild && flatFieldMetadataToDelete.isSystem) {
validationResult.errors.push({
code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
message: 'System fields cannot be deleted',
userFriendlyMessage: msg`System fields cannot be deleted`,
});
}
const relatedFlatObjectMetadata = findFlatEntityByUniversalIdentifier({
universalIdentifier:
flatFieldMetadataToDelete.objectMetadataUniversalIdentifier,
@@ -365,7 +385,7 @@ export class FlatFieldMetadataValidatorService {
flatFieldMetadataToValidate.isLabelSyncedWithName &&
!isFlatFieldMetadataNameSyncedWithLabel({
flatFieldMetadata: flatFieldMetadataToValidate,
isSystemBuild: buildOptions.isSystemBuild,
buildOptions,
})
) {
validationResult.errors.push({
@@ -9,6 +9,7 @@ import { validateFlatObjectMetadataIdentifiers } from 'src/engine/metadata-modul
import { validateFlatObjectMetadataNameAndLabels } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util';
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
import { 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 { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@@ -61,6 +62,14 @@ export class FlatObjectMetadataValidatorService {
nameSingular: existingFlatObjectMetadata.nameSingular,
};
if (!buildOptions.isSystemBuild && existingFlatObjectMetadata.isSystem) {
validationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`System objects cannot be updated`,
userFriendlyMessage: msg`System objects cannot be updated`,
});
}
validationResult.errors.push(
...validateFlatObjectMetadataNameAndLabels({
optimisticUniversalFlatObjectMetadataMaps:
@@ -139,8 +148,16 @@ export class FlatObjectMetadataValidatorService {
});
}
if (!buildOptions.isSystemBuild && flatObjectMetadataToDelete.isSystem) {
validationResult.errors.push({
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
message: t`System objects cannot be deleted`,
userFriendlyMessage: msg`System objects cannot be deleted`,
});
}
if (
!buildOptions.isSystemBuild &&
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp(flatObjectMetadataToDelete)
) {
validationResult.errors.push({
@@ -8,6 +8,7 @@ import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules
import { SkillExceptionCode } from 'src/engine/metadata-modules/skill/skill.exception';
import { type UniversalFlatSkill } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-skill.type';
import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util';
import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util';
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 { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
@@ -90,7 +91,7 @@ export class FlatSkillValidatorService {
}
if (
!buildOptions.isSystemBuild &&
!isCallerTwentyStandardApp(buildOptions) &&
belongsToTwentyStandardApp({
universalIdentifier: existingSkill.universalIdentifier,
applicationUniversalIdentifier:
@@ -153,7 +154,7 @@ export class FlatSkillValidatorService {
});
if (
!buildOptions.isSystemBuild &&
!isCallerTwentyStandardApp(buildOptions) &&
isTwentyStandardSkill &&
hasNonIsActiveUpdates
) {
@@ -0,0 +1,391 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Sync application should fail due to object system fields integrity should fail when trying to delete a system field after a successful sync 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"fieldMetadata": [
{
"errors": [
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "System fields cannot be deleted",
"userFriendlyMessage": "System fields cannot be deleted",
},
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "Cannot delete, please update the label identifier field first",
"userFriendlyMessage": "Cannot delete, please update the label identifier field first",
},
],
"flatEntityMinimalInformation": {
"name": "id",
"objectMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldMetadata",
"status": "fail",
"type": "delete",
},
],
},
"message": "Validation failed for 1 fieldMetadata",
"summary": {
"fieldMetadata": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity should fail when trying to update a system field after a successful sync 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"fieldMetadata": [
{
"errors": [
{
"code": "FIELD_MUTATION_NOT_ALLOWED",
"message": "System fields cannot be updated",
"userFriendlyMessage": "System fields cannot be updated",
},
],
"flatEntityMinimalInformation": {
"name": "id",
"objectMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldMetadata",
"status": "fail",
"type": "update",
},
],
},
"message": "Validation failed for 1 fieldMetadata",
"summary": {
"fieldMetadata": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object has createdAt field with wrong type (TEXT instead of DATE_TIME) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"fieldMetadata": [
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"name": "title",
"objectMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldMetadata",
"status": "fail",
"type": "create",
},
{
"errors": [
{
"code": "OBJECT_METADATA_NOT_FOUND",
"message": "Object metadata not found",
"userFriendlyMessage": "Field to create related object not found",
},
],
"flatEntityMinimalInformation": {
"name": "createdAt",
"objectMetadataUniversalIdentifier": Any<String>,
"universalIdentifier": Any<String>,
},
"metadataName": "fieldMetadata",
"status": "fail",
"type": "create",
},
],
"objectMetadata": [
{
"errors": [
{
"code": "INVALID_OBJECT_INPUT",
"message": "Names are not synced with labels",
"userFriendlyMessage": "Names are not synced with labels",
},
],
"flatEntityMinimalInformation": {
"namePlural": "wrongCreatedAtObjects",
"nameSingular": "wrongCreatedAtObject",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"status": "fail",
"type": "create",
},
],
},
"message": "Validation failed for 2 fieldMetadatas, 1 objectMetadata",
"summary": {
"fieldMetadata": 2,
"objectMetadata": 1,
"totalErrors": 3,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object has id field with wrong type (TEXT instead of UUID) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"objectMetadata": [
{
"errors": [
{
"code": "INVALID_SYSTEM_FIELD",
"message": "System field id has invalid type: expected UUID, got TEXT",
"userFriendlyMessage": "System field id has invalid type",
"value": "TEXT",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field createdAt is missing",
"userFriendlyMessage": "System field createdAt is missing",
"value": "createdAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field updatedAt is missing",
"userFriendlyMessage": "System field updatedAt is missing",
"value": "updatedAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field deletedAt is missing",
"userFriendlyMessage": "System field deletedAt is missing",
"value": "deletedAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field createdBy is missing",
"userFriendlyMessage": "System field createdBy is missing",
"value": "createdBy",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field updatedBy is missing",
"userFriendlyMessage": "System field updatedBy is missing",
"value": "updatedBy",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field position is missing",
"userFriendlyMessage": "System field position is missing",
"value": "position",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field searchVector is missing",
"userFriendlyMessage": "System field searchVector is missing",
"value": "searchVector",
},
],
"flatEntityMinimalInformation": {
"namePlural": "wrongIdTypeObjects",
"nameSingular": "wrongIdTypeObject",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
},
],
},
"message": "Validation failed for 1 objectMetadata",
"summary": {
"objectMetadata": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object has position field with wrong type (TEXT instead of POSITION) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"objectMetadata": [
{
"errors": [
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field id is missing",
"userFriendlyMessage": "System field id is missing",
"value": "id",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field createdAt is missing",
"userFriendlyMessage": "System field createdAt is missing",
"value": "createdAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field updatedAt is missing",
"userFriendlyMessage": "System field updatedAt is missing",
"value": "updatedAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field deletedAt is missing",
"userFriendlyMessage": "System field deletedAt is missing",
"value": "deletedAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field createdBy is missing",
"userFriendlyMessage": "System field createdBy is missing",
"value": "createdBy",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field updatedBy is missing",
"userFriendlyMessage": "System field updatedBy is missing",
"value": "updatedBy",
},
{
"code": "INVALID_SYSTEM_FIELD",
"message": "System field position has invalid type: expected POSITION, got TEXT",
"userFriendlyMessage": "System field position has invalid type",
"value": "TEXT",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field searchVector is missing",
"userFriendlyMessage": "System field searchVector is missing",
"value": "searchVector",
},
],
"flatEntityMinimalInformation": {
"namePlural": "wrongPositionObjects",
"nameSingular": "wrongPositionObject",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
},
],
},
"message": "Validation failed for 1 objectMetadata",
"summary": {
"objectMetadata": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
exports[`Sync application should fail due to object system fields integrity when object is created without any system fields (missing all 8 system fields) 1`] = `
{
"extensions": {
"code": "METADATA_VALIDATION_FAILED",
"errors": {
"objectMetadata": [
{
"errors": [
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field id is missing",
"userFriendlyMessage": "System field id is missing",
"value": "id",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field createdAt is missing",
"userFriendlyMessage": "System field createdAt is missing",
"value": "createdAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field updatedAt is missing",
"userFriendlyMessage": "System field updatedAt is missing",
"value": "updatedAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field deletedAt is missing",
"userFriendlyMessage": "System field deletedAt is missing",
"value": "deletedAt",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field createdBy is missing",
"userFriendlyMessage": "System field createdBy is missing",
"value": "createdBy",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field updatedBy is missing",
"userFriendlyMessage": "System field updatedBy is missing",
"value": "updatedBy",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field position is missing",
"userFriendlyMessage": "System field position is missing",
"value": "position",
},
{
"code": "MISSING_SYSTEM_FIELD",
"message": "System field searchVector is missing",
"userFriendlyMessage": "System field searchVector is missing",
"value": "searchVector",
},
],
"flatEntityMinimalInformation": {
"namePlural": "noSystemFieldsObjects",
"nameSingular": "noSystemFieldsObject",
"universalIdentifier": Any<String>,
},
"metadataName": "objectMetadata",
"type": "create",
},
],
},
"message": "Validation failed for 1 objectMetadata",
"summary": {
"objectMetadata": 1,
"totalErrors": 1,
},
"userFriendlyMessage": "Metadata validation failed",
},
"message": "Validation errors occurred while syncing application manifest metadata",
"name": "GraphQLError",
}
`;
@@ -34,6 +34,222 @@ exports[`syncApplication should return workspace migration actions on initial sy
"metadataName": "objectMetadata",
"type": "create",
"universalFlatFieldMetadatas": [
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Id",
"morphId": null,
"name": "id",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "UUID",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Creation date",
"morphId": null,
"name": "createdAt",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "DATE_TIME",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Last update",
"morphId": null,
"name": "updatedAt",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "DATE_TIME",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Deleted at",
"morphId": null,
"name": "deletedAt",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "DATE_TIME",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": {
"name": "'System'",
"source": "'MANUAL'",
"workspaceMemberId": null,
},
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Created by",
"morphId": null,
"name": "createdBy",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "ACTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": {
"name": "'System'",
"source": "'MANUAL'",
"workspaceMemberId": null,
},
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Updated by",
"morphId": null,
"name": "updatedBy",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "ACTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Position",
"morphId": null,
"name": "position",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "POSITION",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
"defaultValue": null,
"description": null,
"icon": null,
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": true,
"isUIReadOnly": false,
"isUnique": false,
"label": "Search vector",
"morphId": null,
"name": "searchVector",
"objectMetadataUniversalIdentifier": Any<String>,
"options": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"standardOverrides": null,
"type": "TS_VECTOR",
"universalIdentifier": Any<String>,
"universalSettings": null,
"updatedAt": Any<String>,
},
{
"applicationUniversalIdentifier": Any<String>,
"createdAt": Any<String>,
@@ -0,0 +1,307 @@
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: {
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: [],
...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);
});
@@ -1,51 +1,38 @@
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 { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
import { type Manifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
import { createOneApplication } from 'test/integration/metadata/suites/application/utils/create-one-application.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 { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
import { extractRecordIdsAndDatesAsExpectAny } from 'test/utils/extract-record-ids-and-dates-as-expect-any';
const TEST_APP_ID = uuidv4();
const TEST_ROLE_ID = uuidv4();
const TEST_SECOND_ROLE_ID = uuidv4();
const TEST_OBJECT_ID = uuidv4();
const TEST_FIELD_ID = uuidv4();
const TEST_OBJECT = buildDefaultObjectManifest({
nameSingular: 'ticket',
namePlural: 'tickets',
labelSingular: 'Ticket',
labelPlural: 'Tickets',
description: 'A support ticket',
icon: 'IconTicket',
});
describe('syncApplication', () => {
let appCreated = false;
beforeAll(async () => {
await createOneApplication({
universalIdentifier: TEST_APP_ID,
await setupApplicationForSync({
applicationUniversalIdentifier: TEST_APP_ID,
name: 'Test Application',
description: 'A test application',
version: '1.0.0',
sourcePath: 'test-sync',
expectToFail: false,
});
appCreated = true;
// File upload uses multipart which requires real timers
jest.useRealTimers();
const packageJson = JSON.stringify({
name: 'test-application',
version: '1.0.0',
});
await uploadApplicationFile({
applicationUniversalIdentifier: TEST_APP_ID,
fileFolder: 'Dependencies',
filePath: 'package.json',
fileBuffer: Buffer.from(packageJson),
filename: 'package.json',
expectToFail: false,
});
jest.useFakeTimers();
}, 60000);
afterAll(async () => {
@@ -78,19 +65,7 @@ describe('syncApplication', () => {
description: 'A test role',
},
],
objects: [
{
labelIdentifierFieldMetadataUniversalIdentifier: TEST_FIELD_ID,
universalIdentifier: TEST_OBJECT_ID,
nameSingular: 'ticket',
namePlural: 'tickets',
labelSingular: 'Ticket',
labelPlural: 'Tickets',
description: 'A support ticket',
icon: 'IconTicket',
fields: [],
},
],
objects: [TEST_OBJECT],
fields: [
{
universalIdentifier: TEST_FIELD_ID,
@@ -99,7 +74,7 @@ describe('syncApplication', () => {
label: 'Description',
description: 'Ticket description',
icon: 'IconFileDescription',
objectUniversalIdentifier: TEST_OBJECT_ID,
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
},
],
logicFunctions: [],
@@ -140,7 +115,7 @@ describe('syncApplication', () => {
label: 'Body',
description: 'Ticket description',
icon: 'IconFileDescription',
objectUniversalIdentifier: TEST_OBJECT_ID,
objectUniversalIdentifier: TEST_OBJECT.universalIdentifier,
},
],
};
@@ -0,0 +1,85 @@
import { type ObjectManifest } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { v4 as uuidv4 } from 'uuid';
export const buildDefaultObjectManifest = ({
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon = 'IconTicket',
additionalFields = [],
}: {
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
icon?: string;
additionalFields?: ObjectManifest['fields'];
}): ObjectManifest => {
const idFieldUniversalIdentifier = uuidv4();
return {
universalIdentifier: uuidv4(),
labelIdentifierFieldMetadataUniversalIdentifier: idFieldUniversalIdentifier,
nameSingular,
namePlural,
labelSingular,
labelPlural,
description,
icon,
fields: [
{
universalIdentifier: idFieldUniversalIdentifier,
type: FieldMetadataType.UUID,
name: 'id',
label: 'Id',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.DATE_TIME,
name: 'createdAt',
label: 'Creation date',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
label: 'Last update',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.DATE_TIME,
name: 'deletedAt',
label: 'Deleted at',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.ACTOR,
name: 'createdBy',
label: 'Created by',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.ACTOR,
name: 'updatedBy',
label: 'Updated by',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.POSITION,
name: 'position',
label: 'Position',
},
{
universalIdentifier: uuidv4(),
type: FieldMetadataType.TS_VECTOR,
name: 'searchVector',
label: 'Search vector',
},
...additionalFields,
],
};
};
@@ -0,0 +1,42 @@
import { createOneApplication } from 'test/integration/metadata/suites/application/utils/create-one-application.util';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
export const setupApplicationForSync = async ({
applicationUniversalIdentifier,
name,
description,
sourcePath,
}: {
applicationUniversalIdentifier: string;
name: string;
description: string;
sourcePath: string;
}) => {
await createOneApplication({
universalIdentifier: applicationUniversalIdentifier,
name,
description,
version: '1.0.0',
sourcePath,
expectToFail: false,
});
// File upload uses multipart which requires real timers
jest.useRealTimers();
const packageJson = JSON.stringify({
name: sourcePath,
version: '1.0.0',
});
await uploadApplicationFile({
applicationUniversalIdentifier,
fileFolder: 'Dependencies',
filePath: 'package.json',
fileBuffer: Buffer.from(packageJson),
filename: 'package.json',
expectToFail: false,
});
jest.useFakeTimers();
};

Some files were not shown because too many files have changed in this diff Show More