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,24 @@
import { faker } from '@faker-js/faker';
import { FlatIndexFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-field-metadata';
type FlatIndexFieldMetadataOverrides = Required<
Pick<
FlatIndexFieldMetadata,
'fieldMetadataId' | 'indexMetadataId' | 'uniqueIdentifier'
>
> &
Partial<FlatIndexFieldMetadata>;
export const getFlatIndexFieldMetadataMock = (
overrides: FlatIndexFieldMetadataOverrides,
): FlatIndexFieldMetadata => {
const createdAt = faker.date.anytime();
return {
createdAt,
id: faker.string.uuid(),
order: faker.number.int(),
updatedAt: createdAt,
...overrides,
};
};
@@ -0,0 +1,37 @@
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';
type FlatIndexMetadataOverrides = Required<
Pick<FlatIndexMetadata, 'uniqueIdentifier' | 'objectMetadataId'>
> &
Partial<FlatIndexMetadata>;
export const getFlatIndexMetadataMock = (
overrides: FlatIndexMetadataOverrides,
): FlatIndexMetadata => {
const createdAt = faker.date.anytime();
return {
flatIndexFieldMetadatas: [],
createdAt,
id: faker.string.uuid(),
indexType: IndexType.BTREE,
indexWhereClause: undefined,
isCustom: false,
isUnique: false,
name: 'defaultFlatIndexMetadataName',
updatedAt: createdAt,
workspaceId: faker.string.uuid(),
...overrides,
};
};
export const getStandardFlatIndexMetadataMock = (
overrides: Omit<FlatIndexMetadataOverrides, 'isCustom'>,
) => {
return getFlatIndexMetadataMock({
isCustom: false,
...overrides,
});
};
@@ -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;
};
@@ -0,0 +1,16 @@
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { FlatIndexFieldMetadata } from 'src/engine/workspace-manager/workspace-migration-v2/types/flat-index-field-metadata';
import { MetadataEntitiesRelationTarget } from 'src/engine/workspace-manager/workspace-migration-v2/types/metadata-entities-relation-targets.type';
type IndexMetadataRelationProperties = ExtractRecordTypeOrmRelationProperties<
IndexMetadataEntity,
MetadataEntitiesRelationTarget
>;
export type FlatIndexMetadata = Partial<
Omit<IndexMetadataEntity, IndexMetadataRelationProperties>
> & {
flatIndexFieldMetadatas: FlatIndexFieldMetadata[];
uniqueIdentifier: string;
};
@@ -0,0 +1,63 @@
import diff from 'microdiff';
import { FromTo } from 'twenty-shared/types';
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 = [
'flatIndexFieldMetadatas', // Comparing this as whole ? should iterate on each keys ? => TBD should only map over cols as before ?
'indexType',
'indexWhereClause',
'isUnique',
'name',
] as const satisfies (keyof FlatIndexMetadata)[];
type FlatIndexMetadataPropertiesToCompare =
(typeof flatIndexMetadataPropertiesToCompare)[number];
// Should also handle indexFieldMetadata comparison ?
/**
* This comparator handles update on colliding uniqueIdentifier flatIndexMetadata
*/
export const compareTwoFlatIndexMetadata = ({
from,
to,
}: FromTo<FlatIndexMetadata>) => {
const transformOptions = {
shouldIgnoreProperty: (property: string) =>
!flatIndexMetadataPropertiesToCompare.includes(
property as FlatIndexMetadataPropertiesToCompare,
),
};
const fromCompare = transformMetadataForComparison(from, transformOptions);
const toCompare = transformMetadataForComparison(to, transformOptions);
const flatIndexeDifferences = diff(fromCompare, toCompare);
return flatIndexeDifferences.flatMap<{ property: string } & FromTo<unknown>>(
(difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
const property = path[0];
if (typeof property === 'number') {
return [];
}
return {
from: oldValue,
property,
to: value,
};
}
case 'CREATE':
case 'REMOVE':
default:
return [];
}
},
);
};