Add isUnique support for application-defined fields (#19609)
## Summary - Adds `isUnique?: boolean` to `RegularFieldManifest` in `twenty-shared`, allowing SDK applications to declare unique constraints on fields - Updates the manifest-to-flat-field converter to read `isUnique` from the manifest instead of hardcoding `false` - Generates corresponding unique index metadata in `computeApplicationManifestAllUniversalFlatEntityMaps` when a field has `isUnique: true`, matching the behavior of the `CreateFieldInput` path - Adds SDK-side validation rejecting `isUnique` on RELATION, MORPH_RELATION, and FILES field types - Adds integration test verifying manifest sync creates a unique index for `isUnique` fields - Adds SDK unit tests for `isUnique` validation on unsupported field types ## Test plan - [x] SDK unit tests: `defineField` accepts `isUnique: true` on TEXT, rejects on RELATION and FILES - [ ] Integration test: manifest sync with `isUnique: true` creates the unique index in DB - [ ] Verify `isUnique` defaults to `false` when not specified (backward compatible) - [ ] Verify standalone manifest fields (not nested in objects) also generate unique indexes correctly
This commit is contained in:
+1
-1
@@ -83,7 +83,7 @@ export const fromFieldManifestToUniversalFlatFieldMetadata = ({
|
||||
isSystem: fieldManifest.name in PARTIAL_SYSTEM_FLAT_FIELD_METADATAS,
|
||||
isUIReadOnly: false,
|
||||
isNullable: fieldManifest.isNullable ?? true,
|
||||
isUnique: false,
|
||||
isUnique: fieldManifest.isUnique ?? false,
|
||||
isLabelSyncedWithName: false,
|
||||
morphId:
|
||||
fieldManifest.type === FieldMetadataType.MORPH_RELATION
|
||||
|
||||
+52
-15
@@ -2,6 +2,8 @@ import { type Manifest } from 'twenty-shared/application';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { generateIndexForFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/generate-index-for-flat-field-metadata.util';
|
||||
|
||||
import { fromCommandMenuItemManifestToUniversalFlatCommandMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-command-menu-item-manifest-to-universal-flat-command-menu-item.util';
|
||||
import { fromFieldManifestToUniversalFlatFieldMetadata } from 'src/engine/core-modules/application/application-manifest/converters/from-field-manifest-to-universal-flat-field-metadata.util';
|
||||
import { fromFieldPermissionManifestToUniversalFlatFieldPermission } from 'src/engine/core-modules/application/application-manifest/converters/from-field-permission-manifest-to-universal-flat-field-permission.util';
|
||||
@@ -44,12 +46,14 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
ownerFlatApplication;
|
||||
|
||||
for (const objectManifest of manifest.objects) {
|
||||
const flatObjectMetadata = fromObjectManifestToUniversalFlatObjectMetadata({
|
||||
objectManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: fromObjectManifestToUniversalFlatObjectMetadata({
|
||||
objectManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntity: flatObjectMetadata,
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatObjectMetadataMaps,
|
||||
});
|
||||
@@ -71,28 +75,61 @@ export const computeApplicationManifestAllUniversalFlatEntityMaps = ({
|
||||
objectUniversalIdentifier: objectManifest.universalIdentifier,
|
||||
};
|
||||
|
||||
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata({
|
||||
fieldManifest: enrichedFieldManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({
|
||||
fieldManifest: enrichedFieldManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntity: flatFieldMetadata,
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (flatFieldMetadata.isUnique) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: generateIndexForFlatFieldMetadata({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatIndexMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const fieldManifest of manifest.fields) {
|
||||
const flatFieldMetadata = fromFieldManifestToUniversalFlatFieldMetadata({
|
||||
fieldManifest: fieldManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: fromFieldManifestToUniversalFlatFieldMetadata({
|
||||
fieldManifest: fieldManifest,
|
||||
applicationUniversalIdentifier,
|
||||
now,
|
||||
}),
|
||||
universalFlatEntity: flatFieldMetadata,
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (flatFieldMetadata.isUnique) {
|
||||
const flatObjectMetadata =
|
||||
allUniversalFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
flatFieldMetadata.objectMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(flatObjectMetadata)) {
|
||||
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
|
||||
universalFlatEntity: generateIndexForFlatFieldMetadata({
|
||||
flatFieldMetadata,
|
||||
flatObjectMetadata,
|
||||
}),
|
||||
universalFlatEntityMapsToMutate:
|
||||
allUniversalFlatEntityMaps.flatIndexMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const logicFunctionManifest of manifest.logicFunctions) {
|
||||
|
||||
Reference in New Issue
Block a user