ObjectMetadataServiceV2.createOne (#13849)

# Introduction

Introducing v2 object metadata service create one handler v2, not
overkilling the flatObjectMetadata validation for the moment that will
require sequential validation for the import in order to handle
relations and morph relations
This commit is contained in:
Paul Rastoin
2025-08-12 16:49:35 +02:00
committed by GitHub
parent c94f33f1b7
commit de0468d104
26 changed files with 573 additions and 176 deletions
@@ -0,0 +1,36 @@
import { t } from '@lingui/core/macro';
import camelCase from 'lodash.camelcase';
import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type';
import {
beneathDatabaseIdentifierMinimumLength,
exceedsDatabaseIdentifierMaximumLength,
} from 'src/engine/metadata-modules/utils/validate-database-identifier-length.utils';
import { RESERVED_METADATA_NAME_KEYWORDS } from 'src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword';
import { STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX } from 'src/engine/metadata-modules/utils/validate-metadata-name-start-with-lowercase-letter-and-contain-digits-nor-letters.utils';
export const METADATA_NAME_VALIDATORS: FlatMetadataValidator<string>[] = [
{
message: t`Name is too long`,
validator: (name) => exceedsDatabaseIdentifierMaximumLength(name),
},
{
message: t`Name is too short`,
validator: (name) => beneathDatabaseIdentifierMinimumLength(name),
},
{
message: t`Name should be in camelCase`,
validator: (name) => name !== camelCase(name),
},
{
message: t`Name is not valid: it must start with lowercase letter and contain only alphanumeric letters`,
validator: (name) =>
!name.match(
STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX,
),
},
{
message: t`The name is not available`,
validator: (name) => RESERVED_METADATA_NAME_KEYWORDS.includes(name),
},
];
@@ -42,7 +42,7 @@ const coreObjectNames = [
'userWorkspaceRoles',
];
const reservedKeywords = [
export const RESERVED_METADATA_NAME_KEYWORDS = [
...coreObjectNames,
'plan',
'plans',
@@ -64,14 +64,12 @@ const reservedKeywords = [
'index',
'relation',
'relations',
'position',
'positions',
];
export const validateMetadataNameIsNotReservedKeywordOrThrow = (
name: string,
) => {
if (reservedKeywords.includes(name)) {
if (RESERVED_METADATA_NAME_KEYWORDS.includes(name)) {
throw new InvalidMetadataException(
`The name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
@@ -5,7 +5,7 @@ import {
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
export const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
/^[a-z][a-zA-Z0-9]*$/;
export const validateMetadataNameStartWithLowercaseLetterAndContainDigitsNorLettersOrThrow =
@@ -1,21 +0,0 @@
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { validateMetadataNameOrThrow } from 'src/engine/metadata-modules/utils/validate-metadata-name-or-throw.utils';
export const validateMetadataName = (
name: string,
): undefined | FieldMetadataException => {
try {
validateMetadataNameOrThrow(name);
} catch (error) {
return new FieldMetadataException(
error.message,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: error.userFriendlyMessage,
},
);
}
};
@@ -1,6 +1,7 @@
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatObjectMetadataMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-maps.type';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
@@ -11,18 +12,18 @@ type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = {
objectMetadataNameSingular: string;
objectMetadataNamePlural: string;
existingObjectMetadataId?: string;
objectMetadataMaps: ObjectMetadataMaps;
objectMetadataMaps: ObjectMetadataMaps | FlatObjectMetadataMaps;
};
export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({
objectMetadataNameSingular,
objectMetadataNamePlural,
existingObjectMetadataId,
export const doesOtherObjectWithSameNameExists = ({
objectMetadataMaps,
}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) => {
const objectAlreadyExists = Object.values(objectMetadataMaps.byId)
objectMetadataNamePlural,
objectMetadataNameSingular,
existingObjectMetadataId,
}: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams) =>
Object.values(objectMetadataMaps.byId)
.filter(isDefined)
.find(
.some(
(objectMetadata) =>
(objectMetadata.nameSingular === objectMetadataNameSingular ||
objectMetadata.namePlural === objectMetadataNamePlural ||
@@ -31,6 +32,11 @@ export const validatesNoOtherObjectWithSameNameExistsOrThrows = ({
objectMetadata.id !== existingObjectMetadataId,
);
export const validatesNoOtherObjectWithSameNameExistsOrThrows = (
args: ValidateNoOtherObjectWithSameNameExistsOrThrowsParams,
) => {
const objectAlreadyExists = doesOtherObjectWithSameNameExists(args);
if (objectAlreadyExists) {
throw new ObjectMetadataException(
'Object already exists',