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

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

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

## Todo
- ~~Create a feature flag~~
- Integration test for object creation through metadata api + pg col
introspection and snapshoting
This commit is contained in:
Paul Rastoin
2025-07-29 17:47:28 +02:00
committed by GitHub
parent 600df4fd90
commit c1bf0a1fbf
66 changed files with 1224 additions and 164 deletions
@@ -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,
});
};
@@ -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'
>;
@@ -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']}`,
);
}
}
});
};
@@ -0,0 +1,28 @@
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { fromFieldMetadataEntityToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-field-metadata-entity-to-flat-field-metadata.util';
import {
FlatObjectMetadata,
objectMetadataEntityRelationProperties,
} from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
export const fromObjectMetadataEntityToFlatObjectMetadata = (
objectMetadataEntity: ObjectMetadataEntity,
): FlatObjectMetadata => {
const objectMetadataEntityWithoutRelations = removePropertiesFromRecord(
objectMetadataEntity,
objectMetadataEntityRelationProperties,
);
return {
...objectMetadataEntityWithoutRelations,
flatIndexMetadatas: [], // TODO prastoin handle indexes
uniqueIdentifier:
objectMetadataEntityWithoutRelations.standardId ??
objectMetadataEntityWithoutRelations.id,
flatFieldMetadatas: objectMetadataEntity.fields.map(
fromFieldMetadataEntityToFlatFieldMetadata,
),
};
};
@@ -0,0 +1,21 @@
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-entity-to-flat-object-metadata.util';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
export const fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata = (
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
): FlatObjectMetadata => {
const {
fieldsById,
fieldIdByJoinColumnName: _fieldIdByJoinColumnName,
fieldIdByName: _fieldIdByName,
indexMetadatas,
...rest
} = objectMetadataItemWithFieldMaps;
return fromObjectMetadataEntityToFlatObjectMetadata({
...rest,
fields: Object.values(fieldsById),
indexMetadatas,
});
};
@@ -0,0 +1,21 @@
import { isDefined } from 'twenty-shared/utils';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-object-metadata-item-with-field-maps-to-flat-object-metadata.util';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
export const fromObjectMetadataMapsToFlatObjectMetadatas = (
objectMetadataMaps: ObjectMetadataMaps,
): FlatObjectMetadata[] => {
const objectMetadataIds = Object.keys(objectMetadataMaps.byId);
return objectMetadataIds.flatMap<FlatObjectMetadata>((objectMetadataId) => {
const occurrence = objectMetadataMaps.byId[objectMetadataId];
if (!isDefined(occurrence)) {
return [];
}
return fromObjectMetadataItemWithFieldMapsToFlatObjectMetadata(occurrence);
});
};
@@ -0,0 +1,64 @@
import {
deepMerge,
fromArrayToUniqueKeyRecord,
isDefined,
} from 'twenty-shared/utils';
import { mergeTwoFlatFieldMetadatas } from 'src/engine/metadata-modules/flat-field-metadata/utils/merge-two-flat-field-metadatas.util';
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { ToMerge } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/to-merge.type';
export const mergeTwoFlatFieldObjectMetadatas = ({
destFlatObjectMetadatas,
toMergeFlatObjectMetadatas,
}: ToMerge<FlatObjectMetadata[], 'object'>): FlatObjectMetadata[] => {
const initialObjectAccumulator = fromArrayToUniqueKeyRecord({
array: destFlatObjectMetadatas,
uniqueKey: 'uniqueIdentifier',
});
const mergedUniqueIdentifierFlatObjectMetadataRecord =
toMergeFlatObjectMetadatas.reduce<Record<string, FlatObjectMetadata>>(
(acc, toMergeFlatObjectMetadata) => {
const flatObjectUniqueIdentifier =
toMergeFlatObjectMetadata.uniqueIdentifier;
const accumulatorCurrentOccurrence: FlatObjectMetadata | undefined =
initialObjectAccumulator[flatObjectUniqueIdentifier];
if (!isDefined(accumulatorCurrentOccurrence)) {
return {
...acc,
[flatObjectUniqueIdentifier]: toMergeFlatObjectMetadata,
};
}
const {
flatFieldMetadatas: destFlatFieldMetadatas,
...destFlatObjectMetadataRest
} = accumulatorCurrentOccurrence;
const {
flatFieldMetadatas: toMergeFlatFieldMetadatas,
...toMergeFlatObjectMetadataRest
} = toMergeFlatObjectMetadata;
const mergedFlatObjectMetadata: FlatObjectMetadata = {
...deepMerge(
destFlatObjectMetadataRest,
toMergeFlatObjectMetadataRest,
),
// TODO prastoin handle indexes
flatFieldMetadatas: mergeTwoFlatFieldMetadatas({
destFlatFieldMetadatas,
toMergeFlatFieldMetadatas,
}),
};
return {
...acc,
[flatObjectUniqueIdentifier]: mergedFlatObjectMetadata,
};
},
initialObjectAccumulator,
);
return Object.values(mergedUniqueIdentifierFlatObjectMetadataRecord);
};