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:
Charles Bochet
2026-04-12 17:03:09 +02:00
committed by GitHub
parent d9d3648baa
commit ad1a4ecca0
6 changed files with 169 additions and 16 deletions
@@ -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
@@ -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) {