From c804f27846b6a940454613b03d7a604c28baffdf Mon Sep 17 00:00:00 2001 From: martmull Date: Mon, 4 May 2026 18:06:50 +0200 Subject: [PATCH] Add check for manifest uuid version (#20239) As title image --- .../src/views/post-cards-by-status.view.ts | 10 +- .../app-dev/expected-manifest.ts | 10 +- .../__tests__/manifest-validate.spec.ts | 144 +++++++++++++++++- .../build/manifest/manifest-validate.ts | 47 +++++- 4 files changed, 196 insertions(+), 15 deletions(-) diff --git a/packages/twenty-apps/fixtures/rich-app/src/views/post-cards-by-status.view.ts b/packages/twenty-apps/fixtures/rich-app/src/views/post-cards-by-status.view.ts index 9bb1988a7a..a3adacd47c 100644 --- a/packages/twenty-apps/fixtures/rich-app/src/views/post-cards-by-status.view.ts +++ b/packages/twenty-apps/fixtures/rich-app/src/views/post-cards-by-status.view.ts @@ -30,31 +30,31 @@ export default defineView({ ], groups: [ { - universalIdentifier: 'bg1a2b3c-0001-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: 'e9ed34f1-3c3d-41b1-869b-00aae0033d9c', fieldValue: 'DRAFT', position: 0, isVisible: true, }, { - universalIdentifier: 'bg1a2b3c-0002-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: '19b1a3c1-53f0-4d32-b072-d645dac98e38', fieldValue: 'SENT', position: 1, isVisible: true, }, { - universalIdentifier: 'bg1a2b3c-0003-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: 'f545cb5a-370d-423f-9b4e-278a9a465bdf', fieldValue: 'DELIVERED', position: 2, isVisible: true, }, { - universalIdentifier: 'bg1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: '5d4c6d5f-af53-4cd0-a843-df38915561b2', fieldValue: 'RETURNED', position: 3, isVisible: true, }, { - universalIdentifier: 'bg1a2b3c-0005-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: '5ebbd7dc-9939-4594-b2a0-519269b4531f', fieldValue: 'LOST', position: 4, isVisible: true, diff --git a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts index 486804c6a9..b64787b78e 100644 --- a/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts +++ b/packages/twenty-sdk/src/cli/__tests__/apps/rich-app/__integration__/app-dev/expected-manifest.ts @@ -1512,31 +1512,31 @@ export const EXPECTED_MANIFEST: Manifest = { fieldValue: 'DRAFT', isVisible: true, position: 0, - universalIdentifier: 'bg1a2b3c-0001-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: 'e9ed34f1-3c3d-41b1-869b-00aae0033d9c', }, { fieldValue: 'SENT', isVisible: true, position: 1, - universalIdentifier: 'bg1a2b3c-0002-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: '19b1a3c1-53f0-4d32-b072-d645dac98e38', }, { fieldValue: 'DELIVERED', isVisible: true, position: 2, - universalIdentifier: 'bg1a2b3c-0003-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: 'f545cb5a-370d-423f-9b4e-278a9a465bdf', }, { fieldValue: 'RETURNED', isVisible: true, position: 3, - universalIdentifier: 'bg1a2b3c-0004-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: '5d4c6d5f-af53-4cd0-a843-df38915561b2', }, { fieldValue: 'LOST', isVisible: true, position: 4, - universalIdentifier: 'bg1a2b3c-0005-4a7b-8c9d-0e1f2a3b4c5d', + universalIdentifier: '5ebbd7dc-9939-4594-b2a0-519269b4531f', }, ], icon: 'IconLayoutKanban', diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts index 06fef5962a..faf972d49b 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-validate.spec.ts @@ -158,7 +158,7 @@ describe('manifestValidate', () => { ...validManifest, objects: [ { - universalIdentifier: 'obj-uuid', + universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', nameSingular: 'myObject', namePlural: 'myObjects', labelSingular: 'My Object', @@ -224,15 +224,16 @@ describe('manifestValidate', () => { ...validManifest, objects: [ { - universalIdentifier: 'obj-uuid', + universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', nameSingular: 'recipient', namePlural: 'recipients', labelSingular: 'Recipient', labelPlural: 'Recipients', - labelIdentifierFieldMetadataUniversalIdentifier: 'label-field-uuid', + labelIdentifierFieldMetadataUniversalIdentifier: + '7c9e6679-7425-40de-944b-e07fc1f90ae7', fields: [ { - universalIdentifier: 'label-field-uuid', + universalIdentifier: '7c9e6679-7425-40de-944b-e07fc1f90ae7', type: FieldMetadataType.TEXT, name: 'name', label: 'Name', @@ -361,4 +362,139 @@ describe('manifestValidate', () => { expect(result.errors[0]).toContain('invalid relationType'); }); }); + + describe('UUID version validation', () => { + it('should pass with UUID v4 identifiers', () => { + const result = manifestValidate({ + ...validManifest, + fields: [validField], + }); + + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should pass with UUID v5 identifiers', () => { + const v5Field: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: '21f7f8de-8051-5b89-8680-0195ef798b6a', + type: FieldMetadataType.TEXT, + name: 'v5Field', + label: 'V5 Field', + }; + + const result = manifestValidate({ + ...validManifest, + fields: [v5Field], + }); + + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should fail with UUID v1 identifiers', () => { + const v1Uuid = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + const v1Field: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: v1Uuid, + type: FieldMetadataType.TEXT, + name: 'v1Field', + label: 'V1 Field', + }; + + const result = manifestValidate({ + ...validManifest, + fields: [v1Field], + }); + + expect(result.isValid).toBe(false); + expect(result.errors).toContainEqual( + expect.stringContaining(`"${v1Uuid}" is UUID version 1`), + ); + expect(result.errors).toContainEqual( + expect.stringContaining('Only UUID version 4 or higher is allowed'), + ); + }); + + it('should fail with UUID v3 identifiers', () => { + const v3Uuid = 'a3bb189e-8bf9-3888-9912-ace4e6543002'; + const v3Field: FieldManifest = { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: v3Uuid, + type: FieldMetadataType.TEXT, + name: 'v3Field', + label: 'V3 Field', + }; + + const result = manifestValidate({ + ...validManifest, + fields: [v3Field], + }); + + expect(result.isValid).toBe(false); + expect(result.errors).toContainEqual( + expect.stringContaining(`"${v3Uuid}" is UUID version 3`), + ); + }); + + it('should fail with non-UUID universal identifiers', () => { + const result = manifestValidate({ + ...validManifest, + objects: [ + { + universalIdentifier: 'not-a-uuid', + nameSingular: 'myObject', + namePlural: 'myObjects', + labelSingular: 'My Object', + labelPlural: 'My Objects', + labelIdentifierFieldMetadataUniversalIdentifier: + '550e8400-e29b-41d4-a716-446655440030', + fields: [ + { + universalIdentifier: '550e8400-e29b-41d4-a716-446655440030', + type: FieldMetadataType.TEXT, + name: 'name', + label: 'Name', + }, + ], + }, + ], + }); + + expect(result.isValid).toBe(false); + expect(result.errors).toContainEqual( + expect.stringContaining('"not-a-uuid" is not a valid UUID'), + ); + }); + + it('should not report duplicate version errors for the same identifier', () => { + const v1Uuid = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + + const result = manifestValidate({ + ...validManifest, + fields: [ + { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: v1Uuid, + type: FieldMetadataType.TEXT, + name: 'field1', + label: 'Field 1', + }, + { + objectUniversalIdentifier: '20202020-b374-4779-a561-80086cb2e17f', + universalIdentifier: v1Uuid, + type: FieldMetadataType.TEXT, + name: 'field2', + label: 'Field 2', + }, + ], + }); + + const versionErrors = result.errors.filter((e) => + e.includes('is UUID version 1'), + ); + + expect(versionErrors).toHaveLength(1); + }); + }); }); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts index 320e4fde6b..721802e11b 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-validate.ts @@ -1,7 +1,11 @@ +import { validate as uuidValidate, version as uuidVersion } from 'uuid'; + import { type FieldManifest, type Manifest } from 'twenty-shared/application'; import { FieldMetadataType, RelationType } from 'twenty-shared/types'; import { isNonEmptyArray } from 'twenty-shared/utils'; +const MIN_UUID_VERSION = 4; + const RELATION_FIELD_TYPES: string[] = [ FieldMetadataType.RELATION, FieldMetadataType.MORPH_RELATION, @@ -99,16 +103,57 @@ const validateRelationFields = ( return errors; }; +const invalidUniversalIdentifierVersions = ( + identifiers: string[], +): string[] => { + const errors: string[] = []; + const seen = new Set(); + + for (const identifier of identifiers) { + if (seen.has(identifier)) { + continue; + } + seen.add(identifier); + + if (!uuidValidate(identifier)) { + errors.push(`Universal identifier "${identifier}" is not a valid UUID.`); + continue; + } + + const version = uuidVersion(identifier); + + if (version < MIN_UUID_VERSION) { + errors.push( + `Universal identifier "${identifier}" is UUID version ${version}. ` + + `Only UUID version ${MIN_UUID_VERSION} or higher is allowed.`, + ); + } + } + + return errors; +}; + export const manifestValidate = (manifest: Manifest) => { const errors: string[] = []; const warnings: string[] = []; - const duplicates = extractDuplicates(findUniversalIdentifiers(manifest)); + const universalIdentifiers = findUniversalIdentifiers(manifest); + + const duplicates = extractDuplicates(universalIdentifiers); if (duplicates.length > 0) { errors.push(`Duplicate universal identifiers: ${duplicates.join(', ')}`); } + const invalidUniversalIdentifiers = + invalidUniversalIdentifierVersions(universalIdentifiers); + + if (invalidUniversalIdentifiers.length > 0) { + errors.push( + `Duplicate universal identifiers: ${invalidUniversalIdentifiers.join(', ')}`, + ); + } + if (!isNonEmptyArray(manifest.objects)) { warnings.push('No object defined'); }