Add check for manifest uuid version (#20239)

As title

<img width="1059" height="203" alt="image"
src="https://github.com/user-attachments/assets/c6840c4e-792b-45da-b450-addd77af0de7"
/>
This commit is contained in:
martmull
2026-05-04 18:06:50 +02:00
committed by GitHub
parent 54e22423df
commit c804f27846
4 changed files with 196 additions and 15 deletions
@@ -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,
@@ -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',
@@ -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);
});
});
});
@@ -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<string>();
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');
}