Object metadata API create one using workspace migration v2 (#13420)

# Introduction
In this PR we create basic transpilation methods and utils to handle
input to flat, entity to flat, object maps to flat. In order to
transpile everything into a common validation that will be implemented
in another PR

## FieldMetadataEntity typing
Added `never | null` to fields that should never be in order to ease
general abstracted method to pass null, as anw it's what is in the
database

## Todo
- ~~Create a feature flag~~
- Integration test for object creation through metadata api + pg col
introspection and snapshoting
This commit is contained in:
Paul Rastoin
2025-07-29 17:47:28 +02:00
committed by GitHub
parent 600df4fd90
commit c1bf0a1fbf
66 changed files with 1224 additions and 164 deletions
@@ -757,7 +757,8 @@ export enum FeatureFlagKey {
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED'
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
}
export type Field = {
@@ -721,7 +721,8 @@ export enum FeatureFlagKey {
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_FILTERING_ENABLED = 'IS_WORKFLOW_FILTERING_ENABLED',
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED'
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
}
export type Field = {
@@ -13,4 +13,5 @@ export enum FeatureFlagKey {
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED',
}
@@ -76,7 +76,7 @@ export type FieldMetadataDefaultValue<
? FieldMetadataDefaultValueForAnyType | null // Could be improved to be | unknown
: T extends keyof FieldMetadataDefaultValueMapping
? FieldMetadataDefaultValueForType<T>
: never;
: never | null;
type FieldMetadataDefaultValueExtractedTypes = {
[K in keyof FieldMetadataDefaultValueMapping]: ExtractValueType<
@@ -18,4 +18,4 @@ export type FieldMetadataOptions<
? null | (FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]) // Could be improved to be | unknown
: T extends keyof FieldMetadataOptionsMapping
? FieldMetadataOptionsMapping[T]
: never;
: never | null;
@@ -60,4 +60,4 @@ export type FieldMetadataSettings<
? null | AllFieldMetadataSettings // Could be improved to be | unknown
: T extends keyof FieldMetadataSettingsMapping
? FieldMetadataSettingsMapping[T] | null
: never;
: never | null;
@@ -10,4 +10,4 @@ export type AssignTypeIfIsRelationFieldMetadataType<
? Ttype
: T extends FieldMetadataType.MORPH_RELATION
? Ttype
: never;
: never | null;
@@ -2,6 +2,10 @@ import { Expect, HasAllProperties } from 'twenty-shared/testing';
import { FieldMetadataType, NullablePartial } from 'twenty-shared/types';
import { Relation as TypeOrmRelation } from 'typeorm';
import {
FieldMetadataDefaultValueForAnyType,
FieldMetadataDefaultValueForType,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import {
AllFieldMetadataSettings,
FieldMetadataDateSettings,
@@ -10,10 +14,6 @@ import {
FieldMetadataRelationSettings,
FieldMetadataTextSettings,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import {
FieldMetadataDefaultValueForAnyType,
FieldMetadataDefaultValueForType,
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import {
FieldMetadataComplexOption,
@@ -30,10 +30,10 @@ type DefinedRelationRecord = {
};
type NotDefinedRelationRecord = {
relationTargetFieldMetadataId: never;
relationTargetFieldMetadata: never;
relationTargetObjectMetadataId: never;
relationTargetObjectMetadata: never;
relationTargetFieldMetadataId: never | null;
relationTargetFieldMetadata: never | null;
relationTargetObjectMetadataId: never | null;
relationTargetObjectMetadata: never | null;
};
type AbstractFieldMetadata = FieldMetadataEntity<FieldMetadataType>;
@@ -115,23 +115,27 @@ type RelationAssertions = [
>,
];
type NotDefinedSettings = {
settings: never | null;
};
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type SettingsAssertions = [
Expect<HasAllProperties<CurrencyFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<FullNameFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<RatingFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<SelectFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<MultiSelectFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<PositionFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<RawJsonFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<RichTextFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<ActorFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<ArrayFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<PhonesFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<EmailsFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<LinksFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<UUIDFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<BooleanFieldMetadata, { settings: never }>>,
Expect<HasAllProperties<CurrencyFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<FullNameFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<RatingFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<SelectFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<MultiSelectFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<RichTextFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<ArrayFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<PhonesFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<EmailsFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<LinksFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<UUIDFieldMetadata, NotDefinedSettings>>,
Expect<HasAllProperties<BooleanFieldMetadata, NotDefinedSettings>>,
Expect<
HasAllProperties<
@@ -328,8 +332,12 @@ type DefaultValueAssertions = [
>
>,
Expect<HasAllProperties<RelationFieldMetadata, { defaultValue: never }>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, { defaultValue: never }>>,
Expect<
HasAllProperties<RelationFieldMetadata, { defaultValue: never | null }>
>,
Expect<
HasAllProperties<MorphRelationFieldMetadata, { defaultValue: never | null }>
>,
Expect<
HasAllProperties<
@@ -339,6 +347,10 @@ type DefaultValueAssertions = [
>,
];
type NotDefinedOptions = {
options: never | null;
};
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
type OptionsAssertions = [
Expect<
@@ -360,25 +372,24 @@ type OptionsAssertions = [
>
>,
Expect<HasAllProperties<UUIDFieldMetadata, { options: never }>>,
Expect<HasAllProperties<TextFieldMetadata, { options: never }>>,
Expect<HasAllProperties<NumberFieldMetadata, { options: never }>>,
Expect<HasAllProperties<BooleanFieldMetadata, { options: never }>>,
Expect<HasAllProperties<DateFieldMetadata, { options: never }>>,
Expect<HasAllProperties<DateTimeFieldMetadata, { options: never }>>,
Expect<HasAllProperties<CurrencyFieldMetadata, { options: never }>>,
Expect<HasAllProperties<FullNameFieldMetadata, { options: never }>>,
Expect<HasAllProperties<PositionFieldMetadata, { options: never }>>,
Expect<HasAllProperties<RawJsonFieldMetadata, { options: never }>>,
Expect<HasAllProperties<RichTextFieldMetadata, { options: never }>>,
Expect<HasAllProperties<ActorFieldMetadata, { options: never }>>,
Expect<HasAllProperties<ArrayFieldMetadata, { options: never }>>,
Expect<HasAllProperties<PhonesFieldMetadata, { options: never }>>,
Expect<HasAllProperties<EmailsFieldMetadata, { options: never }>>,
Expect<HasAllProperties<LinksFieldMetadata, { options: never }>>,
Expect<HasAllProperties<RelationFieldMetadata, { options: never }>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, { options: never }>>,
Expect<HasAllProperties<UUIDFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<TextFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<NumberFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<BooleanFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<DateFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<DateTimeFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<CurrencyFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<FullNameFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<PositionFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<RawJsonFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<RichTextFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<ActorFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<ArrayFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<PhonesFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<EmailsFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<LinksFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<RelationFieldMetadata, NotDefinedOptions>>,
Expect<HasAllProperties<MorphRelationFieldMetadata, NotDefinedOptions>>,
Expect<
HasAllProperties<
@@ -1,11 +1,11 @@
import { v4 } from 'uuid';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import {
FieldMetadataComplexOption,
FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'src/utils/trim-and-remove-duplicated-whitespaces-from-object-string-properties';
export const prepareCustomFieldMetadataOptions = (
options: FieldMetadataDefaultOption[] | FieldMetadataComplexOption[],
@@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import { FieldMetadataType } from 'twenty-shared/types';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
type FlatFieldMetadataOverrides<
T extends FieldMetadataType = FieldMetadataType,
@@ -35,10 +35,10 @@ export const getFlatFieldMetadataMock = <T extends FieldMetadataType>(
standardOverrides: null,
updatedAt: createdAt,
workspaceId: faker.string.uuid(),
flatRelationTargetFieldMetadata: undefined as never,
relationTargetFieldMetadataId: undefined as never,
flatRelationTargetObjectMetadata: undefined as never,
relationTargetObjectMetadataId: undefined as never,
flatRelationTargetFieldMetadata: null,
relationTargetFieldMetadataId: null,
flatRelationTargetObjectMetadata: null,
relationTargetObjectMetadataId: null,
...overrides,
};
};
@@ -2,9 +2,9 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { AssignTypeIfIsRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/assign-type-if-is-relation-field-metadata-type.type';
import { FlatObjectMetadataWithoutFields } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FlatObjectMetadataWithoutFields } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
const fieldMetadataRelationProperties = [
export const fieldMetadataRelationProperties = [
'relationTargetFieldMetadata',
'relationTargetObjectMetadata',
'fieldPermissions',
@@ -12,7 +12,7 @@ const fieldMetadataRelationProperties = [
'object',
] as const satisfies (keyof FieldMetadataEntity)[];
type FieldMetadataEntityRelationProperties =
export type FieldMetadataEntityRelationProperties =
(typeof fieldMetadataRelationProperties)[number];
export type FlatFieldMetadata<T extends FieldMetadataType = FieldMetadataType> =
@@ -1,10 +1,9 @@
import diff from 'microdiff';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
@@ -20,10 +19,6 @@ const flatFieldMetadataPropertiesToCompare = [
'options',
'standardOverrides',
'settings',
// To reactivate once we authorize relation edition, see https://github.com/twentyhq/twenty/commit/39f6f3c4bb101272a9014e142a842d0801a3c33b
// 'relationTargetFieldMetadataId',
// 'relationTargetObjectMetadataId',
///
] as const satisfies (keyof FlatFieldMetadata)[];
export type FlatFieldMetadataPropertiesToCompare =
@@ -47,6 +42,9 @@ const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
};
type GetWorkspaceMigrationUpdateFieldActionArgs = FromTo<FlatFieldMetadata>;
/**
* This comparator handles update on colliding uniqueIdentifier flatFieldMetadata
*/
export const compareTwoFlatFieldMetadata = ({
from,
to,
@@ -0,0 +1,77 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
FlatFieldMetadata,
fieldMetadataRelationProperties,
} from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { fromFlatObjectMetadataToFlatObjectMetadataWithoutFields } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/from-flat-object-metadata-to-flat-object-metadata-without-fields.util';
export const fromFieldMetadataEntityToFlatFieldMetadata = <
T extends FieldMetadataType,
>(
fieldMetadataEntity: FieldMetadataEntity<T>,
/**
* private depth bottleneck
*/
_depth?: number,
// This is intended to be abstract
): FlatFieldMetadata => {
if (isDefined(_depth) && _depth > 1) {
throw new Error(
'fromFieldMetadataEntityToFlatFieldMetadata entering a possible infinite loop',
);
}
const fieldMetadataWithoutRelations = removePropertiesFromRecord(
fieldMetadataEntity,
fieldMetadataRelationProperties,
);
if (
isFieldMetadataEntityOfType(
fieldMetadataEntity,
FieldMetadataType.RELATION,
) ||
isFieldMetadataEntityOfType(
fieldMetadataEntity,
FieldMetadataType.MORPH_RELATION,
)
) {
const newDepth = isDefined(_depth) ? _depth + 1 : 1;
const flatRelationTargetFieldMetadata =
fromFieldMetadataEntityToFlatFieldMetadata(
fieldMetadataEntity.relationTargetFieldMetadata,
newDepth,
);
const flatObjectTargetFieldMetadata =
fromObjectMetadataEntityToFlatObjectMetadata(
fieldMetadataEntity.relationTargetObjectMetadata,
);
const flatRelationTargetObjectMetadata =
fromFlatObjectMetadataToFlatObjectMetadataWithoutFields(
flatObjectTargetFieldMetadata,
);
return {
...fieldMetadataWithoutRelations,
uniqueIdentifier:
fieldMetadataWithoutRelations.standardId ??
fieldMetadataWithoutRelations.id,
flatRelationTargetFieldMetadata,
flatRelationTargetObjectMetadata,
} as FlatFieldMetadata<FieldMetadataType.RELATION>;
}
return {
...fieldMetadataWithoutRelations,
uniqueIdentifier:
fieldMetadataWithoutRelations.standardId ??
fieldMetadataWithoutRelations.id,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
};
};
@@ -1,6 +1,6 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
export function isFlatFieldMetadataEntityOfType<
Field extends FlatFieldMetadata<FieldMetadataType>,
@@ -0,0 +1,34 @@
import {
deepMerge,
fromArrayToUniqueKeyRecord,
isDefined,
} from 'twenty-shared/utils';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { ToMerge } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/to-merge.type';
export const mergeTwoFlatFieldMetadatas = ({
destFlatFieldMetadatas,
toMergeFlatFieldMetadatas,
}: ToMerge<FlatFieldMetadata[], 'field'>): FlatFieldMetadata[] => {
const initialRecordAccumulator = fromArrayToUniqueKeyRecord({
array: destFlatFieldMetadatas,
uniqueKey: 'uniqueIdentifier',
});
const mergedUniqueIdentifierFlatFieldMetadataRecord =
toMergeFlatFieldMetadatas.reduce((acc, flatFieldToMerge) => {
const fieldUniqueIdentifier = flatFieldToMerge.uniqueIdentifier;
const destFieldMetadata: FlatFieldMetadata | undefined =
acc[fieldUniqueIdentifier];
return {
...acc,
[fieldUniqueIdentifier]: isDefined(destFieldMetadata)
? deepMerge(destFieldMetadata, flatFieldToMerge)
: flatFieldToMerge,
};
}, initialRecordAccumulator);
return Object.values(mergedUniqueIdentifierFlatFieldMetadataRecord);
};
@@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
type FlatIndexMetadataOverrides = Required<
Pick<FlatIndexMetadata, 'uniqueIdentifier' | 'objectMetadataId'>
@@ -0,0 +1,15 @@
import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
type IndexFieldMetadataEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
IndexFieldMetadataEntity,
MetadataEntitiesRelationTarget
>;
export type FlatIndexFieldMetadata = Partial<
Omit<IndexFieldMetadataEntity, IndexFieldMetadataEntityRelationProperties>
> & {
uniqueIdentifier: string;
};
@@ -1,7 +1,7 @@
import diff from 'microdiff';
import { FromTo } from 'twenty-shared/types';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const flatIndexMetadataPropertiesToCompare = [
@@ -16,6 +16,9 @@ type FlatIndexMetadataPropertiesToCompare =
(typeof flatIndexMetadataPropertiesToCompare)[number];
// Should also handle indexFieldMetadata comparison ?
/**
* This comparator handles update on colliding uniqueIdentifier flatIndexMetadata
*/
export const compareTwoFlatIndexMetadata = ({
from,
to,
@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
type FlatObjectMetadataOverrides = Required<
Pick<FlatObjectMetadata, 'uniqueIdentifier'>
@@ -1,9 +1,18 @@
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
export const objectMetadataEntityRelationProperties = [
'fields',
'indexMetadatas',
'targetRelationFields',
'dataSource',
'objectPermissions',
'fieldPermissions',
] as const satisfies ObjectMetadataRelationProperties[];
type ObjectMetadataRelationProperties = ExtractRecordTypeOrmRelationProperties<
ObjectMetadataEntity,
MetadataEntitiesRelationTarget
@@ -1,9 +1,9 @@
import omit from 'lodash.omit';
import diff from 'microdiff';
import { FromTo } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-object-action-v2';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
@@ -22,6 +22,9 @@ const flatObjectMetadataPropertiesToCompare = [
export type FlatObjectMetadataPropertiesToCompare =
(typeof flatObjectMetadataPropertiesToCompare)[number];
/**
* This comparator handles update on colliding uniqueIdentifier flatObjectdMetadata
*/
export const compareTwoFlatObjectMetadata = ({
from,
to,
@@ -0,0 +1,28 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { fromFieldMetadataEntityToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util';
import {
FlatObjectMetadata,
objectMetadataEntityRelationProperties,
} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export const fromObjectMetadataEntityToFlatObjectMetadata = (
objectMetadataEntity: ObjectMetadataEntity,
): FlatObjectMetadata => {
const objectMetadataEntityWithoutRelations = removePropertiesFromRecord(
objectMetadataEntity,
objectMetadataEntityRelationProperties,
);
return {
...objectMetadataEntityWithoutRelations,
flatIndexMetadatas: [], // TODO prastoin handle indexes
uniqueIdentifier:
objectMetadataEntityWithoutRelations.standardId ??
objectMetadataEntityWithoutRelations.id,
flatFieldMetadatas: objectMetadataEntity.fields.map(
fromFieldMetadataEntityToFlatFieldMetadata,
),
};
};
@@ -0,0 +1,21 @@
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata = (
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): FlatObjectMetadata => {
const {
fieldsById,
fieldIdByJoinColumnName: _fieldIdByJoinColumnName,
fieldIdByName: _fieldIdByName,
indexMetadatas,
...rest
} = objectMetadataItemWithFieldMaps;
return fromObjectMetadataEntityToFlatObjectMetadata({
...rest,
fields: Object.values(fieldsById),
indexMetadatas,
});
};
@@ -0,0 +1,21 @@
import { isDefined } from 'twenty-shared/utils';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const fromObjectMetadataMapsToFlatObjectMetadatas = (
objectMetadataMaps: ObjectMetadataMaps,
): FlatObjectMetadata[] => {
const objectMetadataIds = Object.keys(objectMetadataMaps.byId);
return objectMetadataIds.flatMap<FlatObjectMetadata>((objectMetadataId) => {
const occurrence = objectMetadataMaps.byId[objectMetadataId];
if (!isDefined(occurrence)) {
return [];
}
return fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata(occurrence);
});
};
@@ -0,0 +1,64 @@
import {
deepMerge,
fromArrayToUniqueKeyRecord,
isDefined,
} from 'twenty-shared/utils';
import { mergeTwoFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-field-metadata/utils/merge-two-flat-field-metadatas.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { ToMerge } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/to-merge.type';
export const mergeTwoFlatFieldObjectMetadatas = ({
destFlatObjectMetadatas,
toMergeFlatObjectMetadatas,
}: ToMerge<FlatObjectMetadata[], 'object'>): FlatObjectMetadata[] => {
const initialObjectAccumulator = fromArrayToUniqueKeyRecord({
array: destFlatObjectMetadatas,
uniqueKey: 'uniqueIdentifier',
});
const mergedUniqueIdentifierFlatObjectMetadataRecord =
toMergeFlatObjectMetadatas.reduce<Record<string, FlatObjectMetadata>>(
(acc, toMergeFlatObjectMetadata) => {
const flatObjectUniqueIdentifier =
toMergeFlatObjectMetadata.uniqueIdentifier;
const accumulatorCurrentOccurrence: FlatObjectMetadata | undefined =
initialObjectAccumulator[flatObjectUniqueIdentifier];
if (!isDefined(accumulatorCurrentOccurrence)) {
return {
...acc,
[flatObjectUniqueIdentifier]: toMergeFlatObjectMetadata,
};
}
const {
flatFieldMetadatas: destFlatFieldMetadatas,
...destFlatObjectMetadataRest
} = accumulatorCurrentOccurrence;
const {
flatFieldMetadatas: toMergeFlatFieldMetadatas,
...toMergeFlatObjectMetadataRest
} = toMergeFlatObjectMetadata;
const mergedFlatObjectMetadata: FlatObjectMetadata = {
...deepMerge(
destFlatObjectMetadataRest,
toMergeFlatObjectMetadataRest,
),
// TODO prastoin handle indexes
flatFieldMetadatas: mergeTwoFlatFieldMetadatas({
destFlatFieldMetadatas,
toMergeFlatFieldMetadatas,
}),
};
return {
...acc,
[flatObjectUniqueIdentifier]: mergedFlatObjectMetadata,
};
},
initialObjectAccumulator,
);
return Object.values(mergedUniqueIdentifierFlatObjectMetadataRecord);
};
@@ -4,11 +4,16 @@ import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { FieldMetadataType } from 'twenty-shared/types';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
import { buildDefaultFlatFieldMetadataForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/build-default-fields-for-custom-object.util';
@InputType()
@BeforeCreateOne(BeforeCreateOneObject)
@@ -71,6 +76,68 @@ export class CreateObjectInput {
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
@Field({ nullable: true }) // Not nullable to me
isLabelSyncedWithName?: boolean;
}
export const fromCreateObjectInputToFlatObjectMetadata = (
rawCreateObjectInput: CreateObjectInput,
): FlatObjectMetadata => {
if (rawCreateObjectInput.isRemote) {
throw new UserInputError('Remote objects are not supported yet');
}
const createObjectInput =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawCreateObjectInput,
[
'description',
'icon',
'labelPlural',
'labelSingular',
'namePlural',
'nameSingular',
'shortcut',
],
);
const objectMetadataId = v4();
const createdAt = new Date();
const baseCustomFlatFieldMetadatas =
buildDefaultFlatFieldMetadataForCustomObject({
createdAt,
objectMetadataId,
workspaceId: createObjectInput.workspaceId,
});
return {
createdAt,
updatedAt: createdAt,
dataSourceId: createObjectInput.dataSourceId, // TODO is it enough ?
description: createObjectInput.description ?? null,
duplicateCriteria: [], // TODO is it enough ?
flatFieldMetadatas: Object.values(baseCustomFlatFieldMetadatas),
flatIndexMetadatas: [], // TODO is it enough ?
icon: createObjectInput.icon ?? null,
id: objectMetadataId,
imageIdentifierFieldMetadataId: null,
isActive: true,
isAuditLogged: true,
isCustom: true,
isLabelSyncedWithName: createObjectInput.isLabelSyncedWithName ?? false,
isRemote: false,
isSearchable: true,
isSystem: false,
labelIdentifierFieldMetadataId: baseCustomFlatFieldMetadatas.nameField.id,
labelPlural: createObjectInput.labelPlural ?? null,
labelSingular: createObjectInput.labelSingular ?? null,
namePlural: createObjectInput.namePlural ?? null,
nameSingular: createObjectInput.nameSingular ?? null,
shortcut: createObjectInput.shortcut ?? null,
standardId: null,
standardOverrides: null,
uniqueIdentifier: objectMetadataId,
targetTableName: 'DEPRECATED',
workspaceId: createObjectInput.workspaceId,
};
};
@@ -10,6 +10,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -33,6 +34,8 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module';
import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module';
import { ObjectMetadataEntity } from './object-metadata.entity';
import { ObjectMetadataService } from './object-metadata.service';
@@ -63,6 +66,9 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
WorkspaceDataSourceModule,
FeatureFlagModule,
WorkspaceMigrationBuilderV2Module,
WorkspaceMigrationRunnerV2Module,
],
services: [
ObjectMetadataService,
@@ -13,8 +13,12 @@ import {
Repository,
} from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { fromObjectMetadataMapsToFlatObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-maps-to-flat-object-metadatas.util';
import { mergeTwoFlatFieldObjectMetadatas } from 'src/engine/metadata-modules/flat-object-metadata/utils/merge-two-flat-object-metadatas.util';
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
import {
@@ -46,12 +50,17 @@ import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service';
import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.service';
import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { isSearchableFieldType } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util';
import { ObjectMetadataEntity } from './object-metadata.entity';
import { CreateObjectInput } from './dtos/create-object.input';
import {
CreateObjectInput,
fromCreateObjectInputToFlatObjectMetadata,
} from './dtos/create-object.input';
@Injectable()
export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEntity> {
@@ -70,6 +79,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private readonly indexMetadataService: IndexMetadataService,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly workspaceMigrationBuilderV2: WorkspaceMigrationBuilderV2Service,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceMigrationRunnerV2Service: WorkspaceMigrationRunnerV2Service,
) {
super(objectMetadataRepository);
}
@@ -116,6 +128,43 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.workspaceId,
);
const isWorkspaceMigrationV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
objectMetadataInput.workspaceId,
);
if (isWorkspaceMigrationV2Enabled) {
const createdRawFlatObjectMetadata =
fromCreateObjectInputToFlatObjectMetadata(objectMetadataInput);
const existingFlatObjectMetadatas =
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps);
// @ts-expect-error TODO implement validateFlatObjectMetadataData
const createdFlatObjectMetadata = validateFlatObjectMetadataData({
existing:
// Here we assume that EVERYTHING is in cache and up to date, this is very critical, also race condition prone :thinking:
fromObjectMetadataMapsToFlatObjectMetadatas(objectMetadataMaps),
toValidate: [createdRawFlatObjectMetadata],
});
const workpsaceMigration = this.workspaceMigrationBuilderV2.build({
objectMetadataFromToInputs: {
from: existingFlatObjectMetadatas,
to: mergeTwoFlatFieldObjectMetadatas({
destFlatObjectMetadatas: existingFlatObjectMetadatas,
toMergeFlatObjectMetadatas: [createdFlatObjectMetadata],
}),
},
workspaceId: objectMetadataInput.workspaceId, // Where does this comes from ?
});
await this.workspaceMigrationRunnerV2Service.run(workpsaceMigration);
// What to return exactly ? We now won't have access to the entity directly
// We could still retrieve it afterwards using a find on object metadata id or return a flat now
return createdFlatObjectMetadata;
}
objectMetadataInput.labelSingular = capitalize(
objectMetadataInput.labelSingular,
);
@@ -2,6 +2,7 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import {
BASE_OBJECT_STANDARD_FIELD_IDS,
CUSTOM_OBJECT_STANDARD_FIELD_IDS,
@@ -114,3 +115,235 @@ export const buildDefaultFieldsForCustomObject = (
defaultValue: 0,
},
];
type BuildDefaultFlatFieldMetadataForCustomObjectArgs = {
createdAt: Date;
workspaceId: string;
objectMetadataId: string;
};
export const buildDefaultFlatFieldMetadataForCustomObject = ({
createdAt,
workspaceId,
objectMetadataId,
}: BuildDefaultFlatFieldMetadataForCustomObjectArgs) => {
const idField: FlatFieldMetadata<FieldMetadataType.UUID> = {
type: FieldMetadataType.UUID,
createdAt,
id: v4(),
isLabelSyncedWithName: false, // TO CHECK
isUnique: true, // Was false before but unsure if normal ?
objectMetadataId,
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.id,
updatedAt: createdAt,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.id,
name: 'id',
label: 'Id',
icon: 'Icon123',
description: 'Id',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: true,
defaultValue: 'uuid',
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
const nameField: FlatFieldMetadata<FieldMetadataType.TEXT> = {
type: FieldMetadataType.TEXT,
createdAt,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
updatedAt: createdAt,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
name: 'name',
label: 'Name',
icon: 'IconAbc',
description: 'Name',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
defaultValue: "'Untitled'",
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
const createdAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
createdAt,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
updatedAt: createdAt,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.createdAt,
name: 'createdAt',
label: 'Creation date',
icon: 'IconCalendar',
description: 'Creation date',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
defaultValue: 'now',
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
const updatedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
createdAt,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
updatedAt: createdAt,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
name: 'updatedAt',
label: 'Last update',
icon: 'IconCalendarClock',
description: 'Last time the record was changed',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
defaultValue: 'now',
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
const deletedAtField: FlatFieldMetadata<FieldMetadataType.DATE_TIME> = {
type: FieldMetadataType.DATE_TIME,
createdAt,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
updatedAt: createdAt,
workspaceId,
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
name: 'deletedAt',
label: 'Deleted at',
icon: 'IconCalendarClock',
description: 'Deletion date',
isNullable: true,
isActive: true,
isCustom: false,
isSystem: false,
defaultValue: null,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
const createdByField: FlatFieldMetadata<FieldMetadataType.ACTOR> = {
type: FieldMetadataType.ACTOR,
createdAt,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
updatedAt: createdAt,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
name: 'createdBy',
label: 'Created by',
icon: 'IconCreativeCommonsSa',
description: 'The creator of the record',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: false,
defaultValue: { name: "''", source: "'MANUAL'" },
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
const positionField: FlatFieldMetadata<FieldMetadataType.POSITION> = {
type: FieldMetadataType.POSITION,
createdAt,
id: v4(),
isLabelSyncedWithName: false,
isUnique: false,
objectMetadataId,
uniqueIdentifier: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
updatedAt: createdAt,
workspaceId,
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.position,
name: 'position',
label: 'Position',
icon: 'IconHierarchy2',
description: 'Position',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: true,
defaultValue: 0,
flatRelationTargetFieldMetadata: null,
flatRelationTargetObjectMetadata: null,
options: null,
standardOverrides: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadataId: null,
settings: null,
};
return {
idField,
nameField,
createdAtField,
updatedAtField,
deletedAtField,
createdByField,
positionField,
} as const;
};
@@ -112,6 +112,7 @@ describe('WorkspaceEntityManager', () => {
IS_FIELDS_PERMISSIONS_ENABLED: false,
IS_CORE_VIEW_SYNCING_ENABLED: false,
IS_TWO_FACTOR_AUTHENTICATION_ENABLED: false,
IS_WORKSPACE_MIGRATION_V2_ENABLED: false,
},
eventEmitterService: {
emitMutationEvent: jest.fn(),
@@ -60,6 +60,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
workspaceId: workspaceId,
value: false,
},
])
.execute();
};
@@ -1,4 +1,4 @@
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FromTo } from 'twenty-shared/types';
export type DeletedCreatedUpdatedMatrix<T> = {
created: T[];
@@ -9,8 +9,8 @@ exports[`Workspace migration builder field actions test suite It should build a
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -23,8 +23,8 @@ exports[`Workspace migration builder field actions test suite It should build a
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -73,8 +73,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -87,8 +87,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -141,8 +141,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -155,8 +155,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -257,8 +257,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "new description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "new icon",
"id": Any<String>,
"isActive": false,
@@ -271,8 +271,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"name": "new name",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -344,8 +344,8 @@ exports[`Workspace migration builder field actions test suite It should build an
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "new description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": false,
@@ -552,8 +552,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -566,8 +566,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -611,8 +611,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -625,8 +625,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -670,8 +670,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -684,8 +684,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -729,8 +729,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -743,8 +743,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -788,8 +788,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"createdAt": Any<ClockDate>,
"defaultValue": null,
"description": "default flat field metadata description",
"flatRelationTargetFieldMetadata": undefined,
"flatRelationTargetObjectMetadata": undefined,
"flatRelationTargetFieldMetadata": null,
"flatRelationTargetObjectMetadata": null,
"icon": "icon",
"id": Any<String>,
"isActive": true,
@@ -802,8 +802,8 @@ exports[`Workspace migration builder object actions test suite It should build a
"name": "flatFieldMetadataName",
"objectMetadataId": Any<String>,
"options": null,
"relationTargetFieldMetadataId": undefined,
"relationTargetObjectMetadataId": undefined,
"relationTargetFieldMetadataId": null,
"relationTargetObjectMetadataId": null,
"settings": null,
"standardId": null,
"standardOverrides": null,
@@ -3,8 +3,8 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { getFlatFieldMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-object-metadata.mock';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
const basicObjectMetadataId = faker.string.uuid();
@@ -1,7 +1,7 @@
import { faker } from '@faker-js/faker';
import { getFlatIndexMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-index-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-object-metadata.mock';
import { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
// Should test more things such as flatFieldIndex diffing
@@ -1,9 +1,9 @@
import { faker } from '@faker-js/faker';
import { FieldMetadataType } from 'twenty-shared/types';
import { getFlatFieldMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-field-metadata.mock';
import { getFlatIndexMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-index-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/workspace-manager/workspace-migration-v2/__tests__/get-flat-object-metadata.mock';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { getFlatIndexMetadataMock } from 'src/engine/metadata-modules/flat-index-metadata/__mocks__/get-flat-index-metadata.mock';
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
import { WorkspaceMigrationBuilderTestCase } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/types/workspace-migration-builder-test-case.type';
export const WORKSPACE_MIGRATION_OBJECT_BUILDER_TEST_CASES: WorkspaceMigrationBuilderTestCase[] =
@@ -0,0 +1,3 @@
export type ToMerge<T, K extends 'object' | 'field'> = {
[P in ['toMerge', 'dest'][number] as `${P}Flat${Capitalize<K>}Metadatas`]: T;
};
@@ -1,8 +1,9 @@
import { FromTo } from 'twenty-shared/types';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatObjectMetadataWithoutFields } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-field-metadata-comparator.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import { FlatObjectMetadataWithoutFields } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export type CreateFieldAction = {
type: 'create_field';
@@ -1,4 +1,4 @@
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
export type CreateIndexAction = {
type: 'create_index';
@@ -1,7 +1,8 @@
import { FromTo } from 'twenty-shared/types';
import { FlatObjectMetadataWithoutFields } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FlatObjectMetadataWithoutFields } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FlatObjectMetadataPropertiesToCompare } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-object-metadata-comparator.util';
import { CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
export type CreateObjectAction = {
@@ -1,6 +1,7 @@
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FromTo } from 'twenty-shared/types';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
CustomDeletedCreatedUpdatedMatrix,
deletedCreatedUpdatedMatrixDispatcher,
@@ -1,9 +1,10 @@
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { FromTo } from 'twenty-shared/types';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import {
FlatObjectMetadata,
FlatObjectMetadataWithoutFields,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
CustomDeletedCreatedUpdatedMatrix,
deletedCreatedUpdatedMatrixDispatcher,
@@ -1,7 +1,7 @@
import {
FlatObjectMetadata,
FlatObjectMetadataWithoutFields,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
export const fromFlatObjectMetadataToFlatObjectMetadataWithoutFields = ({
flatFieldMetadatas: _flatFieldMetadatas,
@@ -1,5 +1,5 @@
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
CreateFieldAction,
DeleteFieldAction,
@@ -1,4 +1,4 @@
import { FlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-metadata';
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
import {
CreateIndexAction,
DeleteIndexAction,
@@ -1,4 +1,4 @@
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { CreateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-field-action-v2';
import {
CreateObjectAction,
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { FromTo } from 'twenty-shared/types';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { deletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2';
import { computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-field-matrix.util';
@@ -1,4 +1,4 @@
import { compareTwoFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-field-metadata-comparator.util';
import { compareTwoFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import {
UpdateFieldAction,
WorkspaceMigrationFieldActionV2,
@@ -1,4 +1,4 @@
import { compareTwoFlatIndexMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-index-metadata-comparator.util';
import { compareTwoFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/compare-two-flat-index-metadata.util';
import { WorkspaceMigrationIndexActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-index-action-v2';
import { UpdatedObjectMetadataDeletedCreatedUpdatedIndexMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-index-matrix.util';
import {
@@ -1,6 +1,6 @@
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { compareTwoFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util';
import { CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/utils/deleted-created-updated-matrix-dispatcher.util';
import { compareTwoFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-object-metadata-comparator.util';
import {
UpdateObjectAction,
WorkspaceMigrationObjectActionV2,
@@ -1,8 +1,8 @@
import { In } from 'typeorm';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-field-metadata';
import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-field-metadata-comparator.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { FlatFieldMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-field-metadata/utils/compare-two-flat-field-metadata.util';
import {
CreateFieldAction,
DeleteFieldAction,
@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { FlatObjectMetadataPropertiesToCompare } from 'src/engine/metadata-modules/flat-object-metadata/utils/compare-two-flat-object-metadata.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-object-metadata';
import { FlatObjectMetadataPropertiesToCompare } from 'src/engine/workspace-manager/workspace-migration-v2/utils/flat-object-metadata-comparator.util';
import {
CreateObjectAction,
DeleteObjectAction,
@@ -28,10 +28,10 @@ export const getMockFieldMetadataEntity = <
isSystem: false,
isUnique: null,
object: {} as ObjectMetadataEntity,
relationTargetFieldMetadata: null as never,
relationTargetFieldMetadataId: null as never,
relationTargetObjectMetadata: null as never,
relationTargetObjectMetadataId: null as never,
relationTargetFieldMetadata: null,
relationTargetFieldMetadataId: null,
relationTargetObjectMetadata: null,
relationTargetObjectMetadataId: null,
standardId: null,
standardOverrides: null,
id: faker.string.uuid(),
@@ -10,6 +10,7 @@
export type { ConfigVariableValue } from './ConfigVariableValue';
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
export { FieldMetadataType } from './FieldMetadataType';
export type { FromTo } from './FromToType';
export type { IsExactly } from './IsExactly';
export type { NullablePartial } from './NullablePartial';
export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions';
@@ -0,0 +1,283 @@
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
import { deepMerge } from '@/utils';
type DeepMergeTestCase<T extends object> = {
source: Required<T>;
target: Required<T>;
expected: T;
};
describe('deepMerge', () => {
describe('primitive values', () => {
type PrimitiveValue = { value: string | number | boolean };
const primitiveTestCases: EachTestingContext<DeepMergeTestCase<PrimitiveValue>>[] = [
{
title: 'should override string values',
context: {
source: { value: 'hello' },
target: { value: 'world' },
expected: { value: 'world' },
},
},
{
title: 'should override number values',
context: {
source: { value: 42 },
target: { value: 24 },
expected: { value: 24 },
},
},
{
title: 'should override boolean values',
context: {
source: { value: true },
target: { value: false },
expected: { value: false },
},
},
];
it.each(primitiveTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
});
describe('null and undefined handling', () => {
type NullableValue = { value: string | null };
const nullTestCases: EachTestingContext<DeepMergeTestCase<NullableValue>>[] = [
{
title: 'should preserve null values from target',
context: {
source: { value: 'hello' },
target: { value: null },
expected: { value: null },
},
},
{
title: 'should ignore undefined values from target',
context: {
source: { value: 'hello' },
target: { value: undefined as unknown as null },
expected: { value: 'hello' },
},
},
{
title: 'should override null values from source',
context: {
source: { value: null },
target: { value: 'world' },
expected: { value: 'world' },
},
},
];
it.each(nullTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
type MixedNullValue = { a: number | null; b: number };
const mixedNullTestCase: EachTestingContext<DeepMergeTestCase<MixedNullValue>> = {
title: 'should handle mixed null and undefined values',
context: {
source: { a: 1, b: 2 },
target: { a: null, b: 2 },
expected: { a: null, b: 2 },
},
};
it(mixedNullTestCase.title, () => {
const { source, target, expected } = mixedNullTestCase.context;
expect(deepMerge(source, target)).toEqual(expected);
});
});
describe('array handling', () => {
type ArrayValue = { arr: Array<string | number> | null };
const arrayTestCases: EachTestingContext<DeepMergeTestCase<ArrayValue>>[] = [
{
title: 'should concatenate arrays',
context: {
source: { arr: [1, 2] },
target: { arr: [3, 4] },
expected: { arr: [1, 2, 3, 4] },
},
},
{
title: 'should handle empty target array',
context: {
source: { arr: [1, 2] },
target: { arr: [] },
expected: { arr: [1, 2] },
},
},
{
title: 'should handle empty source array',
context: {
source: { arr: [] },
target: { arr: [1, 2] },
expected: { arr: [1, 2] },
},
},
{
title: 'should handle null target array',
context: {
source: { arr: ['a', 'b'] },
target: { arr: null },
expected: { arr: null },
},
},
];
it.each(arrayTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
});
describe('nested objects', () => {
type NestedValue = {
nested: {
a: number;
b: number;
deep?: {
a: number;
b: number;
};
arr: Array<number>;
obj: {
a: number;
b: number;
};
} | null;
};
const nestedTestCases: EachTestingContext<DeepMergeTestCase<NestedValue>>[] = [
{
title: 'should merge nested objects',
context: {
source: { nested: { a: 1, b: 2, arr: [], obj: { a: 1, b: 2 } } },
target: { nested: { a: 1, b: 3, arr: [], obj: { a: 1, b: 2 } } },
expected: { nested: { a: 1, b: 3, arr: [], obj: { a: 1, b: 2 } } },
},
},
{
title: 'should merge deeply nested objects',
context: {
source: { nested: { a: 1, b: 2, deep: { a: 1, b: 2 }, arr: [], obj: { a: 1, b: 2 } } },
target: { nested: { a: 1, b: 2, deep: { a: 1, b: 3 }, arr: [], obj: { a: 1, b: 2 } } },
expected: { nested: { a: 1, b: 2, deep: { a: 1, b: 3 }, arr: [], obj: { a: 1, b: 2 } } },
},
},
{
title: 'should handle null nested object in target',
context: {
source: { nested: { a: 1, b: 2, arr: [], obj: { a: 1, b: 2 } } },
target: { nested: null },
expected: { nested: null },
},
},
{
title: 'should handle complex nested structures',
context: {
source: {
nested: {
a: 1,
b: 2,
arr: [1, 2],
obj: { a: 1, b: 2 },
},
},
target: {
nested: {
a: 1,
b: 2,
arr: [3, 4],
obj: { a: 1, b: 3 },
},
},
expected: {
nested: {
a: 1,
b: 2,
arr: [1, 2, 3, 4],
obj: { a: 1, b: 3 },
},
},
},
},
];
it.each(nestedTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
});
describe('edge cases', () => {
type EdgeValue = {
a: number | Date | RegExp;
b: number | Date | RegExp;
};
const edgeTestCases: EachTestingContext<DeepMergeTestCase<EdgeValue>>[] = [
{
title: 'should handle Date objects by replacing them',
context: {
source: { a: new Date('2023-01-01'), b: new Date('2023-01-01') },
target: { a: new Date('2023-12-31'), b: new Date('2023-12-31') },
expected: { a: new Date('2023-12-31'), b: new Date('2023-12-31') },
},
},
{
title: 'should handle RegExp objects by replacing them',
context: {
source: { a: /test1/, b: /test1/ },
target: { a: /test2/, b: /test2/ },
expected: { a: /test2/, b: /test2/ },
},
},
{
title: 'should handle mixed Date and RegExp objects',
context: {
source: { a: new Date('2023-01-01'), b: /test1/ },
target: { a: new Date('2023-12-31'), b: /test2/ },
expected: { a: new Date('2023-12-31'), b: /test2/ },
},
},
];
it.each(edgeTestCases)('$title', ({ context: { source, target, expected } }) => {
expect(deepMerge(source, target)).toEqual(expected);
});
type NestedDateValue = {
a: { value: Date };
b: { value: Date };
};
const nestedDateTestCase: EachTestingContext<DeepMergeTestCase<NestedDateValue>> = {
title: 'should handle Date objects in nested structures',
context: {
source: {
a: { value: new Date('2023-01-01') },
b: { value: new Date('2023-01-01') },
},
target: {
a: { value: new Date('2023-12-31') },
b: { value: new Date('2023-12-31') },
},
expected: {
a: { value: new Date('2023-12-31') },
b: { value: new Date('2023-12-31') },
},
},
};
it(nestedDateTestCase.title, () => {
const { source, target, expected } = nestedDateTestCase.context;
expect(deepMerge(source, target)).toEqual(expected);
});
});
});
@@ -1,6 +1,5 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'src/utils/trim-and-remove-duplicated-whitespaces-from-object-string-properties';
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from '../trim-and-remove-duplicated-whitespaces-from-object-string-properties';
type SanitizeObjectStringPropertiesTestCase = EachTestingContext<{
input: Record<string, any>;
@@ -1,6 +1,5 @@
import { EachTestingContext } from 'twenty-shared/testing';
import { trimAndRemoveDuplicatedWhitespacesFromString } from 'src/utils/trim-and-remove-duplicated-whitespaces-from-string';
import { EachTestingContext } from '@/testing/types/EachTestingContext.type';
import { trimAndRemoveDuplicatedWhitespacesFromString } from '../trim-and-remove-duplicated-whitespaces-from-string';
type TrimAndRemoveWhitespacesTestCase = EachTestingContext<{
input: string;
@@ -0,0 +1,79 @@
/**
* Deep merges two objects or arrays recursively
* - Objects are merged by combining their properties
* - Arrays are merged by concatenating them
* - Primitive values from target override source
* - Null values from target are preserved
* - Undefined values from target are ignored
* - Date and RegExp objects are treated as primitives (replaced, not merged)
*
* @param source The source object to merge from
* @param target The target object to merge into
* @returns A new merged object
*/
export const deepMerge = <T extends object>(
source: Required<T>,
target: Required<T>,
): T => {
// Handle null/undefined cases
if (!source) return target as T;
if (!target) return source;
// Create a new object to avoid mutations
const output = { ...source };
// Iterate through all keys in target
Object.keys(target).forEach((key) => {
const sourceValue = source[key as keyof T];
const targetValue = target[key as keyof T];
// Skip undefined values in target
if (targetValue === undefined) {
return;
}
// Handle null values - explicitly assign them
if (targetValue === null) {
output[key as keyof T] = null as T[keyof T];
return;
}
// Handle arrays - concatenate them
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
output[key as keyof T] = [...sourceValue, ...targetValue] as T[keyof T];
return;
}
// Handle Date and RegExp objects - treat them as primitives
if (
targetValue instanceof Date ||
targetValue instanceof RegExp ||
sourceValue instanceof Date ||
sourceValue instanceof RegExp
) {
output[key as keyof T] = targetValue as T[keyof T];
return;
}
// Handle nested objects - recurse
if (
sourceValue &&
targetValue &&
typeof sourceValue === 'object' &&
typeof targetValue === 'object' &&
!Array.isArray(sourceValue) &&
!Array.isArray(targetValue)
) {
output[key as keyof T] = deepMerge(
sourceValue as object,
targetValue as object,
) as T[keyof T];
return;
}
// For primitives
output[key as keyof T] = targetValue as T[keyof T];
});
return output;
};
@@ -0,0 +1,25 @@
import { StringPropertyKeys } from '@/utils/trim-and-remove-duplicated-whitespaces-from-object-string-properties';
import { isDefined } from '@/utils/validation';
export const fromArrayToUniqueKeyRecord = <T extends object>({
array,
uniqueKey,
}: {
array: T[];
uniqueKey: StringPropertyKeys<T>;
}) => {
return array.reduce<Record<string, T>>((acc, occurence) => {
const currentUniqueKey = occurence[uniqueKey] as string;
if (isDefined(acc[currentUniqueKey])) {
throw new Error(
`Should never occur, flat array contains twice the same unique key ${occurence[uniqueKey]}`,
);
}
return {
...acc,
[currentUniqueKey]: occurence,
};
}, {});
};
@@ -8,7 +8,9 @@
*/
export { assertUnreachable } from './assertUnreachable';
export { deepMerge } from './deepMerge';
export { isFieldMetadataDateKind } from './fieldMetadata/isFieldMetadataDateKind';
export { fromArrayToUniqueKeyRecord } from './from-array-to-unique-key-record.util';
export { getURLSafely } from './getURLSafely';
export { getImageAbsoluteURI } from './image/getImageAbsoluteURI';
export {
@@ -17,10 +19,14 @@ export {
} from './image/getLogoUrlFromDomainName';
export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields';
export { parseJson } from './parseJson';
export { removePropertiesFromRecord } from './removePropertiesFromRecord';
export { removeUndefinedFields } from './removeUndefinedFields';
export { getGenericOperationName } from './sentry/getGenericOperationName';
export { getHumanReadableNameFromCode } from './sentry/getHumanReadableNameFromCode';
export { capitalize } from './strings/capitalize';
export type { StringPropertyKeys } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties';
export { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from './trim-and-remove-duplicated-whitespaces-from-object-string-properties';
export { trimAndRemoveDuplicatedWhitespacesFromString } from './trim-and-remove-duplicated-whitespaces-from-string';
export { absoluteUrlSchema } from './url/absoluteUrlSchema';
export { buildSignedPath } from './url/buildSignedPath';
export { getAbsoluteUrl } from './url/getAbsoluteUrl';
@@ -0,0 +1,12 @@
export const removePropertiesFromRecord = <T, K extends keyof T>(
record: T,
keysToRemove: K[],
): Omit<T, K> => {
const result = { ...record };
for (const key of keysToRemove) {
delete result[key];
}
return result;
};
@@ -1,8 +1,8 @@
import { trimAndRemoveDuplicatedWhitespacesFromString } from 'src/utils/trim-and-remove-duplicated-whitespaces-from-string';
import { trimAndRemoveDuplicatedWhitespacesFromString } from '@/utils/trim-and-remove-duplicated-whitespaces-from-string';
type OnlyStringPropertiesKey<T> = Extract<keyof T, string>;
type StringPropertyKeys<T> = {
export type StringPropertyKeys<T> = {
[K in OnlyStringPropertiesKey<T>]: T[K] extends string | undefined
? K
: never;