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
@@ -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,
};
};