Add unique indexes and indexes for composite types (#7162)
Add support for indexes on composite fields and unicity constraint on indexes This pull request includes several changes across multiple files to improve error handling, enforce unique constraints, and update database migrations. The most important changes include updating error messages for snack bars, adding a new command to enforce unique constraints, and updating database migrations to include new fields and constraints. ### Error Handling Improvements: * [`packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx`](diffhunk://#diff-e7dc05ced8e4730430f5c7fcd0c75b3aa723da438c26e0bef8130b614427dd9aL23-R23): Updated error messages in `enqueueSnackBar` to use `error.message` directly. * [`packages/twenty-front/src/modules/object-metadata/hooks/useFindManyObjectMetadataItems.ts`](diffhunk://#diff-74c126d6bc7a5ed6b63be994d298df6669058034bfbc367b11045f9f31a3abe6L44-R46): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts`](diffhunk://#diff-af23a1d99639a66c251f87473e63e2b7bceaa4ee4f70fedfa0fcffe5c7d79181L56-R58): Simplified error messages in `enqueueSnackBar`. * [`packages/twenty-front/src/modules/object-record/hooks/useHandleFindManyRecordsError.ts`](diffhunk://#diff-da04296cbe280202a1eaf6b1244a30490d4f400411bee139651172c59719088eL22-R24): Simplified error messages in `enqueueSnackBar`. ### New Command for Unique Constraints: * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-enforce-unique-constraints.command.ts`](diffhunk://#diff-8337096c8c80dd2619a5ba691ae5145101f8ae0368a75192a050047e8c6ab7cbR1-R159): Added a new command to enforce unique constraints on company domain names and person emails. * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts`](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14): Integrated the new `EnforceUniqueConstraintsCommand` into the upgrade process. [[1]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR13-R14) [[2]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR31) [[3]](diffhunk://#diff-20215e9981a53c7566e9cbff96715685125878f5bcb84fe461a7440f2e68f6fcR64-R68) * [`packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts`](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7): Registered the new `EnforceUniqueConstraintsCommand` in the module. [[1]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R7) [[2]](diffhunk://#diff-da52814efc674c25ed55645f8ee2561013641a407f88423e705dd6c77b405527R24) ### Database Migrations: * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368824-migrationDebt.ts`](diffhunk://#diff-c450aeae7bc0ef4416a0ade2dc613ca3f688629f35d2a32f90a09c3f494febdcR1-R53): Added a migration to update the `relationMetadata_ondeleteaction_enum` and set default values. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726757368825-addIsUniqueToIndexMetadata.ts`](diffhunk://#diff-8f1e14bd7f6835ec2c3bb39bcc51e3c318a3008d576a981e682f4c985e746fbfR1-R19): Added a migration to include the `isUnique` field in `indexMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726762935841-addCompostiveColumnToIndexFieldMetadata.ts`](diffhunk://#diff-7c96b7276c7722d41ff31de23b2de4d6e09adfdc74815356ba63bc96a2669440R1-R19): Added a migration to include the `compositeColumn` field in `indexFieldMetadata`. * [`packages/twenty-server/src/database/typeorm/metadata/migrations/1726766871572-addWhereToIndexMetadata.ts`](diffhunk://#diff-26651295a975eb50e672dce0e4e274e861f66feb1b68105eee5a04df32796190R1-R14): Added a migration to include the `indexWhereClause` field in `indexMetadata`. ### GraphQL Exception Handling: * [`packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.ts`](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4): Enhanced exception handling for `QueryFailedError` to provide more specific error messages for unique constraint violations. [[1]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R1-R4) [[2]](diffhunk://#diff-58445eb362dc89e31107777d39b592d7842d2ab09a223012ccd055da325270a8R23-R59) * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts`](diffhunk://#diff-233d58ab2333586dd45e46e33d4f07e04a4b8adde4a11a48e25d86985e5a7943L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. * [`packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts`](diffhunk://#diff-68b803f0762c407f5d2d1f5f8d389655a60654a2dd2394a81318655dcd44dc43L58-R58): Updated the `workspaceQueryRunnerGraphqlApiExceptionHandler` call to include context. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -12,8 +12,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||
@ -157,6 +157,6 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
})
|
||||
@WorkspaceIsNullable()
|
||||
@WorkspaceIsSystem()
|
||||
@WorkspaceIndex({ indexType: IndexType.GIN })
|
||||
@WorkspaceFieldIndex({ indexType: IndexType.GIN })
|
||||
[SEARCH_VECTOR_FIELD.name]: any;
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { WorkspaceIndexOptions } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceFieldIndex(
|
||||
options?: WorkspaceIndexOptions,
|
||||
): PropertyDecorator {
|
||||
return (target: any, propertyKey: string | symbol) => {
|
||||
if (propertyKey === undefined) {
|
||||
throw new Error('This decorator should be used with a field not a class');
|
||||
}
|
||||
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
const additionalDefaultColumnsForIndex = getColumnsForIndex(
|
||||
options?.indexType,
|
||||
);
|
||||
|
||||
const columns = [
|
||||
propertyKey.toString(),
|
||||
...additionalDefaultColumnsForIndex,
|
||||
];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target.constructor,
|
||||
gate,
|
||||
isUnique: options?.isUnique ?? false,
|
||||
whereClause: options?.indexWhereClause ?? null,
|
||||
type: options?.indexType,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -57,6 +57,12 @@ export function WorkspaceField<T extends FieldMetadataType>(
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
) ?? false;
|
||||
const isUnique =
|
||||
TypedReflect.getMetadata(
|
||||
'workspace:is-unique-metadata-args',
|
||||
object,
|
||||
propertyKey.toString(),
|
||||
) ?? false;
|
||||
|
||||
const defaultValue = (options.defaultValue ??
|
||||
generateDefaultValue(options.type)) as FieldMetadataDefaultValue | null;
|
||||
@ -77,6 +83,7 @@ export function WorkspaceField<T extends FieldMetadataType>(
|
||||
isSystem,
|
||||
gate,
|
||||
isDeprecated,
|
||||
isUnique,
|
||||
isActive: options.isActive,
|
||||
asExpression: options.asExpression,
|
||||
generatedType: options.generatedType,
|
||||
|
||||
@ -1,83 +1,40 @@
|
||||
import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export type WorkspaceIndexMetadata = {
|
||||
columns?: string[];
|
||||
export type WorkspaceIndexOptions = {
|
||||
isUnique?: boolean;
|
||||
indexWhereClause?: string;
|
||||
indexType?: IndexType;
|
||||
};
|
||||
|
||||
export function WorkspaceIndex(
|
||||
metadata?: WorkspaceIndexMetadata,
|
||||
): PropertyDecorator;
|
||||
export function WorkspaceIndex(
|
||||
metadata: WorkspaceIndexMetadata,
|
||||
): ClassDecorator;
|
||||
export function WorkspaceIndex(
|
||||
metadata?: WorkspaceIndexMetadata,
|
||||
): PropertyDecorator | ClassDecorator {
|
||||
return (target: any, propertyKey: string | symbol) => {
|
||||
if (propertyKey === undefined && metadata === undefined) {
|
||||
throw new Error('Class level WorkspaceIndex should be used with columns');
|
||||
}
|
||||
|
||||
if (propertyKey !== undefined && metadata?.columns !== undefined) {
|
||||
throw new Error(
|
||||
'Property level WorkspaceIndex should not be used with columns',
|
||||
);
|
||||
}
|
||||
columns: string[],
|
||||
options: WorkspaceIndexOptions,
|
||||
): ClassDecorator {
|
||||
if (!Array.isArray(columns) || columns.length === 0) {
|
||||
throw new Error('Class level WorkspaceIndex should be used with columns');
|
||||
}
|
||||
|
||||
return (target: any) => {
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
// TODO: handle composite field metadata types
|
||||
if (isDefined(metadata?.columns)) {
|
||||
const columns = metadata.columns;
|
||||
|
||||
if (columns.length > 0) {
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target,
|
||||
gate,
|
||||
...(isDefined(metadata?.indexType)
|
||||
? { type: metadata.indexType }
|
||||
: {}),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isDefined(propertyKey)) {
|
||||
const additionalDefaultColumnsForIndex = getColumnsForIndex(
|
||||
metadata?.indexType,
|
||||
);
|
||||
const columns = [
|
||||
propertyKey.toString(),
|
||||
...additionalDefaultColumnsForIndex,
|
||||
];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target.constructor,
|
||||
...(isDefined(metadata?.indexType) ? { type: metadata.indexType } : {}),
|
||||
gate,
|
||||
});
|
||||
}
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target,
|
||||
gate,
|
||||
isUnique: options?.isUnique ?? false,
|
||||
whereClause: options?.indexWhereClause ?? null,
|
||||
type: options?.indexType,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||
|
||||
export function WorkspaceIsUnique(): PropertyDecorator {
|
||||
return (target: any, propertyKey: string | symbol) => {
|
||||
if (propertyKey === undefined) {
|
||||
throw new Error('This decorator should be used with a field not a class');
|
||||
}
|
||||
|
||||
const gate = TypedReflect.getMetadata(
|
||||
'workspace:gate-metadata-args',
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
|
||||
const columns = [propertyKey.toString()];
|
||||
|
||||
metadataArgsStorage.addIndexes({
|
||||
name: `IDX_UNIQUE_${generateDeterministicIndexName([
|
||||
convertClassNameToObjectMetadataName(target.constructor.name),
|
||||
...columns,
|
||||
])}`,
|
||||
columns,
|
||||
target: target.constructor,
|
||||
gate,
|
||||
isUnique: true,
|
||||
whereClause: null,
|
||||
});
|
||||
|
||||
return TypedReflect.defineMetadata(
|
||||
'workspace:is-unique-metadata-args',
|
||||
true,
|
||||
target,
|
||||
propertyKey.toString(),
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||
|
||||
export function WorkspaceJoinColumn(
|
||||
@ -12,6 +12,6 @@ export function WorkspaceJoinColumn(
|
||||
});
|
||||
|
||||
// Register index for join column
|
||||
WorkspaceIndex()(object, propertyKey);
|
||||
WorkspaceFieldIndex()(object, propertyKey);
|
||||
};
|
||||
}
|
||||
|
||||
@ -75,6 +75,11 @@ export interface WorkspaceFieldMetadataArgs {
|
||||
*/
|
||||
readonly isNullable: boolean;
|
||||
|
||||
/**
|
||||
* Is unique field.
|
||||
*/
|
||||
readonly isUnique: boolean;
|
||||
|
||||
/**
|
||||
* Field gate.
|
||||
*/
|
||||
|
||||
@ -19,11 +19,21 @@ export interface WorkspaceIndexMetadataArgs {
|
||||
*/
|
||||
columns: string[];
|
||||
|
||||
/**
|
||||
* Is index unique.
|
||||
*/
|
||||
isUnique: boolean;
|
||||
|
||||
/*
|
||||
* Index type. Defaults to Btree.
|
||||
*/
|
||||
type?: IndexType;
|
||||
|
||||
/**
|
||||
* Index where clause.
|
||||
*/
|
||||
whereClause: string | null;
|
||||
|
||||
/**
|
||||
* Field gate.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user