Twenty standard application Objects and fields as allFlatEntityMaps ID non-agnostic (#16298)
# Introduction Related to https://github.com/twentyhq/core-team-issues/issues/1995 This PR introduces the basis of the `twentyStandard` application as code on demand, it's highly tied to `ids` where it will becomes workspace agnostic following the builder and runner `universalIdentifier` refactor later. The goal here to allow computing the `allFlatEntityMaps` `to` of the `twentyStandard` application on a empty workspace ( workspace creation ). Allowing installing the twenty standard app through a workspace migration instead of passing by the sync metadata Nothing done will be run in production for the moment if it's not the small validation refactor we've introduced Please note that everything introduced here will be replaced at some point by a twenty app instance when the twenty sdk is mature enough to handle of the edge cases we need here ## How we've proceeded We've been iterating over every workspace entity both objects and their fields, and transpiled them to flatEntity. Being sure we migrate the defaultValue, settings and so on accordingly. We've also compute all the ids in prior of the whole entities computation so we don't face any hoisting issue. ## Current state At the moment only handling all of the 29 standard objects and their fields Settings a unique universalIdentifier for all of them Will come views, agent role targets and so on later ## `workspace:compute-twenty-standard-migration` command This command allow generating a workspace migration that will result in installing the twenty standard app in an empty workspace It's temporary and aims to allow debugging for the moment we might not keep it in the future as it is right now It contains debug writeFileSync which is expected no worries greptile ## `LabelFieldMetadataIdentifierId` Small refactor allowing defining the label identifier field metadata id of a uuid field metadata type for system object, as some of our standard object don't have a name field and don't aim to Also please note that we might remove this build options later in the sake of the currently installed universal identifier application that we could compare with the deterministic twenty standard one ## `runFlatFieldMetadataValidators` Deprecated this pattern which was redundant and not v2 friendly pattern ## Current errors that will address in upcoming PR Current standard objects and fields metadata does not pass the validation that we have in place, as historically the sync metadata would directly consume the repositories and would just ignore the validation. This is about to change. Will handle the below errors in dedicated PRs as they will required upgrade commands in order to migrate the data, or will handle that from the sync metadata instead still to be determined but nothing critical here - camel case field metadata name - options label invalid format ```json { "status": "fail", "report": { "fieldMetadata": [ { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Name should be in camelCase", "userFriendlyMessage": { "id": "P+jdmX", "message": "Name should be in camelCase" }, "value": "iCalUID" } ], "flatEntityMinimalInformation": { "id": "68dd83cd-92c8-4233-bb28-47939bab6124", "name": "iCalUID", "objectMetadataId": "11c16ab6-9176-439e-a2db-a12c5a58a524" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"email\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "email" } }, "value": "email" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"sms\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "sms" } }, "value": "sms" } ], "flatEntityMinimalInformation": { "id": "e3caaf2a-e07d-4146-8dfc-9eef904e82c9", "name": "type", "objectMetadataId": "4b777de5-4c7b-4af4-9b92-655c0f87512b" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"incoming\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "incoming" } }, "value": "incoming" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"outgoing\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "outgoing" } }, "value": "outgoing" } ], "flatEntityMinimalInformation": { "id": "d96233a4-93be-45ea-9548-3b50f3c700cf", "name": "direction", "objectMetadataId": "480a648a-d2e5-482a-992f-ef053e1b4bb0" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"from\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "from" } }, "value": "from" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"to\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "to" } }, "value": "to" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"cc\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "cc" } }, "value": "cc" }, { "code": "INVALID_FIELD_INPUT", "message": "Value must be in UPPER_CASE and follow snake_case \"bcc\"", "userFriendlyMessage": { "id": "UBPzFQ", "message": "Value must be in UPPER_CASE and follow snake_case \"{sanitizedValue}\"", "values": { "sanitizedValue": "bcc" } }, "value": "bcc" } ], "flatEntityMinimalInformation": { "id": "961c598e-67c3-452d-8bb2-b92c0bc64404", "name": "role", "objectMetadataId": "8af8a13c-ff97-4cd3-b70d-52a7dc2924b4" }, "type": "create_field" }, { "status": "fail", "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Label must not contain a comma", "userFriendlyMessage": { "id": "k731jp", "message": "Label must not contain a comma" }, "value": "Commas and dot (1,234.56)" }, { "code": "INVALID_FIELD_INPUT", "message": "Label must not contain a comma", "userFriendlyMessage": { "id": "k731jp", "message": "Label must not contain a comma" }, "value": "Spaces and comma (1 234,56)" }, { "code": "INVALID_FIELD_INPUT", "message": "Label must not contain a comma", "userFriendlyMessage": { "id": "k731jp", "message": "Label must not contain a comma" }, "value": "Dots and comma (1.234,56)" } ], "flatEntityMinimalInformation": { "id": "7fa20caf-2597-42e3-84e5-15a91b125b9b", "name": "numberFormat", "objectMetadataId": "a6974302-9e72-461c-aa09-9390f4ff16fc" }, "type": "create_field" } ], "objectMetadata": [], "view": [], "viewField": [], "viewGroup": [], "index": [], "serverlessFunction": [], "cronTrigger": [], "databaseEventTrigger": [], "routeTrigger": [], "viewFilter": [], "role": [], "roleTarget": [], "agent": [] } } ```
This commit is contained in:
-27
@@ -1,27 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type';
|
||||
|
||||
export const runFlatFieldMetadataValidators = <T>({
|
||||
elementToValidate,
|
||||
validators,
|
||||
}: {
|
||||
elementToValidate: T;
|
||||
validators: FlatMetadataValidator<T>[];
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
return validators.flatMap(({ validator, message }) => {
|
||||
const isInvalid = validator(elementToValidate);
|
||||
|
||||
if (isInvalid) {
|
||||
return {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t(message),
|
||||
value: elementToValidate,
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
+205
-155
@@ -4,8 +4,8 @@ import { QUOTED_STRING_REGEX } from 'twenty-shared/constants';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
type EnumFieldMetadataType,
|
||||
type NonNullableRequired,
|
||||
type FieldMetadataOptions,
|
||||
type NonNullableRequired,
|
||||
} from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
@@ -18,128 +18,159 @@ import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-me
|
||||
import { type FlatFieldMetadataTypeValidationArgs } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-type-validator.type';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { runFlatFieldMetadataValidators } from 'src/engine/metadata-modules/flat-field-metadata/utils/run-flat-field-metadata-validators.util';
|
||||
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 { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants';
|
||||
import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants';
|
||||
import { isSnakeCaseString } from 'src/utils/is-snake-case-string';
|
||||
|
||||
const validateMetadataOptionId = (sanitizedId?: string) => {
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: (id) => !isDefined(id),
|
||||
message: msg`Option id is required`,
|
||||
},
|
||||
{
|
||||
validator: (id) => !z.string().uuid().safeParse(id).success,
|
||||
message: msg`Option id is invalid`,
|
||||
},
|
||||
];
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
return runFlatFieldMetadataValidators({
|
||||
elementToValidate: sanitizedId,
|
||||
validators,
|
||||
});
|
||||
if (!isDefined(sanitizedId)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option id is required`,
|
||||
userFriendlyMessage: msg`Option id is required`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!z.string().uuid().safeParse(sanitizedId).success) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option id is invalid`,
|
||||
userFriendlyMessage: msg`Option id is invalid`,
|
||||
value: sanitizedId,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateMetadataOptionLabel = (
|
||||
sanitizedLabel: string,
|
||||
): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (!isDefined(sanitizedLabel)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label is required`,
|
||||
userFriendlyMessage: msg`Option label is required`,
|
||||
},
|
||||
];
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label is required`,
|
||||
userFriendlyMessage: msg`Option label is required`,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(sanitizedLabel)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label must be a string of at least one character`,
|
||||
userFriendlyMessage: msg`Option label format not supported`,
|
||||
value: sanitizedLabel,
|
||||
},
|
||||
];
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label must be a string of at least one character`,
|
||||
userFriendlyMessage: msg`Option label format not supported`,
|
||||
value: sanitizedLabel,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: exceedsDatabaseIdentifierMaximumLength,
|
||||
message: msg`Option label exceeds 63 characters`,
|
||||
},
|
||||
{
|
||||
validator: beneathDatabaseIdentifierMinimumLength,
|
||||
message: msg`Option label "${sanitizedLabel}" is beneath 1 character`,
|
||||
},
|
||||
{
|
||||
validator: (label) => label.includes(','),
|
||||
message: msg`Label must not contain a comma`,
|
||||
},
|
||||
{
|
||||
validator: (label) => !isNonEmptyString(label) || label === ' ',
|
||||
message: msg`Label must not be empty`,
|
||||
},
|
||||
];
|
||||
if (sanitizedLabel.length > IDENTIFIER_MAX_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label exceeds 63 characters`,
|
||||
userFriendlyMessage: msg`Option label exceeds 63 characters`,
|
||||
value: sanitizedLabel,
|
||||
});
|
||||
}
|
||||
|
||||
return runFlatFieldMetadataValidators({
|
||||
elementToValidate: sanitizedLabel,
|
||||
validators,
|
||||
});
|
||||
if (sanitizedLabel.length < IDENTIFIER_MIN_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option label "${sanitizedLabel}" is beneath 1 character`,
|
||||
userFriendlyMessage: msg`Option label "${sanitizedLabel}" is beneath 1 character`,
|
||||
value: sanitizedLabel,
|
||||
});
|
||||
}
|
||||
|
||||
if (sanitizedLabel.includes(',')) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Label must not contain a comma`,
|
||||
userFriendlyMessage: msg`Label must not contain a comma`,
|
||||
value: sanitizedLabel,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(sanitizedLabel) || sanitizedLabel === ' ') {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Label must not be empty`,
|
||||
userFriendlyMessage: msg`Label must not be empty`,
|
||||
value: sanitizedLabel,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateMetadataOptionValue = (
|
||||
sanitizedValue: string,
|
||||
): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (!isDefined(sanitizedValue)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value is required`,
|
||||
userFriendlyMessage: msg`Option value is required`,
|
||||
},
|
||||
];
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value is required`,
|
||||
userFriendlyMessage: msg`Option value is required`,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(sanitizedValue)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value must be a string of at least one character`,
|
||||
userFriendlyMessage: msg`Option value format not supported`,
|
||||
},
|
||||
];
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value must be a string of at least one character`,
|
||||
userFriendlyMessage: msg`Option value format not supported`,
|
||||
value: sanitizedValue,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: exceedsDatabaseIdentifierMaximumLength,
|
||||
message: msg`Option value exceeds 63 characters`,
|
||||
},
|
||||
{
|
||||
validator: beneathDatabaseIdentifierMinimumLength,
|
||||
message: msg`Option value "${sanitizedValue}" is beneath 1 character`,
|
||||
},
|
||||
{
|
||||
validator: (value) => !isSnakeCaseString(value),
|
||||
message: msg`Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`,
|
||||
},
|
||||
];
|
||||
if (sanitizedValue.length > IDENTIFIER_MAX_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value exceeds 63 characters`,
|
||||
userFriendlyMessage: msg`Option value exceeds 63 characters`,
|
||||
value: sanitizedValue,
|
||||
});
|
||||
}
|
||||
|
||||
return runFlatFieldMetadataValidators({
|
||||
elementToValidate: sanitizedValue,
|
||||
validators,
|
||||
});
|
||||
if (sanitizedValue.length < IDENTIFIER_MIN_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Option value "${sanitizedValue}" is beneath 1 character`,
|
||||
userFriendlyMessage: msg`Option value "${sanitizedValue}" is beneath 1 character`,
|
||||
value: sanitizedValue,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSnakeCaseString(sanitizedValue)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`,
|
||||
userFriendlyMessage: msg`Value must be in UPPER_CASE and follow snake_case "${sanitizedValue}"`,
|
||||
value: sanitizedValue,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateDuplicates = (
|
||||
options: FieldMetadataOptions<EnumFieldMetadataType>,
|
||||
) => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
const fieldsToCheckForDuplicates = [
|
||||
'position',
|
||||
'id',
|
||||
@@ -148,20 +179,22 @@ const validateDuplicates = (
|
||||
| FieldMetadataDefaultOption[]
|
||||
| FieldMetadataComplexOption[]
|
||||
)[number])[];
|
||||
const duplicatedValidators = fieldsToCheckForDuplicates.map<
|
||||
FlatMetadataValidator<
|
||||
FieldMetadataDefaultOption[] | FieldMetadataComplexOption[]
|
||||
>
|
||||
>((field) => ({
|
||||
message: msg`Duplicated option ${field}`,
|
||||
validator: () =>
|
||||
new Set(options.map((option) => option[field])).size !== options.length,
|
||||
}));
|
||||
|
||||
return runFlatFieldMetadataValidators({
|
||||
elementToValidate: options,
|
||||
validators: duplicatedValidators,
|
||||
});
|
||||
for (const field of fieldsToCheckForDuplicates) {
|
||||
const hasDuplicates =
|
||||
new Set(options.map((option) => option[field])).size !== options.length;
|
||||
|
||||
if (hasDuplicates) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Duplicated option ${field}`,
|
||||
userFriendlyMessage: msg`Duplicated option ${field}`,
|
||||
value: options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateFieldMetadataInputOptions = <T extends EnumFieldMetadataType>(
|
||||
@@ -200,35 +233,43 @@ const validateSelectDefaultValue = ({
|
||||
options: FieldMetadataOptions<EnumFieldMetadataType>;
|
||||
defaultValue: string | string[];
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (typeof defaultValue !== 'string') {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Default value for select must be a string got ${defaultValue}`,
|
||||
userFriendlyMessage: msg`Default value must be a string`,
|
||||
value: defaultValue,
|
||||
},
|
||||
];
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: `Default value for select must be a string got ${defaultValue}`,
|
||||
userFriendlyMessage: msg`Default value must be a string`,
|
||||
value: defaultValue,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: (value: string) => !QUOTED_STRING_REGEX.test(value),
|
||||
message: msg`Default value should be as quoted string`,
|
||||
},
|
||||
{
|
||||
validator: (value: string) =>
|
||||
!options.some(
|
||||
(option) => option.value === value.replace(QUOTED_STRING_REGEX, '$1'),
|
||||
),
|
||||
message: msg`Default value "${defaultValue}" must be one of the option values`,
|
||||
},
|
||||
];
|
||||
if (!QUOTED_STRING_REGEX.test(defaultValue)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Default value should be as quoted string`,
|
||||
userFriendlyMessage: msg`Default value should be as quoted string`,
|
||||
value: defaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
return runFlatFieldMetadataValidators({
|
||||
elementToValidate: defaultValue,
|
||||
validators,
|
||||
});
|
||||
const matchesOptionValue = options.some(
|
||||
(option) =>
|
||||
option.value === defaultValue.replace(QUOTED_STRING_REGEX, '$1'),
|
||||
);
|
||||
|
||||
if (!matchesOptionValue) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Default value "${defaultValue}" must be one of the option values`,
|
||||
userFriendlyMessage: msg`Default value "${defaultValue}" must be one of the option values`,
|
||||
value: defaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateMultiSelectDefaultValue = ({
|
||||
@@ -238,40 +279,49 @@ const validateMultiSelectDefaultValue = ({
|
||||
options: FieldMetadataOptions<EnumFieldMetadataType>;
|
||||
multiSelectDefaultValue: unknown;
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
if (!Array.isArray(multiSelectDefaultValue)) {
|
||||
return [
|
||||
{
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
userFriendlyMessage: msg`Multi-select field default value must be an array`,
|
||||
message: `Default value for multi-select must be an array got ${multiSelectDefaultValue}`,
|
||||
value: multiSelectDefaultValue,
|
||||
},
|
||||
];
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
userFriendlyMessage: msg`Multi-select field default value must be an array`,
|
||||
message: `Default value for multi-select must be an array got ${multiSelectDefaultValue}`,
|
||||
value: multiSelectDefaultValue,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const validators: FlatMetadataValidator<string[]>[] = [
|
||||
{
|
||||
validator: (values) => values.length === 0,
|
||||
message: msg`If defined default value must contain at least one value`,
|
||||
},
|
||||
{
|
||||
validator: (values) => new Set(values).size !== values.length,
|
||||
message: msg`Default values must be unique`,
|
||||
},
|
||||
];
|
||||
if (multiSelectDefaultValue.length === 0) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`If defined default value must contain at least one value`,
|
||||
userFriendlyMessage: msg`If defined default value must contain at least one value`,
|
||||
value: multiSelectDefaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
runFlatFieldMetadataValidators({
|
||||
elementToValidate: multiSelectDefaultValue,
|
||||
validators,
|
||||
}),
|
||||
multiSelectDefaultValue.flatMap((defaultValue) =>
|
||||
validateSelectDefaultValue({
|
||||
if (
|
||||
new Set(multiSelectDefaultValue).size !== multiSelectDefaultValue.length
|
||||
) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Default values must be unique`,
|
||||
userFriendlyMessage: msg`Default values must be unique`,
|
||||
value: multiSelectDefaultValue,
|
||||
});
|
||||
}
|
||||
|
||||
for (const defaultValue of multiSelectDefaultValue) {
|
||||
errors.push(
|
||||
...validateSelectDefaultValue({
|
||||
defaultValue,
|
||||
options,
|
||||
}),
|
||||
),
|
||||
].flat();
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const validateFieldMetadataDefaultValue = ({
|
||||
|
||||
+7
-1
@@ -11,6 +11,7 @@ import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modul
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { getObjectFieldNamesAndJoinColumnNames } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
|
||||
|
||||
const getReservedCompositeFieldNames = (
|
||||
objectFlatFieldMetadatas: FlatFieldMetadata[],
|
||||
@@ -39,7 +40,9 @@ export const validateFlatFieldMetadataNameAvailability = ({
|
||||
name,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
buildOptions,
|
||||
}: {
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
name: string;
|
||||
flatObjectMetadata: FlatObjectMetadata;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
@@ -54,7 +57,10 @@ export const validateFlatFieldMetadataNameAvailability = ({
|
||||
objectFlatFieldMetadatas,
|
||||
);
|
||||
|
||||
if (reservedCompositeFieldsNames.includes(name)) {
|
||||
if (
|
||||
!buildOptions.isSystemBuild &&
|
||||
reservedCompositeFieldsNames.includes(name)
|
||||
) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.RESERVED_KEYWORD,
|
||||
message: `Name "${name}" is reserved composite field name`,
|
||||
|
||||
+71
-16
@@ -1,21 +1,76 @@
|
||||
import { msg, t } from '@lingui/core/macro';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { RESERVED_METADATA_NAME_KEYWORDS } from 'twenty-shared/metadata';
|
||||
|
||||
import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { METADATA_NAME_VALIDATORS } from 'src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants';
|
||||
import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants';
|
||||
import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const validateFlatFieldMetadataName = (
|
||||
name: string,
|
||||
): FlatFieldMetadataValidationError[] =>
|
||||
METADATA_NAME_VALIDATORS.flatMap(({ validator, message }) => {
|
||||
const isInvalid = validator(name);
|
||||
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
|
||||
/^[a-z][a-zA-Z0-9]*$/;
|
||||
|
||||
if (isInvalid) {
|
||||
return {
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: message.message ?? '',
|
||||
userFriendlyMessage: message,
|
||||
value: name,
|
||||
};
|
||||
}
|
||||
export const validateFlatFieldMetadataName = ({
|
||||
buildOptions,
|
||||
name,
|
||||
}: {
|
||||
name: string;
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
const errors: FlatFieldMetadataValidationError[] = [];
|
||||
|
||||
return [];
|
||||
});
|
||||
if (name.length > IDENTIFIER_MAX_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Name is too long`,
|
||||
userFriendlyMessage: msg`Name is too long`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
if (name.length < IDENTIFIER_MIN_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Name is too short`,
|
||||
userFriendlyMessage: msg`Name is too short`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
if (name !== camelCase(name)) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Name should be in camelCase`,
|
||||
userFriendlyMessage: msg`Name should be in camelCase`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!name.match(
|
||||
STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX,
|
||||
)
|
||||
) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`Name is not valid: it must start with lowercase letter and contain only alphanumeric letters`,
|
||||
userFriendlyMessage: msg`Name is not valid: it must start with lowercase letter and contain only alphanumeric letters`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!buildOptions.isSystemBuild &&
|
||||
RESERVED_METADATA_NAME_KEYWORDS.includes(name)
|
||||
) {
|
||||
errors.push({
|
||||
code: FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
message: t`This name is reserved. Use a different name or the system will add "Custom" suffix.`,
|
||||
userFriendlyMessage: msg`This name is reserved. Use a different name or the system will add "Custom" suffix.`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
+4
@@ -9,6 +9,7 @@ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/
|
||||
import { type FlatFieldMetadataValidationError } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata-validation-error.type';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { validateFlatFieldMetadataNameAvailability } from 'src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name-availability.util';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const validateMorphOrRelationFlatFieldJoinColumName = ({
|
||||
flatFieldMetadata,
|
||||
@@ -16,7 +17,9 @@ export const validateMorphOrRelationFlatFieldJoinColumName = ({
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
},
|
||||
buildOptions,
|
||||
}: {
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
flatFieldMetadata: FlatFieldMetadata<MorphOrRelationFieldMetadataType>;
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: MetadataFlatEntityAndRelatedFlatEntityMapsForValidation<'fieldMetadata'>;
|
||||
}): FlatFieldMetadataValidationError[] => {
|
||||
@@ -55,6 +58,7 @@ export const validateMorphOrRelationFlatFieldJoinColumName = ({
|
||||
name: flatFieldMetadata.settings.joinColumnName,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata,
|
||||
buildOptions,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
|
||||
+2
@@ -18,6 +18,7 @@ export const validateMorphOrRelationFlatFieldMetadata = ({
|
||||
},
|
||||
updates,
|
||||
remainingFlatEntityMapsToValidate,
|
||||
buildOptions,
|
||||
}: FlatFieldMetadataTypeValidationArgs<MorphOrRelationFieldMetadataType>): FlatFieldMetadataValidationError[] => {
|
||||
const { relationTargetFieldMetadataId, relationTargetObjectMetadataId } =
|
||||
flatFieldMetadataToValidate;
|
||||
@@ -92,6 +93,7 @@ export const validateMorphOrRelationFlatFieldMetadata = ({
|
||||
)
|
||||
errors.push(
|
||||
...validateMorphOrRelationFlatFieldJoinColumName({
|
||||
buildOptions,
|
||||
flatFieldMetadata: flatFieldMetadataToValidate,
|
||||
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
|
||||
flatFieldMetadataMaps,
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
|
||||
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { type FlatMetadataValidator } from 'src/engine/metadata-modules/types/flat-metadata-validator.type';
|
||||
|
||||
export const runFlatObjectMetadataValidators = <T>({
|
||||
elementToValidate,
|
||||
validators,
|
||||
}: {
|
||||
elementToValidate: T;
|
||||
validators: FlatMetadataValidator<T>[];
|
||||
}): FlatObjectMetadataValidationError[] => {
|
||||
return validators.flatMap(({ validator, message }) => {
|
||||
const isInvalid = validator(elementToValidate);
|
||||
|
||||
if (isInvalid) {
|
||||
return {
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: t(message),
|
||||
value: elementToValidate,
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
+26
-7
@@ -3,23 +3,28 @@ import {
|
||||
isDefined,
|
||||
isLabelIdentifierFieldMetadataTypes,
|
||||
} from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type';
|
||||
|
||||
export const validateFlatObjectMetadataIdentifiers = ({
|
||||
flatObjectMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
buildOptions,
|
||||
}: {
|
||||
flatObjectMetadata: Pick<
|
||||
FlatObjectMetadata,
|
||||
'labelIdentifierFieldMetadataId' | 'imageIdentifierFieldMetadataId'
|
||||
>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
buildOptions: WorkspaceMigrationBuilderOptions;
|
||||
}) => {
|
||||
const errors: FlatObjectMetadataValidationError[] = [];
|
||||
|
||||
@@ -40,13 +45,27 @@ export const validateFlatObjectMetadataIdentifiers = ({
|
||||
'labelIdentifierFieldMetadataId validation failed: related field metadata not found',
|
||||
userFriendlyMessage: msg`Field declared as label identifier not found`,
|
||||
});
|
||||
} else if (!isLabelIdentifierFieldMetadataTypes(flatFieldMetadata.type)) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message:
|
||||
'labelIdentifierFieldMetadataId validation failed: field type not compatible',
|
||||
userFriendlyMessage: msg`Field cannot be used as label identifier`,
|
||||
});
|
||||
} else {
|
||||
if (!isLabelIdentifierFieldMetadataTypes(flatFieldMetadata.type)) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message:
|
||||
'labelIdentifierFieldMetadataId validation failed: field type not compatible',
|
||||
userFriendlyMessage: msg`Field cannot be used as label identifier`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!buildOptions.isSystemBuild &&
|
||||
isFlatFieldMetadataOfType(flatFieldMetadata, FieldMetadataType.UUID)
|
||||
) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message:
|
||||
'labelIdentifierFieldMetadataId validation failed: field type uuid is reserved for system object metadata',
|
||||
userFriendlyMessage: msg`Field cannot be used as label identifier`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-24
@@ -3,13 +3,9 @@ import { msg } from '@lingui/core/macro';
|
||||
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type';
|
||||
import { runFlatObjectMetadataValidators } from 'src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util';
|
||||
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
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 { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants';
|
||||
import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants';
|
||||
|
||||
export const validateFlatObjectMetadataLabel = ({
|
||||
labelPlural,
|
||||
@@ -17,26 +13,31 @@ export const validateFlatObjectMetadataLabel = ({
|
||||
}: Pick<FlatObjectMetadata, 'labelPlural' | 'labelSingular'> &
|
||||
ObjectMetadataMinimalInformation): FlatObjectMetadataValidationError[] => {
|
||||
const errors: FlatObjectMetadataValidationError[] = [];
|
||||
const validators: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
validator: (label) => beneathDatabaseIdentifierMinimumLength(label),
|
||||
message: msg`Object label is too short`,
|
||||
},
|
||||
{
|
||||
validator: (label) => exceedsDatabaseIdentifierMaximumLength(label),
|
||||
message: msg`Object label is too long`,
|
||||
},
|
||||
];
|
||||
|
||||
errors.push(
|
||||
...[labelSingular, labelPlural].flatMap((label) =>
|
||||
runFlatObjectMetadataValidators({
|
||||
elementToValidate: label,
|
||||
validators,
|
||||
}),
|
||||
),
|
||||
);
|
||||
// Validate both labelSingular and labelPlural
|
||||
for (const label of [labelSingular, labelPlural]) {
|
||||
// Length too short check
|
||||
if (label.length < IDENTIFIER_MIN_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `Object label is too short`,
|
||||
userFriendlyMessage: msg`Object label is too short`,
|
||||
value: label,
|
||||
});
|
||||
}
|
||||
|
||||
// Length too long check
|
||||
if (label.length > IDENTIFIER_MAX_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `Object label is too long`,
|
||||
userFriendlyMessage: msg`Object label is too long`,
|
||||
value: label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if labels are identical
|
||||
const labelsAreIdentical =
|
||||
labelSingular.trim().toLowerCase() === labelPlural.trim().toLowerCase();
|
||||
|
||||
|
||||
+64
-10
@@ -1,11 +1,16 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { RESERVED_METADATA_NAME_KEYWORDS } from 'twenty-shared/metadata';
|
||||
|
||||
import { type FlatObjectMetadataValidationError } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata-validation-error.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { type ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type';
|
||||
import { runFlatObjectMetadataValidators } from 'src/engine/metadata-modules/flat-object-metadata/utils/run-flat-object-metadata-validators.util';
|
||||
import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||
import { METADATA_NAME_VALIDATORS } from 'src/engine/metadata-modules/utils/constants/metadata-name-flat-metadata-validators.constants';
|
||||
import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants';
|
||||
import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants';
|
||||
|
||||
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
|
||||
/^[a-z][a-zA-Z0-9]*$/;
|
||||
|
||||
export const validateFlatObjectMetadataNames = ({
|
||||
namePlural,
|
||||
@@ -14,15 +19,64 @@ export const validateFlatObjectMetadataNames = ({
|
||||
ObjectMetadataMinimalInformation) => {
|
||||
const errors: FlatObjectMetadataValidationError[] = [];
|
||||
|
||||
errors.push(
|
||||
...[nameSingular, namePlural].flatMap((name) =>
|
||||
runFlatObjectMetadataValidators({
|
||||
elementToValidate: name,
|
||||
validators: METADATA_NAME_VALIDATORS,
|
||||
}),
|
||||
),
|
||||
);
|
||||
// Validate both nameSingular and namePlural
|
||||
for (const name of [nameSingular, namePlural]) {
|
||||
// Length too long check
|
||||
if (name.length > IDENTIFIER_MAX_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `Name is too long`,
|
||||
userFriendlyMessage: msg`Name is too long`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
// Length too short check
|
||||
if (name.length < IDENTIFIER_MIN_CHAR_LENGTH) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `Name is too short`,
|
||||
userFriendlyMessage: msg`Name is too short`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
// CamelCase check
|
||||
if (name !== camelCase(name)) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `Name should be in camelCase`,
|
||||
userFriendlyMessage: msg`Name should be in camelCase`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
// Format check
|
||||
if (
|
||||
!name.match(
|
||||
STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX,
|
||||
)
|
||||
) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `Name is not valid: it must start with lowercase letter and contain only alphanumeric letters`,
|
||||
userFriendlyMessage: msg`Name is not valid: it must start with lowercase letter and contain only alphanumeric letters`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
|
||||
// Reserved keywords check
|
||||
if (RESERVED_METADATA_NAME_KEYWORDS.includes(name)) {
|
||||
errors.push({
|
||||
code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||
message: `This name is reserved. Use a different name or the system will add "Custom" suffix.`,
|
||||
userFriendlyMessage: msg`This name is reserved. Use a different name or the system will add "Custom" suffix.`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if names are identical
|
||||
const namesAreIdentical =
|
||||
namePlural.trim().toLowerCase() === nameSingular.trim().toLowerCase();
|
||||
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
export type FlatMetadataValidator<T> = {
|
||||
validator: (value: T) => boolean;
|
||||
message: MessageDescriptor;
|
||||
};
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { RESERVED_METADATA_NAME_KEYWORDS } from 'twenty-shared/metadata';
|
||||
|
||||
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';
|
||||
|
||||
const STARTS_WITH_LOWER_CASE_AND_CONTAINS_ONLY_CAPS_AND_LOWER_LETTERS_AND_NUMBER_STRING_REGEX =
|
||||
/^[a-z][a-zA-Z0-9]*$/;
|
||||
|
||||
export const METADATA_NAME_VALIDATORS: FlatMetadataValidator<string>[] = [
|
||||
{
|
||||
message: msg`Name is too long`,
|
||||
validator: (name) => exceedsDatabaseIdentifierMaximumLength(name),
|
||||
},
|
||||
{
|
||||
message: msg`Name is too short`,
|
||||
validator: (name) => beneathDatabaseIdentifierMinimumLength(name),
|
||||
},
|
||||
{
|
||||
message: msg`Name should be in camelCase`,
|
||||
validator: (name) => name !== camelCase(name),
|
||||
},
|
||||
{
|
||||
message: msg`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,
|
||||
),
|
||||
},
|
||||
{
|
||||
// Safety net: Catch any reserved keywords that bypass frontend sanitization
|
||||
// (e.g., programmatic API access, old clients)
|
||||
// Frontend auto-adds "Custom" suffix, so properly formed requests will pass
|
||||
message: msg`This name is reserved. Use a different name or the system will add "Custom" suffix.`,
|
||||
validator: (name) => RESERVED_METADATA_NAME_KEYWORDS.includes(name),
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user