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:
+56
@@ -0,0 +1,56 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type FlatObjectMetadataOverrides = Required<
|
||||
Pick<FlatObjectMetadata, 'uniqueIdentifier'>
|
||||
> &
|
||||
Partial<FlatObjectMetadata>;
|
||||
export const getFlatObjectMetadataMock = (
|
||||
overrides: FlatObjectMetadataOverrides,
|
||||
): FlatObjectMetadata => {
|
||||
const createdAt = faker.date.anytime();
|
||||
|
||||
return {
|
||||
flatFieldMetadatas: [],
|
||||
flatIndexMetadatas: [],
|
||||
createdAt,
|
||||
dataSourceId: faker.string.uuid(),
|
||||
description: 'default flat object metadata description',
|
||||
duplicateCriteria: [],
|
||||
icon: 'icon',
|
||||
id: faker.string.uuid(),
|
||||
imageIdentifierFieldMetadataId: faker.string.uuid(),
|
||||
isActive: true,
|
||||
isAuditLogged: true,
|
||||
isCustom: true,
|
||||
isLabelSyncedWithName: false,
|
||||
isRemote: false,
|
||||
isSearchable: true,
|
||||
isSystem: false,
|
||||
labelIdentifierFieldMetadataId: faker.string.uuid(),
|
||||
labelPlural: 'default flat object metadata label plural',
|
||||
labelSingular: 'default flat object metadata label singular',
|
||||
namePlural: 'defaultflatObjectMetadataNamePlural',
|
||||
nameSingular: 'defaultflatObjectMetadataNameSingular',
|
||||
shortcut: 'shortcut',
|
||||
standardId: null,
|
||||
standardOverrides: null,
|
||||
targetTableName: '',
|
||||
updatedAt: createdAt,
|
||||
workspaceId: faker.string.uuid(),
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStandardFlatObjectMetadataMock = (
|
||||
overrides: Omit<FlatObjectMetadataOverrides, 'isCustom' | 'isSystem'>,
|
||||
) => {
|
||||
return getFlatObjectMetadataMock({
|
||||
standardId: faker.string.uuid(),
|
||||
standardOverrides: {},
|
||||
isCustom: false,
|
||||
isSystem: true,
|
||||
...overrides,
|
||||
});
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
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 { 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
|
||||
>;
|
||||
|
||||
export type FlatObjectMetadata = Omit<
|
||||
ObjectMetadataEntity,
|
||||
ObjectMetadataRelationProperties
|
||||
> & {
|
||||
uniqueIdentifier: string;
|
||||
flatIndexMetadatas: FlatIndexMetadata[];
|
||||
flatFieldMetadatas: FlatFieldMetadata[];
|
||||
};
|
||||
|
||||
// Could be renamed
|
||||
export type FlatObjectMetadataWithoutFields = Omit<
|
||||
FlatObjectMetadata,
|
||||
'flatFieldMetadatas' | 'flatIndexMetadatas'
|
||||
>;
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
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/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';
|
||||
|
||||
const flatObjectMetadataPropertiesToCompare = [
|
||||
'description',
|
||||
'icon',
|
||||
'isActive',
|
||||
'isLabelSyncedWithName',
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
'namePlural',
|
||||
'nameSingular',
|
||||
'standardOverrides', // Only if standard
|
||||
] as const satisfies (keyof FlatObjectMetadata)[];
|
||||
|
||||
export type FlatObjectMetadataPropertiesToCompare =
|
||||
(typeof flatObjectMetadataPropertiesToCompare)[number];
|
||||
|
||||
/**
|
||||
* This comparator handles update on colliding uniqueIdentifier flatObjectdMetadata
|
||||
*/
|
||||
export const compareTwoFlatObjectMetadata = ({
|
||||
from,
|
||||
to,
|
||||
}: FromTo<FlatObjectMetadata>) => {
|
||||
const fromCompare = transformMetadataForComparison(from, {});
|
||||
const toCompare = transformMetadataForComparison(to, {});
|
||||
const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields'));
|
||||
|
||||
return objectMetadataDifference.flatMap<
|
||||
UpdateObjectAction['updates'][number]
|
||||
>((difference) => {
|
||||
switch (difference.type) {
|
||||
case 'CHANGE': {
|
||||
if (
|
||||
difference.oldValue === null &&
|
||||
(difference.value === null || difference.value === undefined)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const property = difference.path[0];
|
||||
|
||||
// TODO investigate why it would be a number, in case of array I guess ?
|
||||
if (typeof property === 'number') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Could be handled directly from the diff we do above
|
||||
if (
|
||||
!flatObjectMetadataPropertiesToCompare.includes(
|
||||
property as FlatObjectMetadataPropertiesToCompare,
|
||||
)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return {
|
||||
property: property as FlatObjectMetadataPropertiesToCompare,
|
||||
from: difference.oldValue,
|
||||
to: difference.value,
|
||||
};
|
||||
}
|
||||
case 'CREATE':
|
||||
case 'REMOVE': {
|
||||
// Should never occurs ? should throw ?
|
||||
return [];
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(
|
||||
difference,
|
||||
`Unexpected difference type: ${difference['type']}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
+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);
|
||||
};
|
||||
Reference in New Issue
Block a user