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:
@@ -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 = {
|
||||
|
||||
+1
@@ -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',
|
||||
}
|
||||
|
||||
+1
-1
@@ -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<
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ export type AssignTypeIfIsRelationFieldMetadataType<
|
||||
? Ttype
|
||||
: T extends FieldMetadataType.MORPH_RELATION
|
||||
? Ttype
|
||||
: never;
|
||||
: never | null;
|
||||
|
||||
+55
-44
@@ -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
-1
@@ -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[],
|
||||
|
||||
+5
-5
@@ -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,
|
||||
};
|
||||
};
|
||||
+3
-3
@@ -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> =
|
||||
+5
-7
@@ -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,
|
||||
+77
@@ -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
-1
@@ -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>,
|
||||
+34
@@ -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
-1
@@ -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'>
|
||||
+15
@@ -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;
|
||||
};
|
||||
+5
-2
@@ -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
-1
@@ -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'>
|
||||
+11
-2
@@ -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
|
||||
+5
-2
@@ -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,
|
||||
+28
@@ -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,
|
||||
),
|
||||
};
|
||||
};
|
||||
+21
@@ -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,
|
||||
});
|
||||
};
|
||||
+21
@@ -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);
|
||||
});
|
||||
};
|
||||
+64
@@ -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);
|
||||
};
|
||||
+68
-1
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+6
@@ -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,
|
||||
|
||||
+50
-1
@@ -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,
|
||||
);
|
||||
|
||||
+233
@@ -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;
|
||||
};
|
||||
|
||||
+1
@@ -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(),
|
||||
|
||||
+5
@@ -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
-1
@@ -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[];
|
||||
|
||||
+38
-38
@@ -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,
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+3
-3
@@ -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[] =
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type ToMerge<T, K extends 'object' | 'field'> = {
|
||||
[P in ['toMerge', 'dest'][number] as `${P}Flat${Capitalize<K>}Metadatas`]: T;
|
||||
};
|
||||
+5
-4
@@ -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
-1
@@ -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';
|
||||
|
||||
+4
-3
@@ -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 = {
|
||||
|
||||
+4
-3
@@ -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,
|
||||
|
||||
+4
-3
@@ -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
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
-1
@@ -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
-1
@@ -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,
|
||||
|
||||
+3
-2
@@ -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
-1
@@ -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
-1
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
-3
@@ -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>;
|
||||
+2
-3
@@ -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;
|
||||
};
|
||||
+2
-2
@@ -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;
|
||||
Reference in New Issue
Block a user