ObjectMetadataServiceV2.updateOne (#13891)

# Introduction
Last service to get migrated, `updateOne` object metadata service v2
This commit is contained in:
Paul Rastoin
2025-08-14 11:37:52 +02:00
committed by GitHub
parent 3c034c4ab4
commit ad77d2f462
42 changed files with 798 additions and 282 deletions
@@ -0,0 +1,260 @@
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { QUOTED_STRING_REGEX } from 'twenty-shared/constants';
import {
FieldMetadataType,
type EnumFieldMetadataType,
type NonNullableRequired,
} from 'twenty-shared/types';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
import { type FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import {
type FieldMetadataComplexOption,
type FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import {
runFlatFieldMetadataValidators,
type FailedFlatFieldMetadataValidationExceptions,
} from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
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 { isSnakeCaseString } from 'src/utils/is-snake-case-string';
const validateMetadataOptionId = (sanitizedId?: string) => {
const validators: FlatMetadataValidator<string>[] = [
{
validator: (id) => !isDefined(id),
message: t`Option id is required`,
},
{
validator: (id) => !z.string().uuid().safeParse(id).success,
message: t`Option id is invalid`,
},
];
return runFlatFieldMetadataValidators(sanitizedId, validators);
};
const validateMetadataOptionLabel = (sanitizedLabel: string) => {
const validators: FlatMetadataValidator<string>[] = [
{
validator: (label) => !isDefined(label),
message: t`Option label is required`,
},
{
validator: exceedsDatabaseIdentifierMaximumLength,
message: t`Option label exceeds 63 characters`,
},
{
validator: beneathDatabaseIdentifierMinimumLength,
message: t`Option label "${sanitizedLabel}" is beneath 1 character`,
},
{
validator: (label) => label.includes(','),
message: t`Label must not contain a comma`,
},
{
validator: (label) => !isNonEmptyString(label) || label === ' ',
message: t`Label must not be empty`,
},
];
return runFlatFieldMetadataValidators(sanitizedLabel, validators);
};
const validateMetadataOptionValue = (sanitizedValue: string) => {
const validators: FlatMetadataValidator<string>[] = [
{
validator: (value) => !isDefined(value),
message: t`Option value is required`,
},
{
validator: exceedsDatabaseIdentifierMaximumLength,
message: t`Option value exceeds 63 characters`,
},
{
validator: beneathDatabaseIdentifierMinimumLength,
message: t`Option value "${sanitizedValue}" is beneath 1 character`,
},
{
validator: (value) => !isSnakeCaseString(value),
message: t`Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`,
},
];
return runFlatFieldMetadataValidators(sanitizedValue, validators);
};
const validateDuplicates = (
options: FieldMetadataOptions<EnumFieldMetadataType>,
) => {
const fieldsToCheckForDuplicates = [
'position',
'id',
'value',
] as const satisfies (keyof (
| FieldMetadataDefaultOption[]
| FieldMetadataComplexOption[]
)[number])[];
const duplicatedValidators = fieldsToCheckForDuplicates.map<
FlatMetadataValidator<
FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]
>
>((field) => ({
message: t`Duplicated option ${field}`,
validator: () =>
new Set(options.map((option) => option[field])).size !== options.length,
}));
return runFlatFieldMetadataValidators(options, duplicatedValidators);
};
const validateFieldMetadataInputOptions = <T extends EnumFieldMetadataType>(
flatFieldMetadata: FlatFieldMetadata<T>,
) => {
const { options } = flatFieldMetadata;
if (!isDefined(options) || options.length === 0) {
return [
new FieldMetadataException(
'Options are required for enum fields',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
),
];
}
const optionsValidationErrors = options.flatMap((option) =>
[
validateMetadataOptionId(option.id),
validateMetadataOptionValue(option.value),
validateMetadataOptionLabel(option.label),
].flat(),
);
const duplicatedValidationErrors = validateDuplicates(options);
return [...optionsValidationErrors, ...duplicatedValidationErrors];
};
const validateSelectDefaultValue = (
options: FieldMetadataOptions<EnumFieldMetadataType>,
defaultValue: string | string[],
): FailedFlatFieldMetadataValidationExceptions[] => {
if (typeof defaultValue !== 'string') {
return [
new FieldMetadataException(
'Default value for select must be a string',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
),
];
}
const validators: FlatMetadataValidator<string>[] = [
{
validator: (value: string) => !QUOTED_STRING_REGEX.test(value),
message: 'Default value should be as quoted string',
},
{
validator: (value: string) =>
!options.some(
(option) => option.value === value.replace(QUOTED_STRING_REGEX, '$1'),
),
message: `Default value "${defaultValue}" must be one of the option values`,
},
];
return runFlatFieldMetadataValidators(defaultValue, validators);
};
const validateMultiSelectDefaultValue = (
options: FieldMetadataOptions<EnumFieldMetadataType>,
multiSelectDefaultValue: unknown,
): FailedFlatFieldMetadataValidationExceptions[] => {
if (!Array.isArray(multiSelectDefaultValue)) {
return [
new FieldMetadataException(
'Default value for multi-select must be an array',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
),
];
}
const validators: FlatMetadataValidator<string[]>[] = [
{
validator: (values) => values.length === 0,
message: 'If defined default value must contain at least one value',
},
{
validator: (values) => new Set(values).size !== values.length,
message: 'Default values must be unique',
},
];
return [
runFlatFieldMetadataValidators(multiSelectDefaultValue, validators),
multiSelectDefaultValue.flatMap((value) =>
validateSelectDefaultValue(options, value),
),
].flat();
};
const validateFieldMetadataDefaultValue = (
flatFieldMetadata: Omit<
FlatFieldMetadata<EnumFieldMetadataType>,
'defaultValue'
> &
NonNullableRequired<
Pick<FlatFieldMetadata<EnumFieldMetadataType>, 'defaultValue'>
>,
) => {
switch (flatFieldMetadata.type) {
case FieldMetadataType.SELECT:
case FieldMetadataType.RATING:
return validateSelectDefaultValue(
flatFieldMetadata.options,
flatFieldMetadata.defaultValue,
);
case FieldMetadataType.MULTI_SELECT:
return validateMultiSelectDefaultValue(
flatFieldMetadata.options,
flatFieldMetadata.defaultValue,
);
default: {
assertUnreachable(
flatFieldMetadata.type,
'Should never occur, unknown field metadata enum type',
);
}
}
};
export const validateEnumSelectFlatFieldMetadata = ({
flatFieldMetadataToValidate,
}: ValidateOneFieldMetadataArgs<EnumFieldMetadataType>): FailedFlatFieldMetadataValidationExceptions[] => {
const optionsValidationErrors = validateFieldMetadataInputOptions(
flatFieldMetadataToValidate,
);
const defaultValueValidationErrors = isDefined(
flatFieldMetadataToValidate.defaultValue,
)
? validateFieldMetadataDefaultValue({
...flatFieldMetadataToValidate,
defaultValue: flatFieldMetadataToValidate.defaultValue,
})
: [];
return [...optionsValidationErrors, ...defaultValueValidationErrors];
};
@@ -0,0 +1,76 @@
import { t } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import {
InvalidMetadataException,
InvalidMetadataExceptionCode,
} from 'src/engine/metadata-modules/utils/exceptions/invalid-metadata.exception';
const getReservedCompositeFieldNames = (
flatObjectMetadata: FlatObjectMetadata,
): string[] => {
return flatObjectMetadata.flatFieldMetadatas.flatMap((flatFieldMetadata) => {
if (isCompositeFieldMetadataType(flatFieldMetadata.type)) {
const base = flatFieldMetadata.name;
const compositeType = compositeTypeDefinitions.get(
flatFieldMetadata.type,
);
if (!isDefined(compositeType)) {
return [];
}
return compositeType.properties.map((property) =>
computeCompositeColumnName(base, property),
);
}
return [];
});
};
export const validateFlatFieldMetadataNameAvailability = ({
name,
flatObjectMetadata,
}: {
name: string;
flatObjectMetadata: FlatObjectMetadata;
}): FailedFlatFieldMetadataValidationExceptions | undefined => {
const reservedCompositeFieldsNames =
getReservedCompositeFieldNames(flatObjectMetadata);
if (
flatObjectMetadata.flatFieldMetadatas.some(
(field) =>
field.name === name ||
(field.type === FieldMetadataType.RELATION && // Question: Should we also look for MORPH_RELATION field types ?
`${field.name}Id` === name),
)
) {
return new InvalidMetadataException(
`Name "${name}" is not available as it is already used by another field`,
InvalidMetadataExceptionCode.NOT_AVAILABLE,
{
userFriendlyMessage: t`This name is not available as it is already used by another field`,
},
);
}
if (reservedCompositeFieldsNames.includes(name)) {
return new InvalidMetadataException(
`Name "${name}" is not available`,
InvalidMetadataExceptionCode.RESERVED_KEYWORD,
{
userFriendlyMessage: t`This name is not available.`,
},
);
}
return undefined;
};
@@ -0,0 +1,8 @@
import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
import { runFlatFieldMetadataValidators } from 'src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util';
import { METADATA_NAME_VALIDATORS } from 'src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants';
export const validateFlatFieldMetadataName = (
name: string,
): FailedFlatFieldMetadataValidationExceptions[] =>
runFlatFieldMetadataValidators(name, METADATA_NAME_VALIDATORS);
@@ -0,0 +1,73 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { type ValidateOneFieldMetadataArgs } from 'src/engine/metadata-modules/flat-field-metadata/services/flat-field-metadata-validator.service';
import { type FailedFlatFieldMetadataValidationExceptions } from 'src/engine/metadata-modules/flat-field-metadata/types/failed-flat-field-metadata-validation.type';
export const validateRelationFlatFieldMetadata = async ({
existingFlatObjectMetadataMaps,
flatFieldMetadataToValidate: {
relationTargetFieldMetadataId,
relationTargetObjectMetadataId,
},
otherFlatObjectMetadataMapsToValidate,
}: ValidateOneFieldMetadataArgs<FieldMetadataType.RELATION>): Promise<
FailedFlatFieldMetadataValidationExceptions[]
> => {
const uuidsValidation = [
relationTargetObjectMetadataId,
relationTargetFieldMetadataId,
].flatMap<FieldMetadataException>((id) =>
isValidUuid(id)
? []
: new FieldMetadataException(
`Invalid uuid ${id}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
),
);
if (uuidsValidation.length > 0) {
return uuidsValidation;
}
const errors: FailedFlatFieldMetadataValidationExceptions[] = [];
const targetRelationFlatObjectMetadata =
otherFlatObjectMetadataMapsToValidate?.byId[
relationTargetObjectMetadataId
] ?? existingFlatObjectMetadataMaps.byId[relationTargetObjectMetadataId];
if (!isDefined(targetRelationFlatObjectMetadata)) {
errors.push(
new FieldMetadataException(
isDefined(otherFlatObjectMetadataMapsToValidate)
? 'Relation target object metadata not found in both existing and about to be created object metadatas'
: 'Relation target object metadata not found',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
),
);
}
const targetRelationFlatFieldMetadata =
targetRelationFlatObjectMetadata?.fieldsById[relationTargetFieldMetadataId];
if (
isDefined(targetRelationFlatObjectMetadata) &&
!isDefined(targetRelationFlatFieldMetadata)
) {
errors.push(
new FieldMetadataException(
isDefined(otherFlatObjectMetadataMapsToValidate)
? 'Relation field target metadata not found in both existing and about to be created field metadatas'
: 'Relation field target metadata not found',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
),
);
}
return errors;
};