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:
@@ -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,
|
||||
|
||||
+5
-5
@@ -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',
|
||||
|
||||
+140
-4
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user