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:
+2
@@ -25,6 +25,7 @@ import { FieldMetadataServiceV2 } from 'src/engine/metadata-modules/field-metada
|
||||
import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator';
|
||||
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
|
||||
import { FlatFieldMetadataModule } from 'src/engine/metadata-modules/flat-field-metadata/flat-field-metadata.module';
|
||||
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -66,6 +67,7 @@ import { FieldMetadataService } from './services/field-metadata.service';
|
||||
WorkspaceMigrationBuilderV2Module,
|
||||
WorkspaceMigrationRunnerV2Module,
|
||||
FlatFieldMetadataModule,
|
||||
IndexMetadataModule,
|
||||
],
|
||||
services: [
|
||||
IsFieldMetadataDefaultValue,
|
||||
|
||||
+264
-1
@@ -46,12 +46,17 @@ import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-
|
||||
import { isFieldMetadataTypeMorphRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-morph-relation.util';
|
||||
import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util';
|
||||
import { isSelectOrMultiSelectFieldMetadata } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
|
||||
import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util';
|
||||
import { prepareCustomFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/utils/prepare-custom-field-metadata-for-options.util';
|
||||
import { prepareCustomFieldMetadataForCreation } from 'src/engine/metadata-modules/field-metadata/utils/prepare-field-metadata-for-creation.util';
|
||||
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { validateCanCreateUniqueIndex } from 'src/engine/metadata-modules/index-metadata/utils/validate-can-create-unique-index.util';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap } from 'src/engine/metadata-modules/utils/get-object-metadata-entity-from-object-metadata-item-with-fields-map.util';
|
||||
import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
@@ -97,6 +102,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
private readonly fieldMetadataMorphRelationService: FieldMetadataMorphRelationService,
|
||||
private readonly fieldMetadataRelationService: FieldMetadataRelationService,
|
||||
private readonly fieldMetadataServiceV2: FieldMetadataServiceV2,
|
||||
private readonly indexMetadataService: IndexMetadataService,
|
||||
) {
|
||||
super(fieldMetadataRepository);
|
||||
}
|
||||
@@ -168,6 +174,23 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: isDefined(fieldMetadataInput.defaultValue)
|
||||
? fieldMetadataInput.defaultValue
|
||||
: existingFieldMetadata.defaultValue,
|
||||
isUnique: isDefined(fieldMetadataInput.isUnique)
|
||||
? fieldMetadataInput.isUnique
|
||||
: (existingFieldMetadata.isUnique ?? false),
|
||||
type: existingFieldMetadata.type,
|
||||
})
|
||||
) {
|
||||
throw new FieldMetadataException(
|
||||
'Unique field cannot have a default value',
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const queryRunner = this.coreDataSource.createQueryRunner();
|
||||
|
||||
await queryRunner.connect();
|
||||
@@ -257,10 +280,60 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceMigrationsOnCustomUniqueIndex: WorkspaceMigrationTableAction[] =
|
||||
[];
|
||||
|
||||
const shouldUpdateUniqueIndex =
|
||||
isDefined(fieldMetadataInput.name) &&
|
||||
fieldMetadataInput.name !== existingFieldMetadata.name &&
|
||||
existingFieldMetadata.isUnique === true;
|
||||
|
||||
if (shouldUpdateUniqueIndex) {
|
||||
workspaceMigrationsOnCustomUniqueIndex.push(
|
||||
await this.updateUniqueIndexMetdataAndCreateMigrationActions({
|
||||
fieldMetadataInput,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
updatedFieldMetadata,
|
||||
queryRunner,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const shouldCreateUniqueIndex =
|
||||
fieldMetadataInput.isUnique === true && !existingFieldMetadata.isUnique;
|
||||
|
||||
if (shouldCreateUniqueIndex) {
|
||||
workspaceMigrationsOnCustomUniqueIndex.push(
|
||||
await this.createUniqueIndexMetadataAndCreateMigrationActions({
|
||||
fieldMetadataItem: updatedFieldMetadata,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
fieldMetadataInput,
|
||||
queryRunner,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const shouldDeleteUniqueIndex =
|
||||
(fieldMetadataInput.isUnique === null ||
|
||||
fieldMetadataInput.isUnique === false) &&
|
||||
existingFieldMetadata.isUnique;
|
||||
|
||||
if (shouldDeleteUniqueIndex) {
|
||||
workspaceMigrationsOnCustomUniqueIndex.push(
|
||||
await this.deleteUniqueIndexMetadataAndCreateMigrationActions({
|
||||
fieldMetadataInput,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
updatedFieldMetadata,
|
||||
queryRunner,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(fieldMetadataInput.name) ||
|
||||
isDefined(updatableFieldInput.options) ||
|
||||
isDefined(updatableFieldInput.defaultValue)
|
||||
isDefined(updatableFieldInput.defaultValue) ||
|
||||
isDefined(updatableFieldInput.isUnique)
|
||||
) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`update-${updatedFieldMetadata.name}`),
|
||||
@@ -275,6 +348,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
updatedFieldMetadata,
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
...workspaceMigrationsOnCustomUniqueIndex,
|
||||
],
|
||||
queryRunner,
|
||||
);
|
||||
@@ -670,6 +744,30 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
});
|
||||
|
||||
migrationActions.push(...fieldMigrationActions);
|
||||
|
||||
if (fieldMetadataInput.isUnique) {
|
||||
if (createdFieldMetadataItems.length > 1) {
|
||||
throw new FieldMetadataException(
|
||||
'Unique field cannot bet RELATION or MORPH_RELATION type',
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const uniqueIndexMigration =
|
||||
await this.createUniqueIndexForNewField({
|
||||
createdFieldMetadataItem: createdFieldMetadataItems[0],
|
||||
objectMetadata,
|
||||
fieldMetadataInput,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
migrationActions.push(
|
||||
...(isDefined(uniqueIndexMigration)
|
||||
? [uniqueIndexMigration]
|
||||
: []),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,6 +808,171 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
}
|
||||
}
|
||||
|
||||
private async createUniqueIndexForNewField({
|
||||
createdFieldMetadataItem,
|
||||
objectMetadata,
|
||||
fieldMetadataInput,
|
||||
workspaceId,
|
||||
queryRunner,
|
||||
}: {
|
||||
createdFieldMetadataItem: FieldMetadataEntity;
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps;
|
||||
fieldMetadataInput: CreateFieldInput;
|
||||
workspaceId: string;
|
||||
queryRunner: QueryRunner;
|
||||
}) {
|
||||
if (
|
||||
isDefined(fieldMetadataInput.defaultValue) &&
|
||||
!isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: fieldMetadataInput.defaultValue,
|
||||
isUnique: fieldMetadataInput.isUnique ?? false,
|
||||
type: fieldMetadataInput.type,
|
||||
})
|
||||
)
|
||||
throw new FieldMetadataException(
|
||||
'Unique field cannot have a default value',
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
|
||||
if (fieldMetadataInput.isUnique !== true) return;
|
||||
|
||||
validateCanCreateUniqueIndex(createdFieldMetadataItem);
|
||||
|
||||
await this.indexMetadataService.createIndexMetadata({
|
||||
workspaceId,
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadata,
|
||||
),
|
||||
fieldMetadataToIndex: [createdFieldMetadataItem],
|
||||
isUnique: true,
|
||||
isCustom: true,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(createdFieldMetadataItem),
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
return this.indexMetadataService.computeIndexCreationMigration({
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadata,
|
||||
),
|
||||
fieldMetadataToIndex: [createdFieldMetadataItem],
|
||||
isUnique: true,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(createdFieldMetadataItem),
|
||||
});
|
||||
}
|
||||
|
||||
private async createUniqueIndexMetadataAndCreateMigrationActions({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
fieldMetadataInput,
|
||||
queryRunner,
|
||||
}: {
|
||||
fieldMetadataItem: FieldMetadataEntity;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
fieldMetadataInput: UpdateFieldInput;
|
||||
queryRunner: QueryRunner;
|
||||
}) {
|
||||
validateCanCreateUniqueIndex(fieldMetadataItem);
|
||||
|
||||
await this.indexMetadataService.createIndexMetadata({
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
fieldMetadataToIndex: [fieldMetadataItem],
|
||||
isUnique: true,
|
||||
isCustom: true,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(fieldMetadataItem),
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
return this.indexMetadataService.computeIndexCreationMigration({
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
fieldMetadataToIndex: [fieldMetadataItem],
|
||||
isUnique: true,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(fieldMetadataItem),
|
||||
});
|
||||
}
|
||||
|
||||
private async updateUniqueIndexMetdataAndCreateMigrationActions({
|
||||
fieldMetadataInput,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
updatedFieldMetadata,
|
||||
queryRunner,
|
||||
}: {
|
||||
fieldMetadataInput: UpdateFieldInput;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
updatedFieldMetadata: FieldMetadataEntity;
|
||||
queryRunner: QueryRunner;
|
||||
}) {
|
||||
const recomputedIndexPayload =
|
||||
await this.indexMetadataService.recomputeUniqueCustomIndexMetadataForField(
|
||||
{
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
updatedFieldMetadata: updatedFieldMetadata,
|
||||
queryRunner,
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(recomputedIndexPayload)) {
|
||||
throw new FieldMetadataException(
|
||||
'Unique index not found for unique field',
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const { updatedIndex, previousName } = recomputedIndexPayload;
|
||||
|
||||
return this.indexMetadataService.createIndexRecomputeMigrationActions(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
{
|
||||
indexMetadata: updatedIndex,
|
||||
previousName,
|
||||
newName: updatedIndex.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async deleteUniqueIndexMetadataAndCreateMigrationActions({
|
||||
fieldMetadataInput,
|
||||
objectMetadataItemWithFieldMaps,
|
||||
updatedFieldMetadata,
|
||||
queryRunner,
|
||||
}: {
|
||||
fieldMetadataInput: UpdateFieldInput;
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps;
|
||||
updatedFieldMetadata: FieldMetadataEntity;
|
||||
queryRunner: QueryRunner;
|
||||
}) {
|
||||
await this.indexMetadataService.deleteIndexMetadata({
|
||||
workspaceId: fieldMetadataInput.workspaceId,
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
fieldMetadataToIndex: [updatedFieldMetadata],
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
return this.indexMetadataService.computeIndexDeletionMigration({
|
||||
objectMetadata:
|
||||
getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
),
|
||||
fieldMetadataToIndex: [updatedFieldMetadata],
|
||||
isUnique: fieldMetadataInput.isUnique ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
private async validateAndCreateFieldMetadataItems(
|
||||
fieldMetadataInput: CreateFieldInput,
|
||||
objectMetadata: ObjectMetadataItemWithFieldMaps,
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { isValidUniqueFieldDefaultValueCombination } from 'src/engine/metadata-modules/field-metadata/utils/is-valid-unique-input.util';
|
||||
|
||||
describe('isValidUniqueFieldDefaultValueCombination', () => {
|
||||
it('should return true if the field has a custom default value and is not unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "'custom value'",
|
||||
isUnique: false,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if the field has standard default value and is unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "''",
|
||||
isUnique: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the field has custom default value and is unique', () => {
|
||||
const result = isValidUniqueFieldDefaultValueCombination({
|
||||
defaultValue: "'custom value'",
|
||||
isUnique: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { isDeepStrictEqual } from 'util';
|
||||
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { type FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import { generateDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/generate-default-value';
|
||||
|
||||
export const isValidUniqueFieldDefaultValueCombination = ({
|
||||
defaultValue,
|
||||
isUnique,
|
||||
type,
|
||||
}: {
|
||||
defaultValue: FieldMetadataDefaultValue;
|
||||
isUnique: boolean;
|
||||
type: FieldMetadataType;
|
||||
}) => {
|
||||
const defaultDefaultValue = generateDefaultValue(type);
|
||||
|
||||
return !isUnique || isDeepStrictEqual(defaultValue, defaultDefaultValue);
|
||||
};
|
||||
+1
@@ -35,6 +35,7 @@ export const prepareCustomFieldMetadataForCreation = (
|
||||
fieldMetadataInput?.relationCreationPayload?.targetObjectMetadataId,
|
||||
defaultValue,
|
||||
...options,
|
||||
isUnique: fieldMetadataInput.isUnique ?? false,
|
||||
isActive: true,
|
||||
isCustom: true,
|
||||
settings: fieldMetadataInput.settings,
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class IndexMetadataException extends CustomException {
|
||||
declare code: IndexMetadataExceptionCode;
|
||||
constructor(
|
||||
message: string,
|
||||
code: IndexMetadataExceptionCode,
|
||||
{ userFriendlyMessage }: { userFriendlyMessage?: string } = {},
|
||||
) {
|
||||
super(message, code, { userFriendlyMessage });
|
||||
}
|
||||
}
|
||||
|
||||
export enum IndexMetadataExceptionCode {
|
||||
INDEX_CREATION_FAILED = 'INDEX_CREATION_FAILED',
|
||||
INDEX_NOT_SUPPORTED_FOR_COMPOSITE_FIELD = 'INDEX_NOT_SUPPORTED_FOR_COMPOSITE_FIELD',
|
||||
INDEX_NOT_SUPPORTED_FOR_MORH_RELATION_FIELD_AND_RELATION_FIELD = 'INDEX_NOT_SUPPORTED_FOR_MORH_RELATION_FIELD_AND_RELATION_FIELD',
|
||||
}
|
||||
+240
-49
@@ -2,12 +2,23 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { type FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type QueryRunner, Repository } from 'typeorm';
|
||||
import { In, type QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
import { type CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
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 { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
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';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { type IndexType } from 'src/engine/metadata-modules/index-metadata/types/indexType.types';
|
||||
import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
@@ -73,7 +84,7 @@ export class IndexMetadataService {
|
||||
},
|
||||
});
|
||||
|
||||
if (existingIndex) {
|
||||
if (isDefined(existingIndex)) {
|
||||
throw new Error(
|
||||
`Index ${indexName} on object metadata ${objectMetadata.nameSingular} already exists`,
|
||||
);
|
||||
@@ -91,6 +102,8 @@ export class IndexMetadataService {
|
||||
workspaceId,
|
||||
objectMetadataId: objectMetadata.id,
|
||||
...(isDefined(indexType) ? { indexType } : {}),
|
||||
...(isDefined(indexWhereClause) ? { indexWhereClause } : {}),
|
||||
...(isDefined(isUnique) ? { isUnique } : {}),
|
||||
isCustom,
|
||||
});
|
||||
} catch {
|
||||
@@ -104,6 +117,37 @@ export class IndexMetadataService {
|
||||
`Failed to return saved index ${indexName} on object metadata ${objectMetadata.nameSingular}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async createIndex({
|
||||
workspaceId,
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
isUnique,
|
||||
isCustom,
|
||||
indexType,
|
||||
indexWhereClause,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
fieldMetadataToIndex: FieldMetadataEntity[];
|
||||
isUnique: boolean;
|
||||
isCustom: boolean;
|
||||
indexType?: IndexType;
|
||||
indexWhereClause?: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
await this.createIndexMetadata({
|
||||
workspaceId,
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
indexType,
|
||||
indexWhereClause,
|
||||
isUnique,
|
||||
isCustom,
|
||||
queryRunner,
|
||||
});
|
||||
|
||||
await this.createIndexCreationMigration({
|
||||
workspaceId,
|
||||
@@ -116,6 +160,51 @@ export class IndexMetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
async recomputeUniqueCustomIndexMetadataForField({
|
||||
workspaceId,
|
||||
objectMetadata,
|
||||
updatedFieldMetadata,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
updatedFieldMetadata: FieldMetadataEntity;
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
const indexMetadataRepository = queryRunner
|
||||
? queryRunner.manager.getRepository(IndexMetadataEntity)
|
||||
: this.indexMetadataRepository;
|
||||
|
||||
const [index] = await indexMetadataRepository.find({
|
||||
where: {
|
||||
objectMetadataId: objectMetadata.id,
|
||||
workspaceId,
|
||||
indexFieldMetadatas: {
|
||||
fieldMetadataId: In([updatedFieldMetadata.id]),
|
||||
},
|
||||
isUnique: true,
|
||||
isCustom: true,
|
||||
},
|
||||
relations: ['indexFieldMetadatas.fieldMetadata'],
|
||||
});
|
||||
|
||||
if (!isDefined(index)) return;
|
||||
|
||||
const updatedIndex = await indexMetadataRepository.save({
|
||||
...index,
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
computeObjectTargetTable(objectMetadata),
|
||||
updatedFieldMetadata.name,
|
||||
])}`,
|
||||
indexWhereClause: computeUniqueIndexWhereClause(updatedFieldMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
updatedIndex,
|
||||
previousName: index.name,
|
||||
};
|
||||
}
|
||||
|
||||
async recomputeIndexMetadataForObject(
|
||||
workspaceId: string,
|
||||
updatedObjectMetadata: Pick<
|
||||
@@ -173,12 +262,17 @@ export class IndexMetadataService {
|
||||
return recomputedIndexes;
|
||||
}
|
||||
|
||||
async deleteIndexMetadata(
|
||||
workspaceId: string,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[],
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
async deleteIndexMetadata({
|
||||
workspaceId,
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[];
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
const tableName = computeObjectTargetTable(objectMetadata);
|
||||
|
||||
const columnNames: string[] = fieldMetadataToIndex.map(
|
||||
@@ -216,22 +310,14 @@ export class IndexMetadataService {
|
||||
}
|
||||
}
|
||||
|
||||
async createIndexCreationMigration({
|
||||
workspaceId,
|
||||
computeIndexDeletionMigration({
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
isUnique,
|
||||
indexType,
|
||||
indexWhereClause,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
fieldMetadataToIndex: Partial<FieldMetadataEntity>[];
|
||||
isUnique: boolean;
|
||||
indexType?: IndexType;
|
||||
indexWhereClause?: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
const tableName = computeObjectTargetTable(objectMetadata);
|
||||
|
||||
@@ -241,7 +327,69 @@ export class IndexMetadataService {
|
||||
|
||||
const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`;
|
||||
|
||||
const migration = {
|
||||
return {
|
||||
name: tableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
|
||||
indexes: [
|
||||
{
|
||||
action: WorkspaceMigrationIndexActionType.DROP,
|
||||
name: indexName,
|
||||
columns: [],
|
||||
isUnique,
|
||||
} satisfies WorkspaceMigrationIndexAction,
|
||||
],
|
||||
} satisfies WorkspaceMigrationTableAction;
|
||||
}
|
||||
|
||||
computeIndexCreationMigration({
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
isUnique,
|
||||
indexType,
|
||||
indexWhereClause,
|
||||
}: {
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
fieldMetadataToIndex: (Partial<FieldMetadataEntity> & {
|
||||
type: FieldMetadataType;
|
||||
name: string;
|
||||
})[];
|
||||
isUnique: boolean;
|
||||
indexType?: IndexType;
|
||||
indexWhereClause?: string;
|
||||
}) {
|
||||
const tableName = computeObjectTargetTable(objectMetadata);
|
||||
|
||||
const fieldNames: string[] = fieldMetadataToIndex.map(
|
||||
(fieldMetadata) => fieldMetadata.name as string,
|
||||
);
|
||||
|
||||
const indexName = `IDX_${generateDeterministicIndexName([tableName, ...fieldNames])}`;
|
||||
|
||||
const columnNames = fieldMetadataToIndex.flatMap((field) => {
|
||||
if (isCompositeFieldMetadataType(field.type)) {
|
||||
if (!isUnique)
|
||||
throw new IndexMetadataException(
|
||||
`Non unique index cannot be created for composite field ${field.name}`,
|
||||
IndexMetadataExceptionCode.INDEX_NOT_SUPPORTED_FOR_COMPOSITE_FIELD,
|
||||
);
|
||||
|
||||
const compositeType = compositeTypeDefinitions.get(
|
||||
field.type,
|
||||
) as CompositeType;
|
||||
|
||||
const uniqueCompositeProperties = compositeType.properties.filter(
|
||||
(property) => property.isIncludedInUniqueConstraint,
|
||||
);
|
||||
|
||||
return uniqueCompositeProperties.map((subField) =>
|
||||
computeCompositeColumnName(field.name, subField),
|
||||
);
|
||||
}
|
||||
|
||||
return [field.name];
|
||||
});
|
||||
|
||||
return {
|
||||
name: tableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
|
||||
indexes: [
|
||||
@@ -255,6 +403,32 @@ export class IndexMetadataService {
|
||||
},
|
||||
],
|
||||
} satisfies WorkspaceMigrationTableAction;
|
||||
}
|
||||
|
||||
async createIndexCreationMigration({
|
||||
workspaceId,
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
isUnique,
|
||||
indexType,
|
||||
indexWhereClause,
|
||||
queryRunner,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
objectMetadata: ObjectMetadataEntity;
|
||||
fieldMetadataToIndex: FieldMetadataEntity[];
|
||||
isUnique: boolean;
|
||||
indexType?: IndexType;
|
||||
indexWhereClause?: string;
|
||||
queryRunner?: QueryRunner;
|
||||
}) {
|
||||
const migration = this.computeIndexCreationMigration({
|
||||
objectMetadata,
|
||||
fieldMetadataToIndex,
|
||||
isUnique,
|
||||
indexType,
|
||||
indexWhereClause,
|
||||
});
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`create-${objectMetadata.nameSingular}-index`),
|
||||
@@ -264,6 +438,51 @@ export class IndexMetadataService {
|
||||
);
|
||||
}
|
||||
|
||||
createIndexRecomputeMigrationActions(
|
||||
objectMetadata: Pick<
|
||||
ObjectMetadataEntity,
|
||||
'nameSingular' | 'isCustom' | 'id'
|
||||
>,
|
||||
recomputedIndex: {
|
||||
indexMetadata: IndexMetadataEntity;
|
||||
previousName: string;
|
||||
newName: string;
|
||||
},
|
||||
) {
|
||||
const { previousName, newName, indexMetadata } = recomputedIndex;
|
||||
|
||||
const tableName = computeObjectTargetTable(objectMetadata);
|
||||
|
||||
const indexFieldsMetadataOrdered = indexMetadata.indexFieldMetadatas.sort(
|
||||
(a, b) => a.order - b.order,
|
||||
);
|
||||
|
||||
const columnNames = indexFieldsMetadataOrdered.map(
|
||||
(indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name,
|
||||
);
|
||||
|
||||
return {
|
||||
name: tableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
|
||||
indexes: [
|
||||
{
|
||||
action: WorkspaceMigrationIndexActionType.DROP,
|
||||
name: previousName,
|
||||
columns: [],
|
||||
isUnique: indexMetadata.isUnique,
|
||||
} satisfies WorkspaceMigrationIndexAction,
|
||||
{
|
||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||
columns: columnNames,
|
||||
name: newName,
|
||||
isUnique: indexMetadata.isUnique,
|
||||
where: indexMetadata.indexWhereClause,
|
||||
type: indexMetadata.indexType,
|
||||
} satisfies WorkspaceMigrationIndexAction,
|
||||
],
|
||||
} satisfies WorkspaceMigrationTableAction;
|
||||
}
|
||||
|
||||
async createIndexRecomputeMigrations(
|
||||
workspaceId: string,
|
||||
objectMetadata: Pick<
|
||||
@@ -278,39 +497,11 @@ export class IndexMetadataService {
|
||||
queryRunner?: QueryRunner,
|
||||
) {
|
||||
for (const recomputedIndex of recomputedIndexes) {
|
||||
const { previousName, newName, indexMetadata } = recomputedIndex;
|
||||
|
||||
const tableName = computeObjectTargetTable(objectMetadata);
|
||||
|
||||
const indexFieldsMetadataOrdered = indexMetadata.indexFieldMetadatas.sort(
|
||||
(a, b) => a.order - b.order,
|
||||
const migration = this.createIndexRecomputeMigrationActions(
|
||||
objectMetadata,
|
||||
recomputedIndex,
|
||||
);
|
||||
|
||||
const columnNames = indexFieldsMetadataOrdered.map(
|
||||
(indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name,
|
||||
);
|
||||
|
||||
const migration = {
|
||||
name: tableName,
|
||||
action: WorkspaceMigrationTableActionType.ALTER_INDEXES,
|
||||
indexes: [
|
||||
{
|
||||
action: WorkspaceMigrationIndexActionType.DROP,
|
||||
name: previousName,
|
||||
columns: [],
|
||||
isUnique: indexMetadata.isUnique,
|
||||
} satisfies WorkspaceMigrationIndexAction,
|
||||
{
|
||||
action: WorkspaceMigrationIndexActionType.CREATE,
|
||||
columns: columnNames,
|
||||
name: newName,
|
||||
isUnique: indexMetadata.isUnique,
|
||||
where: indexMetadata.indexWhereClause,
|
||||
type: indexMetadata.indexType,
|
||||
} satisfies WorkspaceMigrationIndexAction,
|
||||
],
|
||||
} satisfies WorkspaceMigrationTableAction;
|
||||
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(`update-${objectMetadata.nameSingular}-index`),
|
||||
workspaceId,
|
||||
|
||||
+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.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -120,7 +120,7 @@ export class SearchVectorService {
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
await this.indexMetadataService.createIndexMetadata({
|
||||
await this.indexMetadataService.createIndex({
|
||||
workspaceId: objectMetadataInput.workspaceId,
|
||||
objectMetadata: createdObjectMetadata,
|
||||
fieldMetadataToIndex: [searchVectorFieldMetadata],
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import omit from 'lodash.omit';
|
||||
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export const getObjectMetadataEntityFromObjectMetadataItemWithFieldsMap = (
|
||||
objectMetadataItem: ObjectMetadataItemWithFieldMaps,
|
||||
): ObjectMetadataEntity => {
|
||||
return {
|
||||
...omit(objectMetadataItem, [
|
||||
'fieldsById',
|
||||
'fieldIdByName',
|
||||
'fieldIdByJoinColumnName',
|
||||
]),
|
||||
fields: Object.values(objectMetadataItem.fieldsById),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user