diff --git a/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts b/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts index ac5390ee0..4c4d05fd6 100644 --- a/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts +++ b/packages/twenty-server/src/filters/utils/global-exception-handler.util.ts @@ -6,6 +6,7 @@ import { ForbiddenError, ValidationError, NotFoundError, + ConflictError, } from 'src/filters/utils/graphql-errors.util'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; @@ -14,6 +15,7 @@ const graphQLPredefinedExceptions = { 401: AuthenticationError, 403: ForbiddenError, 404: NotFoundError, + 409: ConflictError, }; export const handleExceptionAndConvertToGraphQLError = ( diff --git a/packages/twenty-server/src/filters/utils/graphql-errors.util.ts b/packages/twenty-server/src/filters/utils/graphql-errors.util.ts index d9329d435..069a03ee3 100644 --- a/packages/twenty-server/src/filters/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/filters/utils/graphql-errors.util.ts @@ -141,3 +141,11 @@ export class NotFoundError extends BaseGraphQLError { Object.defineProperty(this, 'name', { value: 'NotFoundError' }); } } + +export class ConflictError extends BaseGraphQLError { + constructor(message: string, extensions?: Record) { + super(message, 'CONFLICT', extensions); + + Object.defineProperty(this, 'name', { value: 'ConflictError' }); + } +} diff --git a/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.service.ts index 058cff07a..d1b28843d 100644 --- a/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/metadata/relation-metadata/relation-metadata.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ConflictException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -38,149 +39,42 @@ export class RelationMetadataService extends TypeOrmQueryService { - // TODO: This logic is duplicated with the BeforeDeleteOneRelation hook - const relationMetadata = await this.relationMetadataRepository.findOne({ - where: { id }, - relations: ['fromFieldMetadata', 'toFieldMetadata'], - }); - - if (!relationMetadata) { - throw new NotFoundException('Relation does not exist'); - } - - const deletedRelationMetadata = super.deleteOne(id); - - // TODO: Move to a cdc scheduler - this.fieldMetadataService.deleteMany({ - id: { - in: [ - relationMetadata.fromFieldMetadataId, - relationMetadata.toFieldMetadataId, - ], - }, - }); - - return deletedRelationMetadata; - } - override async createOne( relationMetadataInput: CreateRelationInput, ): Promise { - if ( - relationMetadataInput.relationType === RelationMetadataType.MANY_TO_MANY - ) { - throw new BadRequestException( - 'Many to many relations are not supported yet', - ); - } - - /** - * Relation types - * - * MANY TO MANY: - * FROM Ǝ-E TO (NOT YET SUPPORTED) - * - * ONE TO MANY: - * FROM --E TO (host the id in the TO table) - * - * ONE TO ONE: - * FROM --- TO (host the id in the TO table) - */ - - const objectMetadataEntries = - await this.objectMetadataService.findManyWithinWorkspace( - relationMetadataInput.workspaceId, - { - where: { - id: In([ - relationMetadataInput.fromObjectMetadataId, - relationMetadataInput.toObjectMetadataId, - ]), - }, - }, - ); - - const objectMetadataMap = objectMetadataEntries.reduce( - (acc, curr) => { - acc[curr.id] = curr; - - return acc; - }, - {} as { [key: string]: ObjectMetadataEntity }, + const objectMetadataMap = await this.getObjectMetadataMap( + relationMetadataInput, ); - if ( - objectMetadataMap[relationMetadataInput.fromObjectMetadataId] === - undefined || - objectMetadataMap[relationMetadataInput.toObjectMetadataId] === undefined - ) { - throw new NotFoundException( - 'Can\t find an existing object matching fromObjectMetadataId or toObjectMetadataId', - ); - } + await this.validateCreateRelationMetadataInput( + relationMetadataInput, + objectMetadataMap, + ); + // NOTE: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations) + const isCustom = true; const baseColumnName = `${camelCase(relationMetadataInput.toName)}Id`; - // TODO: this logic is called to create relation through metadata graphql endpoint (so only for custom field relations) - const isCustom = true; const foreignKeyColumnName = isCustom ? createCustomColumnName(baseColumnName) : baseColumnName; const createdFields = await this.fieldMetadataService.createMany([ - // FROM - { - name: relationMetadataInput.fromName, - label: relationMetadataInput.fromLabel, - description: relationMetadataInput.fromDescription, - icon: relationMetadataInput.fromIcon, - isCustom: true, - targetColumnMap: {}, - isActive: true, - isNullable: true, - type: FieldMetadataType.RELATION, - objectMetadataId: relationMetadataInput.fromObjectMetadataId, - workspaceId: relationMetadataInput.workspaceId, - }, - // TO - { - name: relationMetadataInput.toName, - label: relationMetadataInput.toLabel, - description: relationMetadataInput.toDescription, - icon: relationMetadataInput.toIcon, - isCustom: true, - targetColumnMap: { - value: isCustom - ? createCustomColumnName(relationMetadataInput.toName) - : relationMetadataInput.toName, - }, - isActive: true, - isNullable: true, - type: FieldMetadataType.RELATION, - objectMetadataId: relationMetadataInput.toObjectMetadataId, - workspaceId: relationMetadataInput.workspaceId, - }, - // FOREIGN KEY - { - name: baseColumnName, - label: `${relationMetadataInput.toLabel} Foreign Key`, - description: relationMetadataInput.toDescription - ? `${relationMetadataInput.toDescription} Foreign Key` - : undefined, - icon: undefined, - isCustom: true, - targetColumnMap: { - value: foreignKeyColumnName, - }, - isActive: true, - isNullable: true, - // Should not be visible on the front side - isSystem: true, - type: FieldMetadataType.UUID, - objectMetadataId: relationMetadataInput.toObjectMetadataId, - workspaceId: relationMetadataInput.workspaceId, - }, + this.createFieldMetadataForRelationMetadata( + relationMetadataInput, + 'from', + isCustom, + ), + this.createFieldMetadataForRelationMetadata( + relationMetadataInput, + 'to', + isCustom, + ), + this.createForeignKeyFieldMetadata( + relationMetadataInput, + baseColumnName, + foreignKeyColumnName, + ), ]); const createdFieldMap = createdFields.reduce((acc, fieldMetadata) => { @@ -197,6 +91,86 @@ export class RelationMetadataService extends TypeOrmQueryService { + const objectMetadataEntries = + await this.objectMetadataService.findManyWithinWorkspace( + relationMetadataInput.workspaceId, + { + where: { + id: In([ + relationMetadataInput.fromObjectMetadataId, + relationMetadataInput.toObjectMetadataId, + ]), + }, + }, + ); + + return objectMetadataEntries.reduce( + (acc, curr) => { + acc[curr.id] = curr; + + return acc; + }, + {} as { [key: string]: ObjectMetadataEntity }, ); - - return createdRelationMetadata; } public async findOneWithinWorkspace( @@ -258,4 +301,30 @@ export class RelationMetadataService extends TypeOrmQueryService { + // TODO: This logic is duplicated with the BeforeDeleteOneRelation hook + const relationMetadata = await this.relationMetadataRepository.findOne({ + where: { id }, + relations: ['fromFieldMetadata', 'toFieldMetadata'], + }); + + if (!relationMetadata) { + throw new NotFoundException('Relation does not exist'); + } + + const deletedRelationMetadata = super.deleteOne(id); + + // TODO: Move to a cdc scheduler + this.fieldMetadataService.deleteMany({ + id: { + in: [ + relationMetadata.fromFieldMetadataId, + relationMetadata.toFieldMetadataId, + ], + }, + }); + + return deletedRelationMetadata; + } } diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts index 2314da3f1..0789ef77b 100644 --- a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts @@ -76,7 +76,11 @@ export class WorkspaceQueryRunnerService { const result = await this.execute(query, workspaceId); const end = performance.now(); - console.log(`query time: ${end - start} ms`); + console.log( + `query time: ${end - start} ms on query ${ + options.objectMetadataItem.nameSingular + }`, + ); return this.parseResult>( result,