diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/DatabaseIdentifierMaximumLength.ts b/packages/twenty-front/src/modules/settings/data-model/constants/DatabaseIdentifierMaximumLength.ts deleted file mode 100644 index d7ac7dd9a2..0000000000 --- a/packages/twenty-front/src/modules/settings/data-model/constants/DatabaseIdentifierMaximumLength.ts +++ /dev/null @@ -1 +0,0 @@ -export const DATABASE_IDENTIFIER_MAXIMUM_LENGTH = 63; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/FieldNameMaximumLength.ts b/packages/twenty-front/src/modules/settings/data-model/constants/FieldNameMaximumLength.ts index bd647b2cbe..6cd437b278 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/FieldNameMaximumLength.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/FieldNameMaximumLength.ts @@ -1,3 +1,3 @@ -import { DATABASE_IDENTIFIER_MAXIMUM_LENGTH } from '@/settings/data-model/constants/DatabaseIdentifierMaximumLength'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; -export const FIELD_NAME_MAXIMUM_LENGTH = DATABASE_IDENTIFIER_MAXIMUM_LENGTH; +export const FIELD_NAME_MAXIMUM_LENGTH = IDENTIFIER_MAX_CHAR_LENGTH; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/ObjectNameMaximumLength.ts b/packages/twenty-front/src/modules/settings/data-model/constants/ObjectNameMaximumLength.ts index 6ba3692374..3a1e0106a4 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/ObjectNameMaximumLength.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/ObjectNameMaximumLength.ts @@ -1,3 +1,3 @@ -import { DATABASE_IDENTIFIER_MAXIMUM_LENGTH } from '@/settings/data-model/constants/DatabaseIdentifierMaximumLength'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; -export const OBJECT_NAME_MAXIMUM_LENGTH = DATABASE_IDENTIFIER_MAXIMUM_LENGTH; +export const OBJECT_NAME_MAXIMUM_LENGTH = IDENTIFIER_MAX_CHAR_LENGTH; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/OptionValueMaximumLength.ts b/packages/twenty-front/src/modules/settings/data-model/constants/OptionValueMaximumLength.ts index 97f5cc7726..b0f95616fb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/OptionValueMaximumLength.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/OptionValueMaximumLength.ts @@ -1,3 +1,3 @@ -import { DATABASE_IDENTIFIER_MAXIMUM_LENGTH } from '@/settings/data-model/constants/DatabaseIdentifierMaximumLength'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; -export const OPTION_VALUE_MAXIMUM_LENGTH = DATABASE_IDENTIFIER_MAXIMUM_LENGTH; +export const OPTION_VALUE_MAXIMUM_LENGTH = IDENTIFIER_MAX_CHAR_LENGTH; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx index 74d2b51446..dc15ebe11b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx @@ -8,7 +8,7 @@ import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fi import { AdvancedSettingsContentWrapperWithDot } from '@/settings/components/AdvancedSettingsContentWrapperWithDot'; import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper'; import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle'; -import { DATABASE_IDENTIFIER_MAXIMUM_LENGTH } from '@/settings/data-model/constants/DatabaseIdentifierMaximumLength'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; @@ -202,7 +202,7 @@ export const SettingsDataModelFieldIconLabelForm = ({ readOnly={readonly} disabled={!isNameEditEnabled} fullWidth - maxLength={DATABASE_IDENTIFIER_MAXIMUM_LENGTH} + maxLength={IDENTIFIER_MAX_CHAR_LENGTH} RightIcon={() => apiNameTooltipText && ( <> diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index a09ffde20c..acc356a15a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -25,7 +25,7 @@ import { Card } from 'twenty-ui/layout'; import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants'; import { type StringKeyOf } from 'type-fest'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/computeMetadataNameFromLabel'; +import { computeMetadataNamesFromLabels } from '~/pages/settings/data-model/utils/computeMetadataNamesFromLabels'; type SettingsDataModelObjectAboutFormProps = { disableEdition?: boolean; @@ -146,25 +146,24 @@ export const SettingsDataModelObjectAboutForm = ({ shouldValidate: true, }); if (isLabelSyncedWithName) { - fillNamePluralFromLabelPlural(labelPluralFromSingularLabel); + fillNamesFromLabels(labelSingular, labelPluralFromSingularLabel); } }; - const fillNameSingularFromLabelSingular = ( - labelSingular: string | undefined, + const fillNamesFromLabels = ( + currentLabelSingular: string, + currentLabelPlural: string, ) => { - if (!isDefined(labelSingular)) return; + const { nameSingular, namePlural } = computeMetadataNamesFromLabels( + currentLabelSingular, + currentLabelPlural, + ); - setValue('nameSingular', computeMetadataNameFromLabel(labelSingular), { + setValue('nameSingular', nameSingular, { shouldDirty: true, shouldValidate: true, }); - }; - - const fillNamePluralFromLabelPlural = (labelPlural: string | undefined) => { - if (!isDefined(labelPlural)) return; - - setValue('namePlural', computeMetadataNameFromLabel(labelPlural), { + setValue('namePlural', namePlural, { shouldDirty: true, shouldValidate: true, }); @@ -215,9 +214,6 @@ export const SettingsDataModelObjectAboutForm = ({ onChange={(value) => { onChange(capitalize(value)); fillLabelPlural(capitalize(value)); - if (isLabelSyncedWithName === true) { - fillNameSingularFromLabelSingular(value); - } }} onBlur={() => onNewDirtyField?.()} disabled={disableEdition} @@ -243,7 +239,7 @@ export const SettingsDataModelObjectAboutForm = ({ onChange={(value) => { onChange(capitalize(value)); if (isLabelSyncedWithName === true) { - fillNamePluralFromLabelPlural(value); + fillNamesFromLabels(labelSingular, capitalize(value)); } }} onBlur={() => onNewDirtyField?.()} @@ -411,8 +407,7 @@ export const SettingsDataModelObjectAboutForm = ({ value === true && (isCustomObject || isbeingCreatedObject) ) { - fillNamePluralFromLabelPlural(labelPlural); - fillNameSingularFromLabelSingular(labelSingular); + fillNamesFromLabels(labelSingular, labelPlural); } onNewDirtyField?.(); }} diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap index 7e23594975..346d195422 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/__snapshots__/settingsDataModelObjectAboutFormSchema.test.ts.snap @@ -34,20 +34,6 @@ exports[`settingsDataModelObjectAboutFormSchema fails when labels are empty stri "labelPlural" ], "message": "Too small: expected string to have >=1 characters" - }, - { - "code": "custom", - "message": "Singular and plural labels must be different", - "path": [ - "labelPlural" - ] - }, - { - "code": "custom", - "message": "Singular and plural labels must be different", - "path": [ - "labelSingular" - ] } ]] `; @@ -92,20 +78,20 @@ exports[`settingsDataModelObjectAboutFormSchema fails when required fields are m ]] `; -exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural labels are the same 1`] = ` +exports[`settingsDataModelObjectAboutFormSchema fails when singular and plural labels are the same and names are the same 1`] = ` [ZodError: [ { "code": "custom", - "message": "Singular and plural labels must be different", + "message": "Singular and plural names must be different", "path": [ - "labelPlural" + "nameSingular" ] }, { "code": "custom", - "message": "Singular and plural labels must be different", + "message": "Singular and plural names must be different", "path": [ - "labelSingular" + "namePlural" ] } ]] diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts index 0eae15fc67..a7e282cbfe 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/__tests__/settingsDataModelObjectAboutFormSchema.test.ts @@ -50,6 +50,19 @@ describe('settingsDataModelObjectAboutFormSchema', () => { expectedSuccess: true, }, }, + { + title: 'validates input with identical labels but different names', + context: { + input: { + ...validInput, + labelSingular: 'Sheep', + labelPlural: 'Sheep', + nameSingular: 'sheep', + namePlural: 'sheeps', + }, + expectedSuccess: true, + }, + }, ]; const failsValidationTestsUseCase: EachTestingContext<{ @@ -90,12 +103,15 @@ describe('settingsDataModelObjectAboutFormSchema', () => { }, }, { - title: 'fails when singular and plural labels are the same', + title: + 'fails when singular and plural labels are the same and names are the same', context: { input: { ...validInput, labelPlural: 'Same Label', labelSingular: 'Same Label', + namePlural: 'sameName', + nameSingular: 'sameName', }, expectedSuccess: false, }, diff --git a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts index 7b8badcb1e..afaa151e18 100644 --- a/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts +++ b/packages/twenty-front/src/modules/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema.ts @@ -30,23 +30,7 @@ const settingsDataModelFormFieldsSchema = z.object({ export const settingsDataModelObjectAboutFormSchema = settingsDataModelFormFieldsSchema.superRefine( - ({ labelPlural, labelSingular, namePlural, nameSingular }, ctx) => { - const labelsAreDifferent = - labelPlural.trim().toLowerCase() !== labelSingular.trim().toLowerCase(); - if (!labelsAreDifferent) { - const labelFields: ReadonlyKeysArray = [ - 'labelPlural', - 'labelSingular', - ]; - labelFields.forEach((field) => - ctx.addIssue({ - code: 'custom', - message: t`Singular and plural labels must be different`, - path: [field], - }), - ); - } - + ({ namePlural, nameSingular }, ctx) => { const nameAreDifferent = nameSingular.toLowerCase() !== namePlural.toLowerCase(); if (!nameAreDifferent) { diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNamesFromLabels.ts b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNamesFromLabels.ts new file mode 100644 index 0000000000..563f5f7938 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNamesFromLabels.ts @@ -0,0 +1,15 @@ +import { computeMetadataNamesFromLabelsOrThrow } from 'twenty-shared/metadata'; + +export const computeMetadataNamesFromLabels = ( + labelSingular: string, + labelPlural: string, +): { nameSingular: string; namePlural: string } => { + try { + return computeMetadataNamesFromLabelsOrThrow({ + labelSingular, + labelPlural, + }); + } catch { + return { nameSingular: '', namePlural: '' }; + } +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts index 273eaebc5f..4f99188124 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-enum-flat-field-metadata.util.ts @@ -17,7 +17,7 @@ import { import { FieldMetadataExceptionCode } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; 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 { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants'; import { type UniversalFlatFieldMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-field-metadata.type'; import { isSnakeCaseString } from 'src/utils/is-snake-case-string'; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts index 005f3cef0b..37958b0d52 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/validators/utils/validate-flat-field-metadata-name.util.ts @@ -1,9 +1,11 @@ import { msg, t } from '@lingui/core/macro'; -import { RESERVED_METADATA_NAME_KEYWORDS } from 'twenty-shared/metadata'; +import { + IDENTIFIER_MAX_CHAR_LENGTH, + 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 { 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 { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util'; import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type'; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts index e04e669af8..ddf0e20ded 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts @@ -1,4 +1,4 @@ -import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; +import { computeMetadataNamesFromLabelsOrThrow } from 'twenty-shared/metadata'; import { isCallerTwentyStandardApp } from 'src/engine/metadata-modules/utils/is-caller-twenty-standard-app.util'; import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; @@ -14,15 +14,12 @@ export const areFlatObjectMetadataNamesSyncedWithLabels = ({ 'namePlural' | 'nameSingular' | 'labelPlural' | 'labelSingular' >; }) => { - const [computedSingularName, computedPluralName] = [ - flatObjectMetadata.labelSingular, - flatObjectMetadata.labelPlural, - ].map((label) => - computeMetadataNameFromLabel({ - label, + const { nameSingular: computedSingularName, namePlural: computedPluralName } = + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: flatObjectMetadata.labelSingular, + labelPlural: flatObjectMetadata.labelPlural, applyCustomSuffix: !isCallerTwentyStandardApp(buildOptions), - }), - ); + }); return ( flatObjectMetadata.nameSingular === computedSingularName && diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts index dc6f168284..c3e3a41056 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util.ts @@ -3,7 +3,7 @@ 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 ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type'; import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants'; import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; @@ -37,18 +37,5 @@ export const validateFlatObjectMetadataLabel = ({ } } - // Check if labels are identical - const labelsAreIdentical = - labelSingular.trim().toLowerCase() === labelPlural.trim().toLowerCase(); - - if (labelsAreIdentical) { - errors.push({ - code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, - message: `The singular and plural labels cannot be the same for an object`, - userFriendlyMessage: msg`The singular and plural labels cannot be the same for an object`, - value: labelSingular, - }); - } - return errors; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts index 0f30ecb26f..f9d6109d6d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util.ts @@ -1,10 +1,12 @@ import { msg } from '@lingui/core/macro'; -import { RESERVED_METADATA_NAME_KEYWORDS } from 'twenty-shared/metadata'; +import { + IDENTIFIER_MAX_CHAR_LENGTH, + 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 ObjectMetadataMinimalInformation } from 'src/engine/metadata-modules/flat-object-metadata/types/object-metadata-minimal-information.type'; import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; -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 UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type'; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants.ts b/packages/twenty-server/src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants.ts deleted file mode 100644 index ce2dabd729..0000000000 --- a/packages/twenty-server/src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const IDENTIFIER_MAX_CHAR_LENGTH = 63; diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts index 2776fb920e..fe22aacb4d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts @@ -1,4 +1,4 @@ -import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'twenty-shared/metadata'; import { IDENTIFIER_MIN_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-min-char-length.constants'; export const exceedsDatabaseIdentifierMaximumLength = (string: string) => diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap index 042ab02579..da90bf50f8 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/__snapshots__/failing-create-one-object-metadata-v2.integration-spec.ts.snap @@ -2950,1470 +2950,6 @@ exports[`Object metadata creation should fail v2 when labelSingular is empty 1`] } `; -exports[`Object metadata creation should fail v2 when labels are identical 1`] = ` -{ - "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "name", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "id", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "createdAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "createdBy", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "deletedAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "position", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "searchVector", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "updatedAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "updatedBy", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "timelineActivities", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "favorites", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "attachments", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "noteTargets", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "taskTargets", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "listinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - ], - "index": [ - { - "errors": [ - { - "code": "INDEX_OBJECT_NOT_FOUND", - "message": "Could not find index related object metadata", - "userFriendlyMessage": "Index related object not found", - }, - { - "code": "INDEX_FIELD_NOT_FOUND", - "message": "Could not find index field related field metadata", - "userFriendlyMessage": "Field referenced in index does not exist", - }, - ], - "flatEntityMinimalInformation": { - "name": "IDX_ffebb98ec981552dbecc71223c8", - "universalIdentifier": Any, - }, - "metadataName": "index", - "status": "fail", - "type": "create", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "The singular and plural labels cannot be the same for an object", - "userFriendlyMessage": "The singular and plural labels cannot be the same for an object", - "value": "FooBar", - }, - ], - "flatEntityMinimalInformation": { - "namePlural": "listingas", - "nameSingular": "listinga", - "universalIdentifier": Any, - }, - "metadataName": "objectMetadata", - "status": "fail", - "type": "create", - }, - ], - "view": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "universalIdentifier": Any, - }, - "metadataName": "view", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "universalIdentifier": Any, - }, - "metadataName": "view", - "status": "fail", - "type": "create", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - ], - }, - "message": "Validation failed for 19 fieldMetadatas, 1 objectMetadata, 2 views, 14 viewFields, 1 index", - "summary": { - "fieldMetadata": 19, - "index": 1, - "objectMetadata": 1, - "totalErrors": 37, - "view": 2, - "viewField": 14, - }, - "userFriendlyMessage": "Metadata validation failed", - }, - "message": "Multiple validation errors occurred while creating object", - "name": "GraphQLError", -} -`; - -exports[`Object metadata creation should fail v2 when labels with whitespaces result to be identical 1`] = ` -{ - "extensions": { - "code": "METADATA_VALIDATION_FAILED", - "errors": { - "fieldMetadata": [ - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "name", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "id", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "createdAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "createdBy", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "deletedAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "position", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "searchVector", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "updatedAt", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "updatedBy", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "timelineActivities", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "favorites", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "attachments", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "noteTargets", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Object metadata not found", - "userFriendlyMessage": "Field to create related object not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "taskTargets", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "listinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "OBJECT_METADATA_NOT_FOUND", - "message": "Relation target object metadata not found", - "userFriendlyMessage": "Object targeted by the relation not found", - }, - ], - "flatEntityMinimalInformation": { - "name": "targetListinga", - "objectMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - }, - "metadataName": "fieldMetadata", - "status": "fail", - "type": "create", - }, - ], - "index": [ - { - "errors": [ - { - "code": "INDEX_OBJECT_NOT_FOUND", - "message": "Could not find index related object metadata", - "userFriendlyMessage": "Index related object not found", - }, - { - "code": "INDEX_FIELD_NOT_FOUND", - "message": "Could not find index field related field metadata", - "userFriendlyMessage": "Field referenced in index does not exist", - }, - ], - "flatEntityMinimalInformation": { - "name": "IDX_ffebb98ec981552dbecc71223c8", - "universalIdentifier": Any, - }, - "metadataName": "index", - "status": "fail", - "type": "create", - }, - ], - "objectMetadata": [ - { - "errors": [ - { - "code": "INVALID_OBJECT_INPUT", - "message": "The singular and plural labels cannot be the same for an object", - "userFriendlyMessage": "The singular and plural labels cannot be the same for an object", - "value": "FooBar", - }, - ], - "flatEntityMinimalInformation": { - "namePlural": "listingas", - "nameSingular": "listinga", - "universalIdentifier": Any, - }, - "metadataName": "objectMetadata", - "status": "fail", - "type": "create", - }, - ], - "view": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "universalIdentifier": Any, - }, - "metadataName": "view", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Object metadata not found", - "userFriendlyMessage": "Object metadata not found", - }, - ], - "flatEntityMinimalInformation": { - "universalIdentifier": Any, - }, - "metadataName": "view", - "status": "fail", - "type": "create", - }, - ], - "viewField": [ - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - { - "errors": [ - { - "code": "INVALID_VIEW_DATA", - "message": "Field metadata not found", - "userFriendlyMessage": "Field metadata not found", - }, - { - "code": "INVALID_VIEW_DATA", - "message": "View not found", - "userFriendlyMessage": "View not found", - }, - ], - "flatEntityMinimalInformation": { - "fieldMetadataUniversalIdentifier": Any, - "universalIdentifier": Any, - "viewUniversalIdentifier": Any, - }, - "metadataName": "viewField", - "status": "fail", - "type": "create", - }, - ], - }, - "message": "Validation failed for 19 fieldMetadatas, 1 objectMetadata, 2 views, 14 viewFields, 1 index", - "summary": { - "fieldMetadata": 19, - "index": 1, - "objectMetadata": 1, - "totalErrors": 37, - "view": 2, - "viewField": 14, - }, - "userFriendlyMessage": "Metadata validation failed", - }, - "message": "Multiple validation errors occurred while creating object", - "name": "GraphQLError", -} -`; - exports[`Object metadata creation should fail v2 when name exceeds maximum length 1`] = ` { "extensions": { diff --git a/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts b/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts index 60ccc1723c..b7cb7c0430 100644 --- a/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts +++ b/packages/twenty-server/test/integration/metadata/suites/object-metadata/common/object-metadata-label-failing-tests-cases.ts @@ -29,18 +29,4 @@ export const OBJECT_METADATA_LABEL_FAILING_TEST_CASES: EachTestingContext< title: 'when labelPlural contains only whitespace', context: { labelPlural: ' ' }, }, - { - title: 'when labels are identical', - context: { - labelPlural: 'fooBar', - labelSingular: 'fooBar', - }, - }, - { - title: 'when labels with whitespaces result to be identical', - context: { - labelPlural: ' fooBar ', - labelSingular: 'fooBar', - }, - }, ]; diff --git a/packages/twenty-shared/src/metadata/__tests__/computeMetadataNamesFromLabelsOrThrow.test.ts b/packages/twenty-shared/src/metadata/__tests__/computeMetadataNamesFromLabelsOrThrow.test.ts new file mode 100644 index 0000000000..3bc8139030 --- /dev/null +++ b/packages/twenty-shared/src/metadata/__tests__/computeMetadataNamesFromLabelsOrThrow.test.ts @@ -0,0 +1,96 @@ +import { computeMetadataNamesFromLabelsOrThrow } from '@/metadata/utils/compute-metadata-names-from-labels-or-throw.util'; + +describe('computeMetadataNamesFromLabelsOrThrow', () => { + it('should compute different names from different labels', () => { + expect( + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Company', + labelPlural: 'Companies', + }), + ).toEqual({ + nameSingular: 'company', + namePlural: 'companies', + }); + }); + + it('should append "s" to plural name when labels produce identical names', () => { + expect( + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Sheep', + labelPlural: 'Sheep', + }), + ).toEqual({ + nameSingular: 'sheep', + namePlural: 'sheeps', + }); + }); + + it('should handle German words with identical singular and plural', () => { + expect( + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Unternehmen', + labelPlural: 'Unternehmen', + }), + ).toEqual({ + nameSingular: 'unternehmen', + namePlural: 'unternehmens', + }); + }); + + it('should not append "s" when labels produce different names', () => { + expect( + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Person', + labelPlural: 'People', + }), + ).toEqual({ + nameSingular: 'person', + namePlural: 'people', + }); + }); + + it('should handle empty labels gracefully', () => { + expect( + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: '', + labelPlural: '', + }), + ).toEqual({ + nameSingular: '', + namePlural: '', + }); + }); + + it('should apply custom suffix for reserved words by default', () => { + const result = computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Job', + labelPlural: 'Jobs', + }); + + expect(result.nameSingular).toBe('jobCustom'); + expect(result.namePlural).toBe('jobsCustom'); + }); + + it('should skip custom suffix when applyCustomSuffix is false', () => { + const result = computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Job', + labelPlural: 'Jobs', + applyCustomSuffix: false, + }); + + expect(result.nameSingular).toBe('job'); + expect(result.namePlural).toBe('jobs'); + }); + + it('should handle case-insensitive collision after camelCase conversion', () => { + expect( + computeMetadataNamesFromLabelsOrThrow({ + labelSingular: 'Aircraft', + labelPlural: 'Aircraft', + }), + ).toEqual({ + nameSingular: 'aircraft', + namePlural: 'aircrafts', + }); + }); +}); diff --git a/packages/twenty-shared/src/metadata/constants/identifier-max-char-length.constant.ts b/packages/twenty-shared/src/metadata/constants/identifier-max-char-length.constant.ts new file mode 100644 index 0000000000..2b9c601f59 --- /dev/null +++ b/packages/twenty-shared/src/metadata/constants/identifier-max-char-length.constant.ts @@ -0,0 +1,2 @@ +// PostgreSQL max identifier length (NAMEDATALEN - 1) +export const IDENTIFIER_MAX_CHAR_LENGTH = 63; diff --git a/packages/twenty-shared/src/metadata/index.ts b/packages/twenty-shared/src/metadata/index.ts index 933edeedbf..d6b5c64224 100644 --- a/packages/twenty-shared/src/metadata/index.ts +++ b/packages/twenty-shared/src/metadata/index.ts @@ -14,6 +14,7 @@ export { } from './check-if-field-is-label-identifier.util'; export { ALL_METADATA_NAME } from './constants/all-metadata-name.constant'; export { DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS } from './constants/default-relations-object-standard-ids.constant'; +export { IDENTIFIER_MAX_CHAR_LENGTH } from './constants/identifier-max-char-length.constant'; export { RESERVED_METADATA_NAME_KEYWORDS } from './constants/reserved-metadata-name-keywords.constant'; export { STANDARD_OBJECTS } from './constants/standard-object.constant'; export type { AllMetadataName } from './types/all-metadata-name.type'; @@ -25,3 +26,4 @@ export type { export { WorkspaceMigrationV2ExceptionCode } from './types/MetadataValidationError'; export { addCustomSuffixIfIsReserved } from './utils/add-custom-suffix-if-reserved.util'; export { computeMetadataNameFromLabel } from './utils/compute-metadata-name-from-label.util'; +export { computeMetadataNamesFromLabelsOrThrow } from './utils/compute-metadata-names-from-labels-or-throw.util'; diff --git a/packages/twenty-shared/src/metadata/utils/compute-metadata-names-from-labels-or-throw.util.ts b/packages/twenty-shared/src/metadata/utils/compute-metadata-names-from-labels-or-throw.util.ts new file mode 100644 index 0000000000..b1a0fb19a4 --- /dev/null +++ b/packages/twenty-shared/src/metadata/utils/compute-metadata-names-from-labels-or-throw.util.ts @@ -0,0 +1,28 @@ +import { IDENTIFIER_MAX_CHAR_LENGTH } from '@/metadata/constants/identifier-max-char-length.constant'; +import { computeMetadataNameFromLabel } from '@/metadata/utils/compute-metadata-name-from-label.util'; + +export const computeMetadataNamesFromLabelsOrThrow = ({ + labelSingular, + labelPlural, + applyCustomSuffix = true, +}: { + labelSingular: string; + labelPlural: string; + applyCustomSuffix?: boolean; +}): { nameSingular: string; namePlural: string } => { + const nameSingular = computeMetadataNameFromLabel({ + label: labelSingular, + applyCustomSuffix, + }); + + let namePlural = computeMetadataNameFromLabel({ + label: labelPlural, + applyCustomSuffix, + }); + + if (namePlural !== '' && namePlural === nameSingular) { + namePlural = (namePlural + 's').slice(0, IDENTIFIER_MAX_CHAR_LENGTH); + } + + return { nameSingular, namePlural }; +};