Unique field - add unique property creation/deletion on field (#13539)
Done : - add isUnique prop availability on gql update/create fieldMetadata resolvers - add unique index creation logic at update & creation - add unique index deletion logic at update - update unique index if field name updated - edge cases : can't have default value and unique fields / standard default value excluded from index (where clause) / can't have composite unique fields / can't create unique fields on MORPH closes https://github.com/twentyhq/core-team-issues/issues/1222
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock';
|
||||
|
||||
describe('computeUniqueIndexWhereClause', () => {
|
||||
it('should return undefined if standard default value is not defined', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'testField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return a where clause for a an atomic type field', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'testTextField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBe('"testTextField" != \'\'');
|
||||
});
|
||||
|
||||
it('should return a where clause for a composite type field', () => {
|
||||
const fieldMetadata = getMockFieldMetadataEntity({
|
||||
workspaceId: 'workspace-id',
|
||||
objectMetadataId: 'object-id',
|
||||
type: FieldMetadataType.EMAILS,
|
||||
name: 'testEmailsField',
|
||||
});
|
||||
|
||||
const result = computeUniqueIndexWhereClause(fieldMetadata);
|
||||
|
||||
expect(result).toBe('"testEmailsFieldPrimaryEmail" != \'\'');
|
||||
});
|
||||
});
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { validateCanCreateUniqueIndex } from 'src/engine/metadata-modules/index-metadata/utils/validate-can-create-unique-index.util';
|
||||
|
||||
describe('validateCanCreateUniqueIndex', () => {
|
||||
it('should throw an error if field to create is a MORPH type', () => {
|
||||
const field = {
|
||||
name: 'testField',
|
||||
type: FieldMetadataType.MORPH_RELATION,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
FieldMetadataException,
|
||||
);
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
'Unique index cannot be created for field testField of type MORPH_RELATION',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if field to create is a RELATION type - ONE_TO_MANY', () => {
|
||||
const field = {
|
||||
name: 'testField',
|
||||
type: FieldMetadataType.RELATION,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
FieldMetadataException,
|
||||
);
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
'Unique index cannot be created for field testField of type RELATION',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if field to create is a FULL_NAME type', () => {
|
||||
const field = {
|
||||
name: 'testField',
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
FieldMetadataException,
|
||||
);
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
'Unique index cannot be created for field testField of type FULL_NAME',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if field to create is an ADDRESS type', () => {
|
||||
const field = {
|
||||
name: 'testField',
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
isCustom: true,
|
||||
};
|
||||
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
FieldMetadataException,
|
||||
);
|
||||
expect(() => validateCanCreateUniqueIndex(field)).toThrow(
|
||||
'Unique index cannot be created for field testField of type ADDRESS',
|
||||
);
|
||||
});
|
||||
});
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
IndexMetadataException,
|
||||
IndexMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/index-metadata/index-field-metadata.exception';
|
||||
|
||||
export const computeUniqueIndexWhereClause = (
|
||||
fieldMetadata: Pick<FieldMetadataEntity, 'type' | 'name'>,
|
||||
) => {
|
||||
const defaultDefaultValue = generateDefaultValue(fieldMetadata.type);
|
||||
|
||||
if (!isDefined(defaultDefaultValue)) return;
|
||||
|
||||
if (
|
||||
fieldMetadata.type === FieldMetadataType.RELATION ||
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION
|
||||
) {
|
||||
throw new IndexMetadataException(
|
||||
`Unique index cannot be created for relation or morph relation field ${fieldMetadata.name}`,
|
||||
IndexMetadataExceptionCode.INDEX_NOT_SUPPORTED_FOR_MORH_RELATION_FIELD_AND_RELATION_FIELD,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
return `"${fieldMetadata.name}" != ${defaultDefaultValue}`;
|
||||
}
|
||||
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
|
||||
if (!isDefined(compositeType)) {
|
||||
throw new FieldMetadataException(
|
||||
`Composite type not found for field metadata type: ${fieldMetadata.type}`,
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const defaultDefaultValueProperties = Object.keys(defaultDefaultValue);
|
||||
|
||||
const columnNamesWithDefaultValues = compositeType.properties
|
||||
.filter(
|
||||
(property) =>
|
||||
property.isIncludedInUniqueConstraint &&
|
||||
defaultDefaultValueProperties.includes(property.name),
|
||||
)
|
||||
.map((property) => {
|
||||
const defaultValue =
|
||||
defaultDefaultValue[property.name as keyof typeof defaultDefaultValue];
|
||||
|
||||
if (isNonEmptyString(defaultValue)) {
|
||||
return [
|
||||
computeCompositeColumnName(fieldMetadata, property),
|
||||
defaultValue,
|
||||
];
|
||||
}
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return columnNamesWithDefaultValues.length > 0
|
||||
? columnNamesWithDefaultValues
|
||||
.map(
|
||||
([columnName, defaultValue]) => `"${columnName}" != ${defaultValue}`,
|
||||
)
|
||||
.join(' OR ')
|
||||
: undefined;
|
||||
};
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { type FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
export const validateCanCreateUniqueIndex = (
|
||||
field: Pick<FieldMetadataEntity, 'type' | 'name' | 'isCustom'>,
|
||||
) => {
|
||||
if (field.isCustom === false)
|
||||
throw new FieldMetadataException(
|
||||
`Unique index cannot be created on standard field`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`Standard fields cannot be unique.`,
|
||||
},
|
||||
);
|
||||
|
||||
const isCompositeFieldWithNonIncludedUniqueConstraint =
|
||||
isCompositeFieldMetadataType(field.type) &&
|
||||
!compositeTypeDefinitions
|
||||
.get(field.type)
|
||||
?.properties.some((property) => property.isIncludedInUniqueConstraint);
|
||||
|
||||
if (
|
||||
[FieldMetadataType.MORPH_RELATION, FieldMetadataType.RELATION].includes(
|
||||
field.type,
|
||||
) ||
|
||||
isCompositeFieldWithNonIncludedUniqueConstraint
|
||||
) {
|
||||
const fieldType = field.type;
|
||||
|
||||
throw new FieldMetadataException(
|
||||
`Unique index cannot be created for field ${field.name} of type ${fieldType}`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
{
|
||||
userFriendlyMessage: t`${fieldType} fields cannot be unique.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user