ObjectMetadataServiceV2.updateOne (#13891)
# Introduction Last service to get migrated, `updateOne` object metadata service v2
This commit is contained in:
+260
@@ -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];
|
||||
};
|
||||
+76
@@ -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;
|
||||
};
|
||||
+8
@@ -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);
|
||||
+73
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user