mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-30 12:22:29 +00:00 
			
		
		
		
	Refactor graphql query runner and add mutation resolvers (#7418)
Fixes https://github.com/twentyhq/twenty/issues/6859 This PR adds all the remaining resolvers for - updateOne/updateMany - createOne/createMany - deleteOne/deleteMany - destroyOne - restoreMany Also - refactored the graphql-query-runner to be able to add other resolvers without too much boilerplate. - add missing events that were not sent anymore as well as webhooks - make resolver injectable so they can inject other services as well - use objectMetadataMap from cache instead of computing it multiple time - various fixes (mutation not correctly parsing JSON, relationHelper fetching data with empty ids set, ...) Next steps: - Wrapping query builder to handle DB events properly - Move webhook emitters to db event listener - Add pagination where it's missing (findDuplicates, nested relations, etc...)
This commit is contained in:
		| @@ -5,12 +5,12 @@ export class AddIndexType1725893697807 implements MigrationInterface { | ||||
|  | ||||
|   public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|     await queryRunner.query( | ||||
|       `CREATE TYPE metadata."indextype_enum" AS ENUM ('BTREE', 'GIN')`, | ||||
|       `CREATE TYPE "metadata"."indexMetadata_indextype_enum" AS ENUM('BTREE', 'GIN')`, | ||||
|     ); | ||||
|  | ||||
|     await queryRunner.query(` | ||||
|       ALTER TABLE metadata."indexMetadata"  | ||||
|       ADD COLUMN "indexType" metadata."indextype_enum" NOT NULL DEFAULT 'BTREE'; | ||||
|       ADD COLUMN "indexType" metadata."indexMetadata_indextype_enum" NOT NULL DEFAULT 'BTREE'; | ||||
|     `); | ||||
|   } | ||||
|  | ||||
| @@ -19,6 +19,8 @@ export class AddIndexType1725893697807 implements MigrationInterface { | ||||
|       ALTER TABLE metadata."indexMetadata" DROP COLUMN "indexType" | ||||
|     `); | ||||
|  | ||||
|     await queryRunner.query(`DROP TYPE metadata."indextype_enum"`); | ||||
|     await queryRunner.query( | ||||
|       `DROP TYPE metadata."indexMetadata_indextype_enum"`, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,51 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { ModuleRef } from '@nestjs/core'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { | ||||
|   ResolverArgs, | ||||
|   WorkspaceResolverBuilderMethodNames, | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; | ||||
| import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; | ||||
| import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service'; | ||||
| import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; | ||||
| import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; | ||||
| import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service'; | ||||
| import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service'; | ||||
| import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class GraphqlQueryResolverFactory { | ||||
|   constructor(private moduleRef: ModuleRef) {} | ||||
|  | ||||
|   public getResolver( | ||||
|     operationName: WorkspaceResolverBuilderMethodNames, | ||||
|   ): ResolverService<ResolverArgs, any> { | ||||
|     switch (operationName) { | ||||
|       case 'findOne': | ||||
|         return this.moduleRef.get(GraphqlQueryFindOneResolverService); | ||||
|       case 'findMany': | ||||
|         return this.moduleRef.get(GraphqlQueryFindManyResolverService); | ||||
|       case 'findDuplicates': | ||||
|         return this.moduleRef.get(GraphqlQueryFindDuplicatesResolverService); | ||||
|       case 'search': | ||||
|         return this.moduleRef.get(GraphqlQuerySearchResolverService); | ||||
|       case 'createOne': | ||||
|       case 'createMany': | ||||
|         return this.moduleRef.get(GraphqlQueryCreateManyResolverService); | ||||
|       case 'destroyOne': | ||||
|         return this.moduleRef.get(GraphqlQueryDestroyOneResolverService); | ||||
|       case 'updateOne': | ||||
|       case 'deleteOne': | ||||
|         return this.moduleRef.get(GraphqlQueryUpdateOneResolverService); | ||||
|       case 'updateMany': | ||||
|       case 'deleteMany': | ||||
|       case 'restoreMany': | ||||
|         return this.moduleRef.get(GraphqlQueryUpdateManyResolverService); | ||||
|       default: | ||||
|         throw new Error(`Unsupported operation: ${operationName}`); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -25,7 +25,7 @@ export class GraphqlQueryFilterConditionParser { | ||||
|   public parse( | ||||
|     queryBuilder: SelectQueryBuilder<any>, | ||||
|     objectNameSingular: string, | ||||
|     filter: RecordFilter, | ||||
|     filter: Partial<RecordFilter>, | ||||
|   ): SelectQueryBuilder<any> { | ||||
|     if (!filter || Object.keys(filter).length === 0) { | ||||
|       return queryBuilder; | ||||
|   | ||||
| @@ -58,7 +58,6 @@ export class GraphqlQueryFilterFieldParser { | ||||
|     } | ||||
|  | ||||
|     const { sql, params } = this.computeWhereConditionParts( | ||||
|       fieldMetadata, | ||||
|       operator, | ||||
|       objectNameSingular, | ||||
|       key, | ||||
| @@ -73,7 +72,6 @@ export class GraphqlQueryFilterFieldParser { | ||||
|   } | ||||
|  | ||||
|   private computeWhereConditionParts( | ||||
|     fieldMetadata: FieldMetadataInterface, | ||||
|     operator: string, | ||||
|     objectNameSingular: string, | ||||
|     key: string, | ||||
| @@ -185,7 +183,6 @@ export class GraphqlQueryFilterFieldParser { | ||||
|       ); | ||||
|  | ||||
|       const { sql, params } = this.computeWhereConditionParts( | ||||
|         fieldMetadata, | ||||
|         operator, | ||||
|         objectNameSingular, | ||||
|         fullFieldName, | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import { | ||||
|   RecordFilter, | ||||
|   RecordOrderBy, | ||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
|  | ||||
| import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; | ||||
| import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; | ||||
| @@ -17,6 +16,7 @@ import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql | ||||
| import { | ||||
|   FieldMetadataMap, | ||||
|   ObjectMetadataMap, | ||||
|   ObjectMetadataMapItem, | ||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
|  | ||||
| export class GraphqlQueryParser { | ||||
| @@ -39,10 +39,10 @@ export class GraphqlQueryParser { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   applyFilterToBuilder( | ||||
|   public applyFilterToBuilder( | ||||
|     queryBuilder: SelectQueryBuilder<any>, | ||||
|     objectNameSingular: string, | ||||
|     recordFilter: RecordFilter, | ||||
|     recordFilter: Partial<RecordFilter>, | ||||
|   ): SelectQueryBuilder<any> { | ||||
|     return this.filterConditionParser.parse( | ||||
|       queryBuilder, | ||||
| @@ -51,7 +51,7 @@ export class GraphqlQueryParser { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   applyDeletedAtToBuilder( | ||||
|   public applyDeletedAtToBuilder( | ||||
|     queryBuilder: SelectQueryBuilder<any>, | ||||
|     recordFilter: RecordFilter, | ||||
|   ): SelectQueryBuilder<any> { | ||||
| @@ -88,7 +88,7 @@ export class GraphqlQueryParser { | ||||
|     return false; | ||||
|   }; | ||||
|  | ||||
|   applyOrderToBuilder( | ||||
|   public applyOrderToBuilder( | ||||
|     queryBuilder: SelectQueryBuilder<any>, | ||||
|     orderBy: RecordOrderBy, | ||||
|     objectNameSingular: string, | ||||
| @@ -103,8 +103,8 @@ export class GraphqlQueryParser { | ||||
|     return queryBuilder.orderBy(parsedOrderBys as OrderByCondition); | ||||
|   } | ||||
|  | ||||
|   parseSelectedFields( | ||||
|     parentObjectMetadata: ObjectMetadataInterface, | ||||
|   public parseSelectedFields( | ||||
|     parentObjectMetadata: ObjectMetadataMapItem, | ||||
|     graphqlSelectedFields: Partial<Record<string, any>>, | ||||
|   ): { select: Record<string, any>; relations: Record<string, any> } { | ||||
|     const parentFields = | ||||
|   | ||||
| @@ -1,16 +1,43 @@ | ||||
| import { Module } from '@nestjs/common'; | ||||
|  | ||||
| import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory'; | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; | ||||
| import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; | ||||
| import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service'; | ||||
| import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; | ||||
| import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; | ||||
| import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service'; | ||||
| import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service'; | ||||
| import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service'; | ||||
| import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; | ||||
| import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; | ||||
| import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module'; | ||||
| import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; | ||||
|  | ||||
| const graphqlQueryResolvers = [ | ||||
|   GraphqlQueryFindOneResolverService, | ||||
|   GraphqlQueryFindManyResolverService, | ||||
|   GraphqlQueryFindDuplicatesResolverService, | ||||
|   GraphqlQueryCreateManyResolverService, | ||||
|   GraphqlQueryDestroyOneResolverService, | ||||
|   GraphqlQueryUpdateOneResolverService, | ||||
|   GraphqlQueryUpdateManyResolverService, | ||||
|   GraphqlQuerySearchResolverService, | ||||
| ]; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [ | ||||
|     WorkspaceQueryHookModule, | ||||
|     WorkspaceQueryRunnerModule, | ||||
|     FeatureFlagModule, | ||||
|   ], | ||||
|   providers: [GraphqlQueryRunnerService], | ||||
|   providers: [ | ||||
|     GraphqlQueryRunnerService, | ||||
|     GraphqlQueryResolverFactory, | ||||
|     ApiEventEmitterService, | ||||
|     ...graphqlQueryResolvers, | ||||
|   ], | ||||
|   exports: [GraphqlQueryRunnerService], | ||||
| }) | ||||
| export class GraphqlQueryRunnerModule {} | ||||
|   | ||||
| @@ -6,180 +6,90 @@ import { | ||||
|   RecordOrderBy, | ||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; | ||||
| import { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { | ||||
|   CreateManyResolverArgs, | ||||
|   CreateOneResolverArgs, | ||||
|   DeleteManyResolverArgs, | ||||
|   DeleteOneResolverArgs, | ||||
|   DestroyOneResolverArgs, | ||||
|   FindDuplicatesResolverArgs, | ||||
|   FindManyResolverArgs, | ||||
|   FindOneResolverArgs, | ||||
|   ResolverArgs, | ||||
|   ResolverArgsType, | ||||
|   RestoreManyResolverArgs, | ||||
|   SearchResolverArgs, | ||||
|   UpdateManyResolverArgs, | ||||
|   UpdateOneResolverArgs, | ||||
|   WorkspaceResolverBuilderMethodNames, | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
|  | ||||
| import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; | ||||
| import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; | ||||
| import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; | ||||
| import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; | ||||
| import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service'; | ||||
| import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory'; | ||||
| import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; | ||||
| import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; | ||||
| import { | ||||
|   CallWebhookJobsJob, | ||||
|   CallWebhookJobsJobData, | ||||
|   CallWebhookJobsJobOperation, | ||||
| } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job'; | ||||
| import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; | ||||
| import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; | ||||
| import { | ||||
|   WorkspaceQueryRunnerException, | ||||
|   WorkspaceQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; | ||||
| import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||
| import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; | ||||
| import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
| import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; | ||||
| import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; | ||||
| import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; | ||||
| import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; | ||||
| import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | ||||
| import { capitalize } from 'src/utils/capitalize'; | ||||
|  | ||||
| @Injectable() | ||||
| export class GraphqlQueryRunnerService { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly workspaceQueryHookService: WorkspaceQueryHookService, | ||||
|     private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, | ||||
|     private readonly workspaceEventEmitter: WorkspaceEventEmitter, | ||||
|     @InjectMessageQueue(MessageQueue.webhookQueue) | ||||
|     private readonly messageQueueService: MessageQueueService, | ||||
|     private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory, | ||||
|     private readonly apiEventEmitterService: ApiEventEmitterService, | ||||
|   ) {} | ||||
|  | ||||
|   /** QUERIES */ | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async findOne< | ||||
|     ObjectRecord extends IRecord = IRecord, | ||||
|     Filter extends RecordFilter = RecordFilter, | ||||
|   >( | ||||
|   async findOne<ObjectRecord extends IRecord, Filter extends RecordFilter>( | ||||
|     args: FindOneResolverArgs<Filter>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord | undefined> { | ||||
|     const graphqlQueryFindOneResolverService = | ||||
|       new GraphqlQueryFindOneResolverService(this.twentyORMGlobalManager); | ||||
|  | ||||
|     const { authContext, objectMetadataItem } = options; | ||||
|  | ||||
|     if (!args.filter || Object.keys(args.filter).length === 0) { | ||||
|       throw new WorkspaceQueryRunnerException( | ||||
|         'Missing filter argument', | ||||
|         WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const hookedArgs = | ||||
|       await this.workspaceQueryHookService.executePreQueryHooks( | ||||
|         authContext, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         'findOne', | ||||
|         args, | ||||
|       ); | ||||
|  | ||||
|     const computedArgs = (await this.queryRunnerArgsFactory.create( | ||||
|       hookedArgs, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     return this.executeQuery<FindOneResolverArgs<Filter>, ObjectRecord>( | ||||
|       'findOne', | ||||
|       args, | ||||
|       options, | ||||
|       ResolverArgsType.FindOne, | ||||
|     )) as FindOneResolverArgs<Filter>; | ||||
|  | ||||
|     return graphqlQueryFindOneResolverService.findOne(computedArgs, options); | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async findMany< | ||||
|     ObjectRecord extends IRecord = IRecord, | ||||
|     Filter extends RecordFilter = RecordFilter, | ||||
|     OrderBy extends RecordOrderBy = RecordOrderBy, | ||||
|     ObjectRecord extends IRecord, | ||||
|     Filter extends RecordFilter, | ||||
|     OrderBy extends RecordOrderBy, | ||||
|   >( | ||||
|     args: FindManyResolverArgs<Filter, OrderBy>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<IConnection<ObjectRecord>> { | ||||
|     const graphqlQueryFindManyResolverService = | ||||
|       new GraphqlQueryFindManyResolverService(this.twentyORMGlobalManager); | ||||
|  | ||||
|     const { authContext, objectMetadataItem } = options; | ||||
|  | ||||
|     const hookedArgs = | ||||
|       await this.workspaceQueryHookService.executePreQueryHooks( | ||||
|         authContext, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         'findMany', | ||||
|         args, | ||||
|       ); | ||||
|  | ||||
|     const computedArgs = (await this.queryRunnerArgsFactory.create( | ||||
|       hookedArgs, | ||||
|       options, | ||||
|       ResolverArgsType.FindMany, | ||||
|     )) as FindManyResolverArgs<Filter, OrderBy>; | ||||
|  | ||||
|     return graphqlQueryFindManyResolverService.findMany(computedArgs, options); | ||||
|   ): Promise<IConnection<ObjectRecord, IEdge<ObjectRecord>>> { | ||||
|     return this.executeQuery< | ||||
|       FindManyResolverArgs<Filter, OrderBy>, | ||||
|       IConnection<ObjectRecord, IEdge<ObjectRecord>> | ||||
|     >('findMany', args, options); | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async createOne<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: CreateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|   async findDuplicates<ObjectRecord extends IRecord>( | ||||
|     args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord | undefined> { | ||||
|     const graphqlQueryCreateManyResolverService = | ||||
|       new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager); | ||||
|  | ||||
|     const { authContext, objectMetadataItem } = options; | ||||
|  | ||||
|     assertMutationNotOnRemoteObject(objectMetadataItem); | ||||
|  | ||||
|     if (args.data.id) { | ||||
|       assertIsValidUuid(args.data.id); | ||||
|     } | ||||
|  | ||||
|     const createManyArgs = { | ||||
|       data: [args.data], | ||||
|       upsert: args.upsert, | ||||
|     } as CreateManyResolverArgs<ObjectRecord>; | ||||
|  | ||||
|     const hookedArgs = | ||||
|       await this.workspaceQueryHookService.executePreQueryHooks( | ||||
|         authContext, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         'createMany', | ||||
|         createManyArgs, | ||||
|       ); | ||||
|  | ||||
|     const computedArgs = (await this.queryRunnerArgsFactory.create( | ||||
|       hookedArgs, | ||||
|       options, | ||||
|       ResolverArgsType.CreateMany, | ||||
|     )) as CreateManyResolverArgs<ObjectRecord>; | ||||
|  | ||||
|     const results = (await graphqlQueryCreateManyResolverService.createMany( | ||||
|       computedArgs, | ||||
|       options, | ||||
|     )) as ObjectRecord[]; | ||||
|  | ||||
|     await this.triggerWebhooks<ObjectRecord>( | ||||
|       results, | ||||
|       CallWebhookJobsJobOperation.create, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     this.emitCreateEvents<ObjectRecord>( | ||||
|       results, | ||||
|       authContext, | ||||
|       objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     return results?.[0] as ObjectRecord; | ||||
|   ): Promise<IConnection<ObjectRecord>[]> { | ||||
|     return this.executeQuery< | ||||
|       FindDuplicatesResolverArgs<Partial<ObjectRecord>>, | ||||
|       IConnection<ObjectRecord>[] | ||||
|     >('findDuplicates', args, options); | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
| @@ -187,104 +97,286 @@ export class GraphqlQueryRunnerService { | ||||
|     args: SearchResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<IConnection<ObjectRecord>> { | ||||
|     const graphqlQuerySearchResolverService = | ||||
|       new GraphqlQuerySearchResolverService( | ||||
|         this.twentyORMGlobalManager, | ||||
|         this.featureFlagService, | ||||
|       ); | ||||
|     return this.executeQuery<SearchResolverArgs, IConnection<ObjectRecord>>( | ||||
|       'search', | ||||
|       args, | ||||
|       options, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|     return graphqlQuerySearchResolverService.search(args, options); | ||||
|   /** MUTATIONS */ | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async createOne<ObjectRecord extends IRecord>( | ||||
|     args: CreateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const results = await this.executeQuery< | ||||
|       CreateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord[] | ||||
|     >('createMany', { data: [args.data], upsert: args.upsert }, options); | ||||
|  | ||||
|     // TODO: emitCreateEvents should be moved to the ORM layer | ||||
|     if (results) { | ||||
|       this.apiEventEmitterService.emitCreateEvents( | ||||
|         results, | ||||
|         options.authContext, | ||||
|         options.objectMetadataItem, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return results[0]; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async createMany<ObjectRecord extends IRecord = IRecord>( | ||||
|   async createMany<ObjectRecord extends IRecord>( | ||||
|     args: CreateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord[] | undefined> { | ||||
|     const graphqlQueryCreateManyResolverService = | ||||
|       new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager); | ||||
|   ): Promise<ObjectRecord[]> { | ||||
|     const results = await this.executeQuery< | ||||
|       CreateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord[] | ||||
|     >('createMany', args, options); | ||||
|  | ||||
|     if (results) { | ||||
|       this.apiEventEmitterService.emitCreateEvents( | ||||
|         results, | ||||
|         options.authContext, | ||||
|         options.objectMetadataItem, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return results; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   public async updateOne<ObjectRecord extends IRecord>( | ||||
|     args: UpdateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const existingRecord = await this.executeQuery< | ||||
|       FindOneResolverArgs, | ||||
|       ObjectRecord | ||||
|     >( | ||||
|       'findOne', | ||||
|       { | ||||
|         filter: { id: { eq: args.id } }, | ||||
|       }, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     const result = await this.executeQuery< | ||||
|       UpdateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord | ||||
|     >('updateOne', args, options); | ||||
|  | ||||
|     this.apiEventEmitterService.emitUpdateEvents( | ||||
|       [existingRecord], | ||||
|       [result], | ||||
|       Object.keys(args.data), | ||||
|       options.authContext, | ||||
|       options.objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   public async updateMany<ObjectRecord extends IRecord>( | ||||
|     args: UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord[]> { | ||||
|     const existingRecords = await this.executeQuery< | ||||
|       FindManyResolverArgs, | ||||
|       IConnection<ObjectRecord, IEdge<ObjectRecord>> | ||||
|     >( | ||||
|       'findMany', | ||||
|       { | ||||
|         filter: args.filter, | ||||
|       }, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     const result = await this.executeQuery< | ||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord[] | ||||
|     >('updateMany', args, options); | ||||
|  | ||||
|     this.apiEventEmitterService.emitUpdateEvents( | ||||
|       existingRecords.edges.map((edge) => edge.node), | ||||
|       result, | ||||
|       Object.keys(args.data), | ||||
|       options.authContext, | ||||
|       options.objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   public async deleteOne<ObjectRecord extends IRecord & { deletedAt?: Date }>( | ||||
|     args: DeleteOneResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const result = await this.executeQuery< | ||||
|       UpdateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord | ||||
|     >( | ||||
|       'deleteOne', | ||||
|       { | ||||
|         id: args.id, | ||||
|         data: { deletedAt: new Date() } as Partial<ObjectRecord>, | ||||
|       }, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     this.apiEventEmitterService.emitDeletedEvents( | ||||
|       [result], | ||||
|       options.authContext, | ||||
|       options.objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   public async deleteMany<ObjectRecord extends IRecord & { deletedAt?: Date }>( | ||||
|     args: DeleteManyResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord[]> { | ||||
|     const result = await this.executeQuery< | ||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord[] | ||||
|     >( | ||||
|       'deleteMany', | ||||
|       { | ||||
|         filter: args.filter, | ||||
|  | ||||
|         data: { deletedAt: new Date() } as Partial<ObjectRecord>, | ||||
|       }, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     this.apiEventEmitterService.emitDeletedEvents( | ||||
|       result, | ||||
|       options.authContext, | ||||
|       options.objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async destroyOne<ObjectRecord extends IRecord>( | ||||
|     args: DestroyOneResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const result = await this.executeQuery< | ||||
|       DestroyOneResolverArgs, | ||||
|       ObjectRecord | ||||
|     >('destroyOne', args, options); | ||||
|  | ||||
|     this.apiEventEmitterService.emitDestroyEvents( | ||||
|       [result], | ||||
|       options.authContext, | ||||
|       options.objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   public async restoreMany<ObjectRecord extends IRecord>( | ||||
|     args: RestoreManyResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const result = await this.executeQuery< | ||||
|       UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|       ObjectRecord | ||||
|     >( | ||||
|       'restoreMany', | ||||
|       { | ||||
|         filter: args.filter, | ||||
|         data: { deletedAt: null } as Partial<ObjectRecord>, | ||||
|       }, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   private async executeQuery<Input extends ResolverArgs, Response>( | ||||
|     operationName: WorkspaceResolverBuilderMethodNames, | ||||
|     args: Input, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<Response> { | ||||
|     const { authContext, objectMetadataItem } = options; | ||||
|  | ||||
|     assertMutationNotOnRemoteObject(objectMetadataItem); | ||||
|     const resolver = | ||||
|       this.graphqlQueryResolverFactory.getResolver(operationName); | ||||
|  | ||||
|     args.data.forEach((record) => { | ||||
|       if (record?.id) { | ||||
|         assertIsValidUuid(record.id); | ||||
|       } | ||||
|     }); | ||||
|     await resolver.validate(args, options); | ||||
|  | ||||
|     const hookedArgs = | ||||
|       await this.workspaceQueryHookService.executePreQueryHooks( | ||||
|         authContext, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         'createMany', | ||||
|         operationName, | ||||
|         args, | ||||
|       ); | ||||
|  | ||||
|     const computedArgs = (await this.queryRunnerArgsFactory.create( | ||||
|     const computedArgs = await this.queryRunnerArgsFactory.create( | ||||
|       hookedArgs, | ||||
|       options, | ||||
|       ResolverArgsType.CreateMany, | ||||
|     )) as CreateManyResolverArgs<ObjectRecord>; | ||||
|       ResolverArgsType[capitalize(operationName)], | ||||
|     ); | ||||
|  | ||||
|     const results = (await graphqlQueryCreateManyResolverService.createMany( | ||||
|       computedArgs, | ||||
|       options, | ||||
|     )) as ObjectRecord[]; | ||||
|     const results = await resolver.resolve(computedArgs as Input, options); | ||||
|  | ||||
|     await this.workspaceQueryHookService.executePostQueryHooks( | ||||
|       authContext, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       'createMany', | ||||
|       results, | ||||
|       operationName, | ||||
|       Array.isArray(results) ? results : [results], | ||||
|     ); | ||||
|  | ||||
|     await this.triggerWebhooks<ObjectRecord>( | ||||
|       results, | ||||
|       CallWebhookJobsJobOperation.create, | ||||
|       options, | ||||
|     ); | ||||
|     const jobOperation = this.operationNameToJobOperation(operationName); | ||||
|  | ||||
|     this.emitCreateEvents<ObjectRecord>( | ||||
|       results, | ||||
|       authContext, | ||||
|       objectMetadataItem, | ||||
|     ); | ||||
|     if (jobOperation) { | ||||
|       await this.triggerWebhooks(results, jobOperation, options); | ||||
|     } | ||||
|  | ||||
|     return results; | ||||
|   } | ||||
|  | ||||
|   private emitCreateEvents<BaseRecord extends IRecord = IRecord>( | ||||
|     records: BaseRecord[], | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataItem: ObjectMetadataInterface, | ||||
|   ) { | ||||
|     this.workspaceEventEmitter.emit( | ||||
|       `${objectMetadataItem.nameSingular}.created`, | ||||
|       records.map( | ||||
|         (record) => | ||||
|           ({ | ||||
|             userId: authContext.user?.id, | ||||
|             recordId: record.id, | ||||
|             objectMetadata: objectMetadataItem, | ||||
|             properties: { | ||||
|               after: record, | ||||
|             }, | ||||
|           }) satisfies ObjectRecordCreateEvent<any>, | ||||
|       ), | ||||
|       authContext.workspace.id, | ||||
|     ); | ||||
|   private operationNameToJobOperation( | ||||
|     operationName: WorkspaceResolverBuilderMethodNames, | ||||
|   ): CallWebhookJobsJobOperation | undefined { | ||||
|     switch (operationName) { | ||||
|       case 'createOne': | ||||
|       case 'createMany': | ||||
|         return CallWebhookJobsJobOperation.create; | ||||
|       case 'updateOne': | ||||
|       case 'updateMany': | ||||
|       case 'restoreMany': | ||||
|         return CallWebhookJobsJobOperation.update; | ||||
|       case 'deleteOne': | ||||
|       case 'deleteMany': | ||||
|         return CallWebhookJobsJobOperation.delete; | ||||
|       case 'destroyOne': | ||||
|         return CallWebhookJobsJobOperation.destroy; | ||||
|       default: | ||||
|         return undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async triggerWebhooks<Record>( | ||||
|     jobsData: Record[] | undefined, | ||||
|   private async triggerWebhooks<T>( | ||||
|     jobsData: T[] | undefined, | ||||
|     operation: CallWebhookJobsJobOperation, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ) { | ||||
|     if (!Array.isArray(jobsData)) { | ||||
|       return; | ||||
|     } | ||||
|   ): Promise<void> { | ||||
|     if (!jobsData || !Array.isArray(jobsData)) return; | ||||
|  | ||||
|     jobsData.forEach((jobData) => { | ||||
|       this.messageQueueService.add<CallWebhookJobsJobData>( | ||||
|         CallWebhookJobsJob.name, | ||||
| @@ -298,99 +390,4 @@ export class GraphqlQueryRunnerService { | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @LogExecutionTime() | ||||
|   async destroyOne<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: DestroyOneResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord | undefined> { | ||||
|     const graphqlQueryDestroyOneResolverService = | ||||
|       new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager); | ||||
|  | ||||
|     const { authContext, objectMetadataItem } = options; | ||||
|  | ||||
|     assertMutationNotOnRemoteObject(objectMetadataItem); | ||||
|     assertIsValidUuid(args.id); | ||||
|  | ||||
|     const hookedArgs = | ||||
|       await this.workspaceQueryHookService.executePreQueryHooks( | ||||
|         authContext, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         'destroyOne', | ||||
|         args, | ||||
|       ); | ||||
|  | ||||
|     const computedArgs = (await this.queryRunnerArgsFactory.create( | ||||
|       hookedArgs, | ||||
|       options, | ||||
|       ResolverArgsType.DestroyOne, | ||||
|     )) as DestroyOneResolverArgs; | ||||
|  | ||||
|     const result = (await graphqlQueryDestroyOneResolverService.destroyOne( | ||||
|       computedArgs, | ||||
|       options, | ||||
|     )) as ObjectRecord; | ||||
|  | ||||
|     await this.workspaceQueryHookService.executePostQueryHooks( | ||||
|       authContext, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       'destroyOne', | ||||
|       [result], | ||||
|     ); | ||||
|  | ||||
|     await this.triggerWebhooks<IRecord>( | ||||
|       [result], | ||||
|       CallWebhookJobsJobOperation.destroy, | ||||
|       options, | ||||
|     ); | ||||
|  | ||||
|     this.emitDestroyEvents<IRecord>([result], authContext, objectMetadataItem); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   private emitDestroyEvents<BaseRecord extends IRecord = IRecord>( | ||||
|     records: BaseRecord[], | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataItem: ObjectMetadataInterface, | ||||
|   ) { | ||||
|     this.workspaceEventEmitter.emit( | ||||
|       `${objectMetadataItem.nameSingular}.destroyed`, | ||||
|       records.map((record) => { | ||||
|         return { | ||||
|           userId: authContext.user?.id, | ||||
|           recordId: record.id, | ||||
|           objectMetadata: objectMetadataItem, | ||||
|           properties: { | ||||
|             before: this.removeNestedProperties(record), | ||||
|           }, | ||||
|         } satisfies ObjectRecordDeleteEvent<any>; | ||||
|       }), | ||||
|       authContext.workspace.id, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private removeNestedProperties<Record extends IRecord = IRecord>( | ||||
|     record: Record, | ||||
|   ) { | ||||
|     if (!record) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const sanitizedRecord = {}; | ||||
|  | ||||
|     for (const [key, value] of Object.entries(record)) { | ||||
|       if (value && typeof value === 'object' && value['edges']) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       if (key === '__typename') { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       sanitizedRecord[key] = value; | ||||
|     } | ||||
|  | ||||
|     return sanitizedRecord; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac | ||||
| import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; | ||||
| import { isPlainObject } from 'src/utils/is-plain-object'; | ||||
| 
 | ||||
| export class ObjectRecordsToGraphqlConnectionMapper { | ||||
| export class ObjectRecordsToGraphqlConnectionHelper { | ||||
|   private objectMetadataMap: ObjectMetadataMap; | ||||
| 
 | ||||
|   constructor(objectMetadataMap: ObjectMetadataMap) { | ||||
| @@ -4,6 +4,7 @@ import { | ||||
|   FindOptionsRelations, | ||||
|   In, | ||||
|   ObjectLiteral, | ||||
|   Repository, | ||||
| } from 'typeorm'; | ||||
|  | ||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| @@ -16,14 +17,70 @@ import { | ||||
|   ObjectMetadataMap, | ||||
|   ObjectMetadataMapItem, | ||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util'; | ||||
|  | ||||
| export class ProcessNestedRelationsHelper { | ||||
|   private readonly twentyORMGlobalManager: TwentyORMGlobalManager; | ||||
|   constructor() {} | ||||
|  | ||||
|   constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { | ||||
|     this.twentyORMGlobalManager = twentyORMGlobalManager; | ||||
|   public async processNestedRelations<ObjectRecord extends IRecord = IRecord>( | ||||
|     objectMetadataMap: ObjectMetadataMap, | ||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, | ||||
|     parentObjectRecords: ObjectRecord[], | ||||
|     relations: Record<string, FindOptionsRelations<ObjectLiteral>>, | ||||
|     limit: number, | ||||
|     authContext: any, | ||||
|     dataSource: DataSource, | ||||
|   ): Promise<void> { | ||||
|     const processRelationTasks = Object.entries(relations).map( | ||||
|       ([relationName, nestedRelations]) => | ||||
|         this.processRelation( | ||||
|           objectMetadataMap, | ||||
|           parentObjectMetadataItem, | ||||
|           parentObjectRecords, | ||||
|           relationName, | ||||
|           nestedRelations, | ||||
|           limit, | ||||
|           authContext, | ||||
|           dataSource, | ||||
|         ), | ||||
|     ); | ||||
|  | ||||
|     await Promise.all(processRelationTasks); | ||||
|   } | ||||
|  | ||||
|   private async processRelation<ObjectRecord extends IRecord = IRecord>( | ||||
|     objectMetadataMap: ObjectMetadataMap, | ||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, | ||||
|     parentObjectRecords: ObjectRecord[], | ||||
|     relationName: string, | ||||
|     nestedRelations: any, | ||||
|     limit: number, | ||||
|     authContext: any, | ||||
|     dataSource: DataSource, | ||||
|   ): Promise<void> { | ||||
|     const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; | ||||
|     const relationMetadata = getRelationMetadata(relationFieldMetadata); | ||||
|     const relationDirection = deduceRelationDirection( | ||||
|       relationFieldMetadata, | ||||
|       relationMetadata, | ||||
|     ); | ||||
|  | ||||
|     const processor = | ||||
|       relationDirection === 'to' | ||||
|         ? this.processToRelation | ||||
|         : this.processFromRelation; | ||||
|  | ||||
|     await processor.call( | ||||
|       this, | ||||
|       objectMetadataMap, | ||||
|       parentObjectMetadataItem, | ||||
|       parentObjectRecords, | ||||
|       relationName, | ||||
|       nestedRelations, | ||||
|       limit, | ||||
|       authContext, | ||||
|       dataSource, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private async processFromRelation<ObjectRecord extends IRecord = IRecord>( | ||||
| @@ -35,49 +92,36 @@ export class ProcessNestedRelationsHelper { | ||||
|     limit: number, | ||||
|     authContext: any, | ||||
|     dataSource: DataSource, | ||||
|   ) { | ||||
|     const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; | ||||
|     const relationMetadata = getRelationMetadata(relationFieldMetadata); | ||||
|  | ||||
|     const inverseRelationName = | ||||
|       objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[ | ||||
|         relationMetadata.toFieldMetadataId | ||||
|       ]?.name; | ||||
|  | ||||
|     const referenceObjectMetadata = getRelationObjectMetadata( | ||||
|       relationFieldMetadata, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const referenceObjectMetadataName = referenceObjectMetadata.nameSingular; | ||||
|  | ||||
|     const relationRepository = await dataSource.getRepository( | ||||
|       referenceObjectMetadataName, | ||||
|     ); | ||||
|  | ||||
|     const relationIds = parentObjectRecords.map((item) => item.id); | ||||
|  | ||||
|     const uniqueRelationIds = [...new Set(relationIds)]; | ||||
|  | ||||
|     const relationFindOptions: FindManyOptions = { | ||||
|       where: { | ||||
|         [`${inverseRelationName}Id`]: In(uniqueRelationIds), | ||||
|       }, | ||||
|       take: limit * parentObjectRecords.length, | ||||
|     }; | ||||
|  | ||||
|     const relationResults = await relationRepository.find(relationFindOptions); | ||||
|  | ||||
|     parentObjectRecords.forEach((item) => { | ||||
|       (item as any)[relationName] = relationResults.filter( | ||||
|         (rel) => rel[`${inverseRelationName}Id`] === item.id, | ||||
|   ): Promise<void> { | ||||
|     const { inverseRelationName, referenceObjectMetadata } = | ||||
|       this.getRelationMetadata( | ||||
|         objectMetadataMap, | ||||
|         parentObjectMetadataItem, | ||||
|         relationName, | ||||
|       ); | ||||
|     }); | ||||
|     const relationRepository = dataSource.getRepository( | ||||
|       referenceObjectMetadata.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const relationIds = this.getUniqueIds(parentObjectRecords, 'id'); | ||||
|     const relationResults = await this.findRelations( | ||||
|       relationRepository, | ||||
|       inverseRelationName, | ||||
|       relationIds, | ||||
|       limit * parentObjectRecords.length, | ||||
|     ); | ||||
|  | ||||
|     this.assignRelationResults( | ||||
|       parentObjectRecords, | ||||
|       relationResults, | ||||
|       relationName, | ||||
|       `${inverseRelationName}Id`, | ||||
|     ); | ||||
|  | ||||
|     if (Object.keys(nestedRelations).length > 0) { | ||||
|       await this.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadataMap[referenceObjectMetadataName], | ||||
|         objectMetadataMap[referenceObjectMetadata.nameSingular], | ||||
|         relationResults as ObjectRecord[], | ||||
|         nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>, | ||||
|         limit, | ||||
| @@ -96,48 +140,37 @@ export class ProcessNestedRelationsHelper { | ||||
|     limit: number, | ||||
|     authContext: any, | ||||
|     dataSource: DataSource, | ||||
|   ) { | ||||
|     const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; | ||||
|  | ||||
|     const referenceObjectMetadata = getRelationObjectMetadata( | ||||
|       relationFieldMetadata, | ||||
|   ): Promise<void> { | ||||
|     const { referenceObjectMetadata } = this.getRelationMetadata( | ||||
|       objectMetadataMap, | ||||
|       parentObjectMetadataItem, | ||||
|       relationName, | ||||
|     ); | ||||
|  | ||||
|     const referenceObjectMetadataName = referenceObjectMetadata.nameSingular; | ||||
|  | ||||
|     const relationRepository = dataSource.getRepository( | ||||
|       referenceObjectMetadataName, | ||||
|       referenceObjectMetadata.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const relationIds = parentObjectRecords.map( | ||||
|       (item) => item[`${relationName}Id`], | ||||
|     const relationIds = this.getUniqueIds( | ||||
|       parentObjectRecords, | ||||
|       `${relationName}Id`, | ||||
|     ); | ||||
|     const relationResults = await this.findRelations( | ||||
|       relationRepository, | ||||
|       'id', | ||||
|       relationIds, | ||||
|       limit, | ||||
|     ); | ||||
|  | ||||
|     const uniqueRelationIds = [...new Set(relationIds)]; | ||||
|  | ||||
|     const relationFindOptions: FindManyOptions = { | ||||
|       where: { | ||||
|         id: In(uniqueRelationIds), | ||||
|       }, | ||||
|       take: limit, | ||||
|     }; | ||||
|  | ||||
|     const relationResults = await relationRepository.find(relationFindOptions); | ||||
|  | ||||
|     parentObjectRecords.forEach((item) => { | ||||
|       if (relationResults.length === 0) { | ||||
|         (item as any)[`${relationName}Id`] = null; | ||||
|       } | ||||
|       (item as any)[relationName] = relationResults.filter( | ||||
|         (rel) => rel.id === item[`${relationName}Id`], | ||||
|       )[0]; | ||||
|     }); | ||||
|     this.assignToRelationResults( | ||||
|       parentObjectRecords, | ||||
|       relationResults, | ||||
|       relationName, | ||||
|     ); | ||||
|  | ||||
|     if (Object.keys(nestedRelations).length > 0) { | ||||
|       await this.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadataMap[referenceObjectMetadataName], | ||||
|         objectMetadataMap[referenceObjectMetadata.nameSingular], | ||||
|         relationResults as ObjectRecord[], | ||||
|         nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>, | ||||
|         limit, | ||||
| @@ -147,48 +180,71 @@ export class ProcessNestedRelationsHelper { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async processNestedRelations<ObjectRecord extends IRecord = IRecord>( | ||||
|   private getRelationMetadata( | ||||
|     objectMetadataMap: ObjectMetadataMap, | ||||
|     parentObjectMetadataItem: ObjectMetadataMapItem, | ||||
|     parentObjectRecords: ObjectRecord[], | ||||
|     relations: Record<string, FindOptionsRelations<ObjectLiteral>>, | ||||
|     limit: number, | ||||
|     authContext: any, | ||||
|     dataSource: DataSource, | ||||
|     relationName: string, | ||||
|   ) { | ||||
|     for (const [relationName, nestedRelations] of Object.entries(relations)) { | ||||
|       const relationFieldMetadata = | ||||
|         parentObjectMetadataItem.fields[relationName]; | ||||
|       const relationMetadata = getRelationMetadata(relationFieldMetadata); | ||||
|     const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; | ||||
|     const relationMetadata = getRelationMetadata(relationFieldMetadata); | ||||
|     const referenceObjectMetadata = getRelationObjectMetadata( | ||||
|       relationFieldMetadata, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|     const inverseRelationName = | ||||
|       objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[ | ||||
|         relationMetadata.toFieldMetadataId | ||||
|       ]?.name; | ||||
|  | ||||
|       const relationDirection = deduceRelationDirection( | ||||
|         relationFieldMetadata, | ||||
|         relationMetadata, | ||||
|       ); | ||||
|     return { inverseRelationName, referenceObjectMetadata }; | ||||
|   } | ||||
|  | ||||
|       if (relationDirection === 'to') { | ||||
|         await this.processToRelation( | ||||
|           objectMetadataMap, | ||||
|           parentObjectMetadataItem, | ||||
|           parentObjectRecords, | ||||
|           relationName, | ||||
|           nestedRelations, | ||||
|           limit, | ||||
|           authContext, | ||||
|           dataSource, | ||||
|         ); | ||||
|       } else { | ||||
|         await this.processFromRelation( | ||||
|           objectMetadataMap, | ||||
|           parentObjectMetadataItem, | ||||
|           parentObjectRecords, | ||||
|           relationName, | ||||
|           nestedRelations, | ||||
|           limit, | ||||
|           authContext, | ||||
|           dataSource, | ||||
|         ); | ||||
|       } | ||||
|   private getUniqueIds(records: IRecord[], idField: string): any[] { | ||||
|     return [...new Set(records.map((item) => item[idField]))]; | ||||
|   } | ||||
|  | ||||
|   private async findRelations( | ||||
|     repository: Repository<any>, | ||||
|     field: string, | ||||
|     ids: any[], | ||||
|     limit: number, | ||||
|   ): Promise<any[]> { | ||||
|     if (ids.length === 0) { | ||||
|       return []; | ||||
|     } | ||||
|     const findOptions: FindManyOptions = { | ||||
|       where: { [field]: In(ids) }, | ||||
|       take: limit, | ||||
|     }; | ||||
|  | ||||
|     return repository.find(findOptions); | ||||
|   } | ||||
|  | ||||
|   private assignRelationResults( | ||||
|     parentRecords: IRecord[], | ||||
|     relationResults: any[], | ||||
|     relationName: string, | ||||
|     joinField: string, | ||||
|   ): void { | ||||
|     parentRecords.forEach((item) => { | ||||
|       (item as any)[relationName] = relationResults.filter( | ||||
|         (rel) => rel[joinField] === item.id, | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private assignToRelationResults( | ||||
|     parentRecords: IRecord[], | ||||
|     relationResults: any[], | ||||
|     relationName: string, | ||||
|   ): void { | ||||
|     parentRecords.forEach((item) => { | ||||
|       if (relationResults.length === 0) { | ||||
|         (item as any)[`${relationName}Id`] = null; | ||||
|       } | ||||
|       (item as any)[relationName] = | ||||
|         relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ?? | ||||
|         null; | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
|  | ||||
| export interface ResolverService<ResolverArgs, T> { | ||||
|   resolve: ( | ||||
|     args: ResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ) => Promise<T>; | ||||
|   validate: ( | ||||
|     args: ResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ) => Promise<void>; | ||||
| } | ||||
| @@ -1,51 +1,53 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import graphqlFields from 'graphql-fields'; | ||||
| import { In, InsertResult } from 'typeorm'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; | ||||
| import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; | ||||
| import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; | ||||
| import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util'; | ||||
| import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; | ||||
| import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; | ||||
| import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| export class GraphqlQueryCreateManyResolverService { | ||||
|   private twentyORMGlobalManager: TwentyORMGlobalManager; | ||||
| @Injectable() | ||||
| export class GraphqlQueryCreateManyResolverService | ||||
|   implements ResolverService<CreateManyResolverArgs, IRecord[]> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { | ||||
|     this.twentyORMGlobalManager = twentyORMGlobalManager; | ||||
|   } | ||||
|  | ||||
|   async createMany<ObjectRecord extends IRecord = IRecord>( | ||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: CreateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord[] | undefined> { | ||||
|     const { authContext, objectMetadataItem, objectMetadataCollection, info } = | ||||
|   ): Promise<ObjectRecord[]> { | ||||
|     const { authContext, info, objectMetadataMap, objectMetadataMapItem } = | ||||
|       options; | ||||
|     const repository = | ||||
|       await this.twentyORMGlobalManager.getRepositoryForWorkspace( | ||||
|     const dataSource = | ||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||
|         authContext.workspace.id, | ||||
|         objectMetadataItem.nameSingular, | ||||
|       ); | ||||
|     const repository = dataSource.getRepository( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const objectMetadataMap = generateObjectMetadataMap( | ||||
|       objectMetadataCollection, | ||||
|     ); | ||||
|     const objectMetadata = getObjectMetadataOrThrow( | ||||
|       objectMetadataMap, | ||||
|       objectMetadataItem.nameSingular, | ||||
|     ); | ||||
|     const graphqlQueryParser = new GraphqlQueryParser( | ||||
|       objectMetadata.fields, | ||||
|       objectMetadataMapItem.fields, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const selectedFields = graphqlFields(info); | ||||
|  | ||||
|     const { select, relations } = graphqlQueryParser.parseSelectedFields( | ||||
|       objectMetadataItem, | ||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||
|       objectMetadataMapItem, | ||||
|       selectedFields, | ||||
|     ); | ||||
|  | ||||
| @@ -56,24 +58,59 @@ export class GraphqlQueryCreateManyResolverService { | ||||
|           skipUpdateIfNoValuesChanged: true, | ||||
|         }); | ||||
|  | ||||
|     const upsertedRecords = await repository.find({ | ||||
|       where: { | ||||
|     const queryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const nonFormattedUpsertedRecords = (await queryBuilder | ||||
|       .where({ | ||||
|         id: In(objectRecords.generatedMaps.map((record) => record.id)), | ||||
|       }, | ||||
|       select, | ||||
|       relations, | ||||
|     }); | ||||
|       }) | ||||
|       .take(QUERY_MAX_RECORDS) | ||||
|       .getMany()) as ObjectRecord[]; | ||||
|  | ||||
|     const upsertedRecords = formatResult( | ||||
|       nonFormattedUpsertedRecords, | ||||
|       objectMetadataMapItem, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||
|  | ||||
|     if (relations) { | ||||
|       await processNestedRelationsHelper.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadataMapItem, | ||||
|         upsertedRecords, | ||||
|         relations, | ||||
|         QUERY_MAX_RECORDS, | ||||
|         authContext, | ||||
|         dataSource, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     return upsertedRecords.map((record: ObjectRecord) => | ||||
|       typeORMObjectRecordsParser.processRecord( | ||||
|         record, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         objectMetadataMapItem.nameSingular, | ||||
|         1, | ||||
|         1, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   async validate<ObjectRecord extends IRecord>( | ||||
|     args: CreateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     assertMutationNotOnRemoteObject(options.objectMetadataItem); | ||||
|     args.data.forEach((record) => { | ||||
|       if (record?.id) { | ||||
|         assertIsValidUuid(record.id); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,33 +1,68 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { | ||||
|   GraphqlQueryRunnerException, | ||||
|   GraphqlQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| export class GraphqlQueryDestroyOneResolverService { | ||||
|   private twentyORMGlobalManager: TwentyORMGlobalManager; | ||||
| @Injectable() | ||||
| export class GraphqlQueryDestroyOneResolverService | ||||
|   implements ResolverService<DestroyOneResolverArgs, IRecord> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { | ||||
|     this.twentyORMGlobalManager = twentyORMGlobalManager; | ||||
|   } | ||||
|  | ||||
|   async destroyOne<ObjectRecord extends IRecord = IRecord>( | ||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: DestroyOneResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const { authContext, objectMetadataItem } = options; | ||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap } = options; | ||||
|     const repository = | ||||
|       await this.twentyORMGlobalManager.getRepositoryForWorkspace( | ||||
|         authContext.workspace.id, | ||||
|         objectMetadataItem.nameSingular, | ||||
|         objectMetadataMapItem.nameSingular, | ||||
|       ); | ||||
|  | ||||
|     const record = await repository.findOne({ | ||||
|     const nonFormattedRecordBeforeDeletion = await repository.findOne({ | ||||
|       where: { id: args.id }, | ||||
|       withDeleted: true, | ||||
|     }); | ||||
|  | ||||
|     if (!nonFormattedRecordBeforeDeletion) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'Record not found', | ||||
|         GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const recordBeforeDeletion = formatResult( | ||||
|       [nonFormattedRecordBeforeDeletion], | ||||
|       objectMetadataMapItem, | ||||
|       objectMetadataMap, | ||||
|     )[0]; | ||||
|  | ||||
|     await repository.delete(args.id); | ||||
|  | ||||
|     return record as ObjectRecord; | ||||
|     return recordBeforeDeletion as ObjectRecord; | ||||
|   } | ||||
|  | ||||
|   async validate( | ||||
|     args: DestroyOneResolverArgs, | ||||
|     _options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     if (!args.id) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'Missing id', | ||||
|         GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,214 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import isEmpty from 'lodash.isempty'; | ||||
| import { In } from 'typeorm'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { | ||||
|   Record as IRecord, | ||||
|   OrderByDirection, | ||||
|   RecordFilter, | ||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { | ||||
|   GraphqlQueryRunnerException, | ||||
|   GraphqlQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||||
| import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { settings } from 'src/engine/constants/settings'; | ||||
| import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; | ||||
| import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| @Injectable() | ||||
| export class GraphqlQueryFindDuplicatesResolverService | ||||
|   implements | ||||
|     ResolverService<FindDuplicatesResolverArgs, IConnection<IRecord>[]> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<IConnection<ObjectRecord>[]> { | ||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap } = options; | ||||
|  | ||||
|     const dataSource = | ||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||
|         authContext.workspace.id, | ||||
|       ); | ||||
|     const repository = dataSource.getRepository( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|     const existingRecordsQueryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|     const duplicateRecordsQueryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const graphqlQueryParser = new GraphqlQueryParser( | ||||
|       objectMetadataMap[objectMetadataMapItem.nameSingular].fields, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     let objectRecords: Partial<ObjectRecord>[] = []; | ||||
|  | ||||
|     if (args.ids) { | ||||
|       const nonFormattedObjectRecords = (await existingRecordsQueryBuilder | ||||
|         .where({ id: In(args.ids) }) | ||||
|         .getMany()) as ObjectRecord[]; | ||||
|  | ||||
|       objectRecords = formatResult( | ||||
|         nonFormattedObjectRecords, | ||||
|         objectMetadataMapItem, | ||||
|         objectMetadataMap, | ||||
|       ); | ||||
|     } else if (args.data && !isEmpty(args.data)) { | ||||
|       objectRecords = formatData(args.data, objectMetadataMapItem); | ||||
|     } | ||||
|  | ||||
|     const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all( | ||||
|       objectRecords.map(async (record) => { | ||||
|         const duplicateConditions = this.buildDuplicateConditions( | ||||
|           objectMetadataMapItem, | ||||
|           [record], | ||||
|           record.id, | ||||
|         ); | ||||
|  | ||||
|         if (isEmpty(duplicateConditions)) { | ||||
|           return typeORMObjectRecordsParser.createConnection( | ||||
|             [], | ||||
|             objectMetadataMapItem.nameSingular, | ||||
|             0, | ||||
|             0, | ||||
|             [{ id: OrderByDirection.AscNullsFirst }], | ||||
|             false, | ||||
|             false, | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||
|           duplicateRecordsQueryBuilder, | ||||
|           objectMetadataMapItem.nameSingular, | ||||
|           duplicateConditions, | ||||
|         ); | ||||
|  | ||||
|         const nonFormattedDuplicates = | ||||
|           (await withFilterQueryBuilder.getMany()) as ObjectRecord[]; | ||||
|  | ||||
|         const duplicates = formatResult( | ||||
|           nonFormattedDuplicates, | ||||
|           objectMetadataMapItem, | ||||
|           objectMetadataMap, | ||||
|         ); | ||||
|  | ||||
|         return typeORMObjectRecordsParser.createConnection( | ||||
|           duplicates, | ||||
|           objectMetadataMapItem.nameSingular, | ||||
|           duplicates.length, | ||||
|           duplicates.length, | ||||
|           [{ id: OrderByDirection.AscNullsFirst }], | ||||
|           false, | ||||
|           false, | ||||
|         ); | ||||
|       }), | ||||
|     ); | ||||
|  | ||||
|     return duplicateConnections; | ||||
|   } | ||||
|  | ||||
|   private buildDuplicateConditions( | ||||
|     objectMetadataMapItem: ObjectMetadataMapItem, | ||||
|     records?: Partial<IRecord>[] | undefined, | ||||
|     filteringByExistingRecordId?: string, | ||||
|   ): Partial<RecordFilter> { | ||||
|     if (!records || records.length === 0) { | ||||
|       return {}; | ||||
|     } | ||||
|  | ||||
|     const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( | ||||
|       objectMetadataMapItem, | ||||
|     ); | ||||
|  | ||||
|     const conditions = records.flatMap((record) => { | ||||
|       const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) => | ||||
|         criteria.columnNames.every((columnName) => { | ||||
|           const value = record[columnName] as string | undefined; | ||||
|  | ||||
|           return ( | ||||
|             value && value.length >= settings.minLengthOfStringForDuplicateCheck | ||||
|           ); | ||||
|         }), | ||||
|       ); | ||||
|  | ||||
|       return criteriaWithMatchingArgs.map((criteria) => { | ||||
|         const condition = {}; | ||||
|  | ||||
|         criteria.columnNames.forEach((columnName) => { | ||||
|           condition[columnName] = { eq: record[columnName] }; | ||||
|         }); | ||||
|  | ||||
|         return condition; | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     const filter: Partial<RecordFilter> = {}; | ||||
|  | ||||
|     if (conditions && !isEmpty(conditions)) { | ||||
|       filter.or = conditions; | ||||
|  | ||||
|       if (filteringByExistingRecordId) { | ||||
|         filter.id = { neq: filteringByExistingRecordId }; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return filter; | ||||
|   } | ||||
|  | ||||
|   private getApplicableDuplicateCriteriaCollection( | ||||
|     objectMetadataMapItem: ObjectMetadataMapItem, | ||||
|   ) { | ||||
|     return DUPLICATE_CRITERIA_COLLECTION.filter( | ||||
|       (duplicateCriteria) => | ||||
|         duplicateCriteria.objectName === objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   async validate( | ||||
|     args: FindDuplicatesResolverArgs, | ||||
|     _options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     if (!args.data && !args.ids) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'You have to provide either "data" or "ids" argument', | ||||
|         GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (args.data && args.ids) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'You cannot provide both "data" and "ids" arguments', | ||||
|         GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (!args.ids && isEmpty(args.data)) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'The "data" condition can not be empty when "ids" input not provided', | ||||
|         GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { isDefined } from 'class-validator'; | ||||
| import graphqlFields from 'graphql-fields'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { | ||||
|   Record as IRecord, | ||||
|   OrderByDirection, | ||||
| @@ -17,26 +20,25 @@ import { | ||||
|   GraphqlQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||||
| import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; | ||||
| import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; | ||||
| import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; | ||||
| import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; | ||||
| import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util'; | ||||
| import { | ||||
|   ObjectMetadataMapItem, | ||||
|   generateObjectMetadataMap, | ||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
|   getCursor, | ||||
|   getPaginationInfo, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| export class GraphqlQueryFindManyResolverService { | ||||
|   private twentyORMGlobalManager: TwentyORMGlobalManager; | ||||
| @Injectable() | ||||
| export class GraphqlQueryFindManyResolverService | ||||
|   implements ResolverService<FindManyResolverArgs, IConnection<IRecord>> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { | ||||
|     this.twentyORMGlobalManager = twentyORMGlobalManager; | ||||
|   } | ||||
|  | ||||
|   async findMany< | ||||
|   async resolve< | ||||
|     ObjectRecord extends IRecord = IRecord, | ||||
|     Filter extends RecordFilter = RecordFilter, | ||||
|     OrderBy extends RecordOrderBy = RecordOrderBy, | ||||
| @@ -44,51 +46,41 @@ export class GraphqlQueryFindManyResolverService { | ||||
|     args: FindManyResolverArgs<Filter, OrderBy>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<IConnection<ObjectRecord>> { | ||||
|     const { authContext, objectMetadataItem, info, objectMetadataCollection } = | ||||
|     const { authContext, objectMetadataMapItem, info, objectMetadataMap } = | ||||
|       options; | ||||
|  | ||||
|     this.validateArgsOrThrow(args); | ||||
|  | ||||
|     const dataSource = | ||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||
|         authContext.workspace.id, | ||||
|       ); | ||||
|  | ||||
|     const repository = dataSource.getRepository( | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const queryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const countQueryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const objectMetadataMap = generateObjectMetadataMap( | ||||
|       objectMetadataCollection, | ||||
|     ); | ||||
|  | ||||
|     const objectMetadata = getObjectMetadataOrThrow( | ||||
|       objectMetadataMap, | ||||
|       objectMetadataItem.nameSingular, | ||||
|     ); | ||||
|     const graphqlQueryParser = new GraphqlQueryParser( | ||||
|       objectMetadata.fields, | ||||
|       objectMetadataMapItem.fields, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||
|       countQueryBuilder, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       args.filter ?? ({} as Filter), | ||||
|     ); | ||||
|  | ||||
|     const selectedFields = graphqlFields(info); | ||||
|  | ||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||
|       objectMetadataItem, | ||||
|       objectMetadataMapItem, | ||||
|       selectedFields, | ||||
|     ); | ||||
|     const isForwardPagination = !isDefined(args.before); | ||||
| @@ -105,7 +97,7 @@ export class GraphqlQueryFindManyResolverService { | ||||
|       ? await withDeletedCountQueryBuilder.getCount() | ||||
|       : 0; | ||||
|  | ||||
|     const cursor = this.getCursor(args); | ||||
|     const cursor = getCursor(args); | ||||
|  | ||||
|     let appliedFilters = args.filter ?? ({} as Filter); | ||||
|  | ||||
| @@ -118,7 +110,7 @@ export class GraphqlQueryFindManyResolverService { | ||||
|       const cursorArgFilter = computeCursorArgFilter( | ||||
|         cursor, | ||||
|         orderByWithIdCondition, | ||||
|         objectMetadata.fields, | ||||
|         objectMetadataMapItem.fields, | ||||
|         isForwardPagination, | ||||
|       ); | ||||
|  | ||||
| @@ -131,14 +123,14 @@ export class GraphqlQueryFindManyResolverService { | ||||
|  | ||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||
|       queryBuilder, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       appliedFilters, | ||||
|     ); | ||||
|  | ||||
|     const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder( | ||||
|       withFilterQueryBuilder, | ||||
|       orderByWithIdCondition, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       isForwardPagination, | ||||
|     ); | ||||
|  | ||||
| @@ -153,11 +145,11 @@ export class GraphqlQueryFindManyResolverService { | ||||
|  | ||||
|     const objectRecords = formatResult( | ||||
|       nonFormattedObjectRecords, | ||||
|       objectMetadata, | ||||
|       objectMetadataMapItem, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const { hasNextPage, hasPreviousPage } = this.getPaginationInfo( | ||||
|     const { hasNextPage, hasPreviousPage } = getPaginationInfo( | ||||
|       objectRecords, | ||||
|       limit, | ||||
|       isForwardPagination, | ||||
| @@ -167,14 +159,12 @@ export class GraphqlQueryFindManyResolverService { | ||||
|       objectRecords.pop(); | ||||
|     } | ||||
|  | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper( | ||||
|       this.twentyORMGlobalManager, | ||||
|     ); | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||
|  | ||||
|     if (relations) { | ||||
|       await processNestedRelationsHelper.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadata, | ||||
|         objectMetadataMapItem, | ||||
|         objectRecords, | ||||
|         relations, | ||||
|         limit, | ||||
| @@ -184,20 +174,25 @@ export class GraphqlQueryFindManyResolverService { | ||||
|     } | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     return typeORMObjectRecordsParser.createConnection( | ||||
|     const result = typeORMObjectRecordsParser.createConnection( | ||||
|       objectRecords, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       limit, | ||||
|       totalCount, | ||||
|       orderByWithIdCondition, | ||||
|       hasNextPage, | ||||
|       hasPreviousPage, | ||||
|     ); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   private validateArgsOrThrow(args: FindManyResolverArgs<any, any>) { | ||||
|   async validate<Filter extends RecordFilter>( | ||||
|     args: FindManyResolverArgs<Filter>, | ||||
|     _options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     if (args.first && args.last) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'Cannot provide both first and last', | ||||
| @@ -235,49 +230,4 @@ export class GraphqlQueryFindManyResolverService { | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private getCursor( | ||||
|     args: FindManyResolverArgs<any, any>, | ||||
|   ): Record<string, any> | undefined { | ||||
|     if (args.after) return decodeCursor(args.after); | ||||
|     if (args.before) return decodeCursor(args.before); | ||||
|  | ||||
|     return undefined; | ||||
|   } | ||||
|  | ||||
|   private addOrderByColumnsToSelect( | ||||
|     order: Record<string, any>, | ||||
|     select: Record<string, boolean>, | ||||
|   ) { | ||||
|     for (const column of Object.keys(order || {})) { | ||||
|       if (!select[column]) { | ||||
|         select[column] = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private addForeingKeyColumnsToSelect( | ||||
|     relations: Record<string, any>, | ||||
|     select: Record<string, boolean>, | ||||
|     objectMetadata: ObjectMetadataMapItem, | ||||
|   ) { | ||||
|     for (const column of Object.keys(relations || {})) { | ||||
|       if (!select[`${column}Id`] && objectMetadata.fields[`${column}Id`]) { | ||||
|         select[`${column}Id`] = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private getPaginationInfo( | ||||
|     objectRecords: any[], | ||||
|     limit: number, | ||||
|     isForwardPagination: boolean, | ||||
|   ) { | ||||
|     const hasMoreRecords = objectRecords.length > limit; | ||||
|  | ||||
|     return { | ||||
|       hasNextPage: isForwardPagination && hasMoreRecords, | ||||
|       hasPreviousPage: !isForwardPagination && hasMoreRecords, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import graphqlFields from 'graphql-fields'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { | ||||
|   Record as IRecord, | ||||
|   RecordFilter, | ||||
| @@ -13,28 +16,31 @@ import { | ||||
|   GraphqlQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||||
| import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; | ||||
| import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; | ||||
| import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util'; | ||||
| import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { | ||||
|   WorkspaceQueryRunnerException, | ||||
|   WorkspaceQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| export class GraphqlQueryFindOneResolverService { | ||||
|   private twentyORMGlobalManager: TwentyORMGlobalManager; | ||||
| @Injectable() | ||||
| export class GraphqlQueryFindOneResolverService | ||||
|   implements ResolverService<FindOneResolverArgs, IRecord> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { | ||||
|     this.twentyORMGlobalManager = twentyORMGlobalManager; | ||||
|   } | ||||
|  | ||||
|   async findOne< | ||||
|   async resolve< | ||||
|     ObjectRecord extends IRecord = IRecord, | ||||
|     Filter extends RecordFilter = RecordFilter, | ||||
|   >( | ||||
|     args: FindOneResolverArgs<Filter>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord | undefined> { | ||||
|     const { authContext, objectMetadataItem, info, objectMetadataCollection } = | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const { authContext, objectMetadataMapItem, info, objectMetadataMap } = | ||||
|       options; | ||||
|  | ||||
|     const dataSource = | ||||
| @@ -43,37 +49,28 @@ export class GraphqlQueryFindOneResolverService { | ||||
|       ); | ||||
|  | ||||
|     const repository = dataSource.getRepository( | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const queryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const objectMetadataMap = generateObjectMetadataMap( | ||||
|       objectMetadataCollection, | ||||
|     ); | ||||
|  | ||||
|     const objectMetadata = getObjectMetadataOrThrow( | ||||
|       objectMetadataMap, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const graphqlQueryParser = new GraphqlQueryParser( | ||||
|       objectMetadata.fields, | ||||
|       objectMetadataMapItem.fields, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const selectedFields = graphqlFields(info); | ||||
|  | ||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||
|       objectMetadataItem, | ||||
|       objectMetadataMapItem, | ||||
|       selectedFields, | ||||
|     ); | ||||
|  | ||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||
|       queryBuilder, | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       args.filter ?? ({} as Filter), | ||||
|     ); | ||||
|  | ||||
| @@ -86,12 +83,10 @@ export class GraphqlQueryFindOneResolverService { | ||||
|  | ||||
|     const objectRecord = formatResult( | ||||
|       nonFormattedObjectRecord, | ||||
|       objectMetadata, | ||||
|       objectMetadataMapItem, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const limit = QUERY_MAX_RECORDS; | ||||
|  | ||||
|     if (!objectRecord) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'Record not found', | ||||
| @@ -99,32 +94,42 @@ export class GraphqlQueryFindOneResolverService { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper( | ||||
|       this.twentyORMGlobalManager, | ||||
|     ); | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||
|  | ||||
|     const objectRecords = [objectRecord]; | ||||
|  | ||||
|     if (relations) { | ||||
|       await processNestedRelationsHelper.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadata, | ||||
|         objectMetadataMapItem, | ||||
|         objectRecords, | ||||
|         relations, | ||||
|         limit, | ||||
|         QUERY_MAX_RECORDS, | ||||
|         authContext, | ||||
|         dataSource, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     return typeORMObjectRecordsParser.processRecord( | ||||
|       objectRecords[0], | ||||
|       objectMetadataItem.nameSingular, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       1, | ||||
|       1, | ||||
|     ) as ObjectRecord; | ||||
|   } | ||||
|  | ||||
|   async validate<Filter extends RecordFilter>( | ||||
|     args: FindOneResolverArgs<Filter>, | ||||
|     _options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     if (!args.filter || Object.keys(args.filter).length === 0) { | ||||
|       throw new WorkspaceQueryRunnerException( | ||||
|         'Missing filter argument', | ||||
|         WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { | ||||
|   Record as IRecord, | ||||
|   OrderByDirection, | ||||
| @@ -11,47 +14,25 @@ import { | ||||
|   GraphqlQueryRunnerException, | ||||
|   GraphqlQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||||
| import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
| import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; | ||||
| import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
|  | ||||
| export class GraphqlQuerySearchResolverService { | ||||
|   private twentyORMGlobalManager: TwentyORMGlobalManager; | ||||
|   private featureFlagService: FeatureFlagService; | ||||
|  | ||||
| @Injectable() | ||||
| export class GraphqlQuerySearchResolverService | ||||
|   implements ResolverService<SearchResolverArgs, IConnection<IRecord>> | ||||
| { | ||||
|   constructor( | ||||
|     twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|     featureFlagService: FeatureFlagService, | ||||
|   ) { | ||||
|     this.twentyORMGlobalManager = twentyORMGlobalManager; | ||||
|     this.featureFlagService = featureFlagService; | ||||
|   } | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|   ) {} | ||||
|  | ||||
|   async search<ObjectRecord extends IRecord = IRecord>( | ||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: SearchResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<IConnection<ObjectRecord>> { | ||||
|     const { authContext, objectMetadataItem, objectMetadataCollection } = | ||||
|       options; | ||||
|  | ||||
|     const featureFlagsForWorkspace = | ||||
|       await this.featureFlagService.getWorkspaceFeatureFlags( | ||||
|         authContext.workspace.id, | ||||
|       ); | ||||
|  | ||||
|     const isQueryRunnerTwentyORMEnabled = | ||||
|       featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED; | ||||
|  | ||||
|     const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED; | ||||
|  | ||||
|     if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'This endpoint is not available yet, please use findMany instead.', | ||||
|         GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|     const { authContext, objectMetadataItem, objectMetadataMap } = options; | ||||
|  | ||||
|     const repository = | ||||
|       await this.twentyORMGlobalManager.getRepositoryForWorkspace( | ||||
| @@ -59,21 +40,8 @@ export class GraphqlQuerySearchResolverService { | ||||
|         objectMetadataItem.nameSingular, | ||||
|       ); | ||||
|  | ||||
|     const objectMetadataMap = generateObjectMetadataMap( | ||||
|       objectMetadataCollection, | ||||
|     ); | ||||
|  | ||||
|     const objectMetadata = objectMetadataMap[objectMetadataItem.nameSingular]; | ||||
|  | ||||
|     if (!objectMetadata) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         `Object metadata not found for ${objectMetadataItem.nameSingular}`, | ||||
|         GraphqlQueryRunnerExceptionCode.OBJECT_METADATA_NOT_FOUND, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     if (!args.searchInput) { | ||||
|       return typeORMObjectRecordsParser.createConnection( | ||||
| @@ -100,7 +68,7 @@ export class GraphqlQuerySearchResolverService { | ||||
|         'DESC', | ||||
|       ) | ||||
|       .setParameter('searchTerms', searchTerms) | ||||
|       .limit(limit) | ||||
|       .take(limit) | ||||
|       .getMany()) as ObjectRecord[]; | ||||
|  | ||||
|     const objectRecords = await repository.formatResult(resultsWithTsVector); | ||||
| @@ -129,4 +97,26 @@ export class GraphqlQuerySearchResolverService { | ||||
|  | ||||
|     return formattedWords.join(' | '); | ||||
|   } | ||||
|  | ||||
|   async validate( | ||||
|     _args: SearchResolverArgs, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     const featureFlagsForWorkspace = | ||||
|       await this.featureFlagService.getWorkspaceFeatureFlags( | ||||
|         options.authContext.workspace.id, | ||||
|       ); | ||||
|  | ||||
|     const isQueryRunnerTwentyORMEnabled = | ||||
|       featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED; | ||||
|  | ||||
|     const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED; | ||||
|  | ||||
|     if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'This endpoint is not available yet, please use findMany instead.', | ||||
|         GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,116 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import graphqlFields from 'graphql-fields'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { UpdateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; | ||||
| import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; | ||||
| import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; | ||||
| import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| @Injectable() | ||||
| export class GraphqlQueryUpdateManyResolverService | ||||
|   implements ResolverService<UpdateManyResolverArgs, IRecord[]> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord[]> { | ||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap, info } = | ||||
|       options; | ||||
|  | ||||
|     const dataSource = | ||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||
|         authContext.workspace.id, | ||||
|       ); | ||||
|  | ||||
|     const repository = dataSource.getRepository( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const graphqlQueryParser = new GraphqlQueryParser( | ||||
|       objectMetadataMapItem.fields, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const selectedFields = graphqlFields(info); | ||||
|  | ||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||
|       objectMetadataMapItem, | ||||
|       selectedFields, | ||||
|     ); | ||||
|  | ||||
|     const queryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( | ||||
|       queryBuilder, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       args.filter, | ||||
|     ); | ||||
|  | ||||
|     const data = formatData(args.data, objectMetadataMapItem); | ||||
|  | ||||
|     const result = await withFilterQueryBuilder | ||||
|       .update() | ||||
|       .set(data) | ||||
|       .returning('*') | ||||
|       .execute(); | ||||
|  | ||||
|     const nonFormattedUpdatedObjectRecords = result.raw; | ||||
|  | ||||
|     const updatedRecords = formatResult( | ||||
|       nonFormattedUpdatedObjectRecords, | ||||
|       objectMetadataMapItem, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||
|  | ||||
|     if (relations) { | ||||
|       await processNestedRelationsHelper.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadataMapItem, | ||||
|         updatedRecords, | ||||
|         relations, | ||||
|         QUERY_MAX_RECORDS, | ||||
|         authContext, | ||||
|         dataSource, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     return updatedRecords.map((record: ObjectRecord) => | ||||
|       typeORMObjectRecordsParser.processRecord( | ||||
|         record, | ||||
|         objectMetadataMapItem.nameSingular, | ||||
|         1, | ||||
|         1, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   async validate<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: UpdateManyResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     assertMutationNotOnRemoteObject(options.objectMetadataMapItem); | ||||
|     args.filter?.id?.in?.forEach((id: string) => assertIsValidUuid(id)); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,123 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import graphqlFields from 'graphql-fields'; | ||||
|  | ||||
| import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; | ||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; | ||||
| import { | ||||
|   GraphqlQueryRunnerException, | ||||
|   GraphqlQueryRunnerExceptionCode, | ||||
| } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; | ||||
| import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; | ||||
| import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; | ||||
| import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; | ||||
| import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; | ||||
| import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; | ||||
| import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; | ||||
| import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; | ||||
| import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; | ||||
|  | ||||
| @Injectable() | ||||
| export class GraphqlQueryUpdateOneResolverService | ||||
|   implements ResolverService<UpdateOneResolverArgs, IRecord> | ||||
| { | ||||
|   constructor( | ||||
|     private readonly twentyORMGlobalManager: TwentyORMGlobalManager, | ||||
|   ) {} | ||||
|  | ||||
|   async resolve<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: UpdateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<ObjectRecord> { | ||||
|     const { authContext, objectMetadataMapItem, objectMetadataMap, info } = | ||||
|       options; | ||||
|  | ||||
|     const dataSource = | ||||
|       await this.twentyORMGlobalManager.getDataSourceForWorkspace( | ||||
|         authContext.workspace.id, | ||||
|       ); | ||||
|  | ||||
|     const repository = dataSource.getRepository( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const graphqlQueryParser = new GraphqlQueryParser( | ||||
|       objectMetadataMapItem.fields, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     const selectedFields = graphqlFields(info); | ||||
|  | ||||
|     const { relations } = graphqlQueryParser.parseSelectedFields( | ||||
|       objectMetadataMapItem, | ||||
|       selectedFields, | ||||
|     ); | ||||
|  | ||||
|     const queryBuilder = repository.createQueryBuilder( | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|     ); | ||||
|  | ||||
|     const withFilterQueryBuilder = queryBuilder.where({ id: args.id }); | ||||
|  | ||||
|     const data = formatData(args.data, objectMetadataMapItem); | ||||
|  | ||||
|     const result = await withFilterQueryBuilder | ||||
|       .update() | ||||
|       .set(data) | ||||
|       .returning('*') | ||||
|       .execute(); | ||||
|  | ||||
|     const nonFormattedUpdatedObjectRecords = result.raw; | ||||
|  | ||||
|     const updatedRecords = formatResult( | ||||
|       nonFormattedUpdatedObjectRecords, | ||||
|       objectMetadataMapItem, | ||||
|       objectMetadataMap, | ||||
|     ); | ||||
|  | ||||
|     if (updatedRecords.length === 0) { | ||||
|       throw new GraphqlQueryRunnerException( | ||||
|         'Record not found', | ||||
|         GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const updatedRecord = updatedRecords[0] as ObjectRecord; | ||||
|  | ||||
|     const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); | ||||
|  | ||||
|     if (relations) { | ||||
|       await processNestedRelationsHelper.processNestedRelations( | ||||
|         objectMetadataMap, | ||||
|         objectMetadataMapItem, | ||||
|         [updatedRecord], | ||||
|         relations, | ||||
|         QUERY_MAX_RECORDS, | ||||
|         authContext, | ||||
|         dataSource, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     const typeORMObjectRecordsParser = | ||||
|       new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); | ||||
|  | ||||
|     return typeORMObjectRecordsParser.processRecord<ObjectRecord>( | ||||
|       updatedRecord, | ||||
|       objectMetadataMapItem.nameSingular, | ||||
|       1, | ||||
|       1, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   async validate<ObjectRecord extends IRecord = IRecord>( | ||||
|     args: UpdateOneResolverArgs<Partial<ObjectRecord>>, | ||||
|     options: WorkspaceQueryRunnerOptions, | ||||
|   ): Promise<void> { | ||||
|     assertMutationNotOnRemoteObject(options.objectMetadataMapItem); | ||||
|     assertIsValidUuid(args.id); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,137 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
|  | ||||
| import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||
| import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; | ||||
|  | ||||
| @Injectable() | ||||
| export class ApiEventEmitterService { | ||||
|   constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {} | ||||
|  | ||||
|   public emitCreateEvents<T extends IRecord>( | ||||
|     records: T[], | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataItem: ObjectMetadataInterface, | ||||
|   ): void { | ||||
|     this.workspaceEventEmitter.emit( | ||||
|       `${objectMetadataItem.nameSingular}.created`, | ||||
|       records.map((record) => ({ | ||||
|         userId: authContext.user?.id, | ||||
|         recordId: record.id, | ||||
|         objectMetadata: objectMetadataItem, | ||||
|         properties: { | ||||
|           before: null, | ||||
|           after: this.removeGraphQLAndNestedProperties(record), | ||||
|         }, | ||||
|       })), | ||||
|       authContext.workspace.id, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   public emitUpdateEvents<T extends IRecord>( | ||||
|     existingRecords: T[], | ||||
|     records: T[], | ||||
|     updatedFields: string[], | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataItem: ObjectMetadataInterface, | ||||
|   ): void { | ||||
|     const mappedExistingRecords = existingRecords.reduce( | ||||
|       (acc, { id, ...record }) => ({ | ||||
|         ...acc, | ||||
|         [id]: record, | ||||
|       }), | ||||
|       {}, | ||||
|     ); | ||||
|  | ||||
|     this.workspaceEventEmitter.emit( | ||||
|       `${objectMetadataItem.nameSingular}.updated`, | ||||
|       records.map((record) => { | ||||
|         return { | ||||
|           userId: authContext.user?.id, | ||||
|           recordId: record.id, | ||||
|           objectMetadata: objectMetadataItem, | ||||
|           properties: { | ||||
|             before: mappedExistingRecords[record.id] | ||||
|               ? this.removeGraphQLAndNestedProperties( | ||||
|                   mappedExistingRecords[record.id], | ||||
|                 ) | ||||
|               : undefined, | ||||
|             after: this.removeGraphQLAndNestedProperties(record), | ||||
|             updatedFields, | ||||
|           }, | ||||
|         }; | ||||
|       }), | ||||
|       authContext.workspace.id, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   public emitDeletedEvents<T extends IRecord>( | ||||
|     records: T[], | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataItem: ObjectMetadataInterface, | ||||
|   ): void { | ||||
|     this.workspaceEventEmitter.emit( | ||||
|       `${objectMetadataItem.nameSingular}.deleted`, | ||||
|       records.map((record) => { | ||||
|         return { | ||||
|           userId: authContext.user?.id, | ||||
|           recordId: record.id, | ||||
|           objectMetadata: objectMetadataItem, | ||||
|           properties: { | ||||
|             before: this.removeGraphQLAndNestedProperties(record), | ||||
|             after: null, | ||||
|           }, | ||||
|         }; | ||||
|       }), | ||||
|       authContext.workspace.id, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   public emitDestroyEvents<T extends IRecord>( | ||||
|     records: T[], | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataItem: ObjectMetadataInterface, | ||||
|   ): void { | ||||
|     this.workspaceEventEmitter.emit( | ||||
|       `${objectMetadataItem.nameSingular}.destroyed`, | ||||
|       records.map((record) => { | ||||
|         return { | ||||
|           userId: authContext.user?.id, | ||||
|           recordId: record.id, | ||||
|           objectMetadata: objectMetadataItem, | ||||
|           properties: { | ||||
|             before: this.removeGraphQLAndNestedProperties(record), | ||||
|             after: null, | ||||
|           }, | ||||
|         }; | ||||
|       }), | ||||
|       authContext.workspace.id, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private removeGraphQLAndNestedProperties<ObjectRecord extends IRecord>( | ||||
|     record: ObjectRecord, | ||||
|   ) { | ||||
|     if (!record) { | ||||
|       return {}; | ||||
|     } | ||||
|  | ||||
|     const sanitizedRecord = {}; | ||||
|  | ||||
|     for (const [key, value] of Object.entries(record)) { | ||||
|       if (value && typeof value === 'object' && value['edges']) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       if (key === '__typename') { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       sanitizedRecord[key] = value; | ||||
|     } | ||||
|  | ||||
|     return sanitizedRecord; | ||||
|   } | ||||
| } | ||||
| @@ -2,6 +2,7 @@ import { | ||||
|   Record as IRecord, | ||||
|   RecordOrderBy, | ||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
|  | ||||
| import { | ||||
|   GraphqlQueryRunnerException, | ||||
| @@ -44,3 +45,25 @@ export const encodeCursor = <ObjectRecord extends IRecord = IRecord>( | ||||
|  | ||||
|   return Buffer.from(JSON.stringify(cursorData)).toString('base64'); | ||||
| }; | ||||
|  | ||||
| export const getCursor = ( | ||||
|   args: FindManyResolverArgs<any, any>, | ||||
| ): Record<string, any> | undefined => { | ||||
|   if (args.after) return decodeCursor(args.after); | ||||
|   if (args.before) return decodeCursor(args.before); | ||||
|  | ||||
|   return undefined; | ||||
| }; | ||||
|  | ||||
| export const getPaginationInfo = ( | ||||
|   objectRecords: any[], | ||||
|   limit: number, | ||||
|   isForwardPagination: boolean, | ||||
| ) => { | ||||
|   const hasMoreRecords = objectRecords.length > limit; | ||||
|  | ||||
|   return { | ||||
|     hasNextPage: isForwardPagination && hasMoreRecords, | ||||
|     hasPreviousPage: !isForwardPagination && hasMoreRecords, | ||||
|   }; | ||||
| }; | ||||
|   | ||||
| @@ -4,6 +4,10 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
|  | ||||
| import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||
| import { | ||||
|   ObjectMetadataMap, | ||||
|   ObjectMetadataMapItem, | ||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
|  | ||||
| export interface WorkspaceQueryRunnerOptions { | ||||
|   authContext: AuthContext; | ||||
| @@ -11,4 +15,6 @@ export interface WorkspaceQueryRunnerOptions { | ||||
|   objectMetadataItem: ObjectMetadataInterface; | ||||
|   fieldMetadataCollection: FieldMetadataInterface[]; | ||||
|   objectMetadataCollection: ObjectMetadataInterface[]; | ||||
|   objectMetadataMap: ObjectMetadataMap; | ||||
|   objectMetadataMapItem: ObjectMetadataMapItem; | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { | ||||
|   FindManyResolverArgs, | ||||
|   FindOneResolverArgs, | ||||
|   RestoreManyResolverArgs, | ||||
|   SearchResolverArgs, | ||||
|   UpdateManyResolverArgs, | ||||
|   UpdateOneResolverArgs, | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| @@ -42,4 +43,6 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany' | ||||
|                       ? DestroyManyResolverArgs | ||||
|                       : T extends 'destroyOne' | ||||
|                         ? DestroyOneResolverArgs | ||||
|                         : never; | ||||
|                         : T extends 'search' | ||||
|                           ? SearchResolverArgs | ||||
|                           : never; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   CreateManyResolverArgs, | ||||
| @@ -32,12 +33,14 @@ export class CreateManyResolverFactory | ||||
|  | ||||
|     return async (_source, args, _context, info) => { | ||||
|       try { | ||||
|         const options = { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   CreateOneResolverArgs, | ||||
| @@ -32,12 +33,14 @@ export class CreateOneResolverFactory | ||||
|  | ||||
|     return async (_source, args, _context, info) => { | ||||
|       try { | ||||
|         const options = { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
| @@ -50,13 +53,7 @@ export class CreateOneResolverFactory | ||||
|           return await this.graphqlQueryRunnerService.createOne(args, options); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.createOne(args, { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|         return await this.workspaceQueryRunnerService.createOne(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   DeleteManyResolverArgs, | ||||
| @@ -7,8 +8,11 @@ import { | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; | ||||
|  | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; | ||||
| import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; | ||||
| import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class DeleteManyResolverFactory | ||||
| @@ -18,6 +22,8 @@ export class DeleteManyResolverFactory | ||||
|  | ||||
|   constructor( | ||||
|     private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, | ||||
|   ) {} | ||||
|  | ||||
|   create( | ||||
| @@ -27,13 +33,27 @@ export class DeleteManyResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.deleteMany(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|           await this.featureFlagService.isFeatureEnabled( | ||||
|             FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, | ||||
|             internalContext.authContext.workspace.id, | ||||
|           ); | ||||
|  | ||||
|         if (isQueryRunnerTwentyORMEnabled) { | ||||
|           return await this.graphqlQueryRunnerService.deleteMany(args, options); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.deleteMany(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   DeleteOneResolverArgs, | ||||
| @@ -7,8 +8,11 @@ import { | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; | ||||
|  | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; | ||||
| import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; | ||||
| import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class DeleteOneResolverFactory | ||||
| @@ -18,6 +22,8 @@ export class DeleteOneResolverFactory | ||||
|  | ||||
|   constructor( | ||||
|     private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, | ||||
|   ) {} | ||||
|  | ||||
|   create( | ||||
| @@ -27,13 +33,27 @@ export class DeleteOneResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.deleteOne(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|           await this.featureFlagService.isFeatureEnabled( | ||||
|             FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, | ||||
|             internalContext.authContext.workspace.id, | ||||
|           ); | ||||
|  | ||||
|         if (isQueryRunnerTwentyORMEnabled) { | ||||
|           return await this.graphqlQueryRunnerService.deleteOne(args, options); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.deleteOne(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   DestroyManyResolverArgs, | ||||
| @@ -27,13 +28,20 @@ export class DestroyManyResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.destroyMany(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.destroyMany( | ||||
|           args, | ||||
|           options, | ||||
|         ); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   DestroyOneResolverArgs, | ||||
| @@ -27,13 +28,17 @@ export class DestroyOneResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.graphQLQueryRunnerService.destroyOne(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         return await this.graphQLQueryRunnerService.destroyOne(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   FindDuplicatesResolverArgs, | ||||
| @@ -7,8 +8,11 @@ import { | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; | ||||
|  | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; | ||||
| import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; | ||||
| import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class FindDuplicatesResolverFactory | ||||
| @@ -18,6 +22,8 @@ export class FindDuplicatesResolverFactory | ||||
|  | ||||
|   constructor( | ||||
|     private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, | ||||
|   ) {} | ||||
|  | ||||
|   create( | ||||
| @@ -27,13 +33,33 @@ export class FindDuplicatesResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.findDuplicates(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|           await this.featureFlagService.isFeatureEnabled( | ||||
|             FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, | ||||
|             internalContext.authContext.workspace.id, | ||||
|           ); | ||||
|  | ||||
|         if (isQueryRunnerTwentyORMEnabled) { | ||||
|           return await this.graphqlQueryRunnerService.findDuplicates( | ||||
|             args, | ||||
|             options, | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.findDuplicates( | ||||
|           args, | ||||
|           options, | ||||
|         ); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   FindManyResolverArgs, | ||||
| @@ -27,12 +28,14 @@ export class FindManyResolverFactory | ||||
|  | ||||
|     return async (_source, args, _context, info) => { | ||||
|       try { | ||||
|         const options = { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         return await this.graphqlQueryRunnerService.findMany(args, options); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   FindOneResolverArgs, | ||||
| @@ -27,12 +28,14 @@ export class FindOneResolverFactory | ||||
|  | ||||
|     return async (_source, args, _context, info) => { | ||||
|       try { | ||||
|         const options = { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         return await this.graphqlQueryRunnerService.findOne(args, options); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   Resolver, | ||||
| @@ -7,8 +8,11 @@ import { | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; | ||||
|  | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; | ||||
| import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; | ||||
| import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class RestoreManyResolverFactory | ||||
| @@ -18,6 +22,8 @@ export class RestoreManyResolverFactory | ||||
|  | ||||
|   constructor( | ||||
|     private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, | ||||
|   ) {} | ||||
|  | ||||
|   create( | ||||
| @@ -27,13 +33,33 @@ export class RestoreManyResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.restoreMany(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|           await this.featureFlagService.isFeatureEnabled( | ||||
|             FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, | ||||
|             internalContext.authContext.workspace.id, | ||||
|           ); | ||||
|  | ||||
|         if (isQueryRunnerTwentyORMEnabled) { | ||||
|           return await this.graphqlQueryRunnerService.restoreMany( | ||||
|             args, | ||||
|             options, | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.restoreMany( | ||||
|           args, | ||||
|           options, | ||||
|         ); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   Resolver, | ||||
| @@ -25,13 +26,17 @@ export class SearchResolverFactory | ||||
|  | ||||
|     return async (_source, args, _context, info) => { | ||||
|       try { | ||||
|         return await this.graphqlQueryRunnerService.search(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         return await this.graphqlQueryRunnerService.search(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   Resolver, | ||||
| @@ -7,8 +8,11 @@ import { | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; | ||||
|  | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; | ||||
| import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; | ||||
| import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class UpdateManyResolverFactory | ||||
| @@ -18,6 +22,8 @@ export class UpdateManyResolverFactory | ||||
|  | ||||
|   constructor( | ||||
|     private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, | ||||
|   ) {} | ||||
|  | ||||
|   create( | ||||
| @@ -27,13 +33,27 @@ export class UpdateManyResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.updateMany(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|           await this.featureFlagService.isFeatureEnabled( | ||||
|             FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, | ||||
|             internalContext.authContext.workspace.id, | ||||
|           ); | ||||
|  | ||||
|         if (isQueryRunnerTwentyORMEnabled) { | ||||
|           return await this.graphqlQueryRunnerService.updateMany(args, options); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.updateMany(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; | ||||
| import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; | ||||
| import { | ||||
|   Resolver, | ||||
| @@ -7,8 +8,11 @@ import { | ||||
| } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; | ||||
| import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; | ||||
|  | ||||
| import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; | ||||
| import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; | ||||
| import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; | ||||
| import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; | ||||
| import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; | ||||
|  | ||||
| @Injectable() | ||||
| export class UpdateOneResolverFactory | ||||
| @@ -18,6 +22,8 @@ export class UpdateOneResolverFactory | ||||
|  | ||||
|   constructor( | ||||
|     private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, | ||||
|     private readonly featureFlagService: FeatureFlagService, | ||||
|     private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, | ||||
|   ) {} | ||||
|  | ||||
|   create( | ||||
| @@ -27,13 +33,27 @@ export class UpdateOneResolverFactory | ||||
|  | ||||
|     return async (_source, args, context, info) => { | ||||
|       try { | ||||
|         return await this.workspaceQueryRunnerService.updateOne(args, { | ||||
|         const options: WorkspaceQueryRunnerOptions = { | ||||
|           authContext: internalContext.authContext, | ||||
|           objectMetadataItem: internalContext.objectMetadataItem, | ||||
|           info, | ||||
|           fieldMetadataCollection: internalContext.fieldMetadataCollection, | ||||
|           objectMetadataCollection: internalContext.objectMetadataCollection, | ||||
|         }); | ||||
|           objectMetadataMap: internalContext.objectMetadataMap, | ||||
|           objectMetadataMapItem: internalContext.objectMetadataMapItem, | ||||
|         }; | ||||
|  | ||||
|         const isQueryRunnerTwentyORMEnabled = | ||||
|           await this.featureFlagService.isFeatureEnabled( | ||||
|             FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, | ||||
|             internalContext.authContext.workspace.id, | ||||
|           ); | ||||
|  | ||||
|         if (isQueryRunnerTwentyORMEnabled) { | ||||
|           return await this.graphqlQueryRunnerService.updateOne(args, options); | ||||
|         } | ||||
|  | ||||
|         return await this.workspaceQueryRunnerService.updateOne(args, options); | ||||
|       } catch (error) { | ||||
|         workspaceQueryRunnerGraphqlApiExceptionHandler(error); | ||||
|       } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-res | ||||
| import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory'; | ||||
| import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; | ||||
| import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||
| import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; | ||||
|  | ||||
| import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; | ||||
| @@ -49,6 +50,7 @@ export class WorkspaceResolverFactory { | ||||
|   async create( | ||||
|     authContext: AuthContext, | ||||
|     objectMetadataCollection: ObjectMetadataInterface[], | ||||
|     objectMetadataMap: ObjectMetadataMap, | ||||
|     workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, | ||||
|   ): Promise<IResolvers> { | ||||
|     const factories = new Map< | ||||
| @@ -94,7 +96,9 @@ export class WorkspaceResolverFactory { | ||||
|           authContext, | ||||
|           objectMetadataItem: objectMetadata, | ||||
|           fieldMetadataCollection: objectMetadata.fields, | ||||
|           objectMetadataCollection: objectMetadataCollection, | ||||
|           objectMetadataCollection, | ||||
|           objectMetadataMap, | ||||
|           objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], | ||||
|         }); | ||||
|       } | ||||
|  | ||||
| @@ -117,7 +121,9 @@ export class WorkspaceResolverFactory { | ||||
|           authContext, | ||||
|           objectMetadataItem: objectMetadata, | ||||
|           fieldMetadataCollection: objectMetadata.fields, | ||||
|           objectMetadataCollection: objectMetadataCollection, | ||||
|           objectMetadataCollection, | ||||
|           objectMetadataMap, | ||||
|           objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -2,10 +2,16 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
|  | ||||
| import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; | ||||
| import { | ||||
|   ObjectMetadataMap, | ||||
|   ObjectMetadataMapItem, | ||||
| } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
|  | ||||
| export interface WorkspaceSchemaBuilderContext { | ||||
|   authContext: AuthContext; | ||||
|   objectMetadataItem: ObjectMetadataInterface; | ||||
|   fieldMetadataCollection: FieldMetadataInterface[]; | ||||
|   objectMetadataCollection: ObjectMetadataInterface[]; | ||||
|   objectMetadataItem: ObjectMetadataInterface; | ||||
|   objectMetadataMap: ObjectMetadataMap; | ||||
|   objectMetadataMapItem: ObjectMetadataMapItem; | ||||
| } | ||||
|   | ||||
| @@ -117,6 +117,7 @@ export class WorkspaceSchemaFactory { | ||||
|     const autoGeneratedResolvers = await this.workspaceResolverFactory.create( | ||||
|       authContext, | ||||
|       objectMetadataCollection, | ||||
|       objectMetadataMap, | ||||
|       workspaceResolverBuilderMethodNames, | ||||
|     ); | ||||
|     const scalarsResolvers = | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
| import { | ||||
|   Record as IRecord, | ||||
|   Record, | ||||
| } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; | ||||
| import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; | ||||
|  | ||||
| import { settings } from 'src/engine/constants/settings'; | ||||
| import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; | ||||
| import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; | ||||
| import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; | ||||
| import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; | ||||
|  | ||||
| @Injectable() | ||||
| export class DuplicateService { | ||||
| @@ -94,80 +94,4 @@ export class DuplicateService { | ||||
|         duplicateCriteria.objectName === objectMetadataItem.nameSingular, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * TODO: Remove this code by September 1st, 2024 if it isn't used | ||||
|    * It was build to be used by the upsertMany function, but it was not used. | ||||
|    * It's a re-implementation of the methods to findDuplicates, but done | ||||
|    * at the SQL layer instead of doing it at the GraphQL layer | ||||
|    *  | ||||
|   async findDuplicate( | ||||
|     data: Partial<Record>, | ||||
|     objectMetadata: ObjectMetadataInterface, | ||||
|     workspaceId: string, | ||||
|   ) { | ||||
|     const dataSourceSchema = | ||||
|       this.workspaceDataSourceService.getSchemaName(workspaceId); | ||||
|  | ||||
|     const { duplicateWhereClause, duplicateWhereParameters } = | ||||
|       this.buildDuplicateConditionForUpsert(objectMetadata, data); | ||||
|  | ||||
|     const results = await this.workspaceDataSourceService.executeRawQuery( | ||||
|       ` | ||||
|           SELECT  | ||||
|              * | ||||
|           FROM | ||||
|               ${dataSourceSchema}."${computeObjectTargetTable( | ||||
|                 objectMetadata, | ||||
|               )}" p | ||||
|           WHERE | ||||
|               ${duplicateWhereClause} | ||||
|           `, | ||||
|       duplicateWhereParameters, | ||||
|       workspaceId, | ||||
|     ); | ||||
|  | ||||
|     return results.length > 0 ? results[0] : null; | ||||
|   } | ||||
|  | ||||
|   private buildDuplicateConditionForUpsert( | ||||
|     objectMetadata: ObjectMetadataInterface, | ||||
|     data: Partial<Record>, | ||||
|   ) { | ||||
|     const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( | ||||
|       objectMetadata, | ||||
|     ).filter( | ||||
|       (duplicateCriteria) => duplicateCriteria.useAsUniqueKeyForUpsert === true, | ||||
|     ); | ||||
|  | ||||
|     const whereClauses: string[] = []; | ||||
|     const whereParameters: any[] = []; | ||||
|     let parameterIndex = 1; | ||||
|  | ||||
|     criteriaCollection.forEach((c) => { | ||||
|       const clauseParts: string[] = []; | ||||
|  | ||||
|       c.columnNames.forEach((column) => { | ||||
|         const dataKey = Object.keys(data).find( | ||||
|           (key) => key.toLowerCase() === column.toLowerCase(), | ||||
|         ); | ||||
|  | ||||
|         if (dataKey) { | ||||
|           clauseParts.push(`p."${column}" = $${parameterIndex}`); | ||||
|           whereParameters.push(data[dataKey]); | ||||
|           parameterIndex++; | ||||
|         } | ||||
|       }); | ||||
|       if (clauseParts.length > 0) { | ||||
|         whereClauses.push(`(${clauseParts.join(' AND ')})`); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     const duplicateWhereClause = whereClauses.join(' OR '); | ||||
|     const duplicateWhereParameters = whereParameters; | ||||
|  | ||||
|     return { duplicateWhereClause, duplicateWhereParameters }; | ||||
|   } | ||||
|   * | ||||
|   */ | ||||
| } | ||||
|   | ||||
| @@ -60,8 +60,8 @@ export class IndexMetadataEntity { | ||||
|   @Column({ | ||||
|     type: 'enum', | ||||
|     enum: IndexType, | ||||
|     nullable: true, | ||||
|     default: IndexType.BTREE, | ||||
|     nullable: false, | ||||
|   }) | ||||
|   indexType?: IndexType; | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import { isPlainObject } from '@nestjs/common/utils/shared.utils'; | ||||
| import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | ||||
|  | ||||
| import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | ||||
| import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; | ||||
| import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; | ||||
| import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; | ||||
| import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; | ||||
| import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; | ||||
| import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; | ||||
| import { capitalize } from 'src/utils/capitalize'; | ||||
|  | ||||
| export function formatData<T>( | ||||
|   data: T, | ||||
| @@ -17,49 +19,70 @@ export function formatData<T>( | ||||
|     return data.map((item) => formatData(item, objectMetadata)) as T; | ||||
|   } | ||||
|  | ||||
|   const compositeFieldMetadataCollection = | ||||
|     getCompositeFieldMetadataCollection(objectMetadata); | ||||
|  | ||||
|   const compositeFieldMetadataMap = new Map( | ||||
|     compositeFieldMetadataCollection.map((fieldMetadata) => [ | ||||
|       fieldMetadata.name, | ||||
|       fieldMetadata, | ||||
|     ]), | ||||
|   ); | ||||
|   const newData: object = {}; | ||||
|   const newData: Record<string, any> = {}; | ||||
|  | ||||
|   for (const [key, value] of Object.entries(data)) { | ||||
|     const fieldMetadata = compositeFieldMetadataMap.get(key); | ||||
|     const fieldMetadata = objectMetadata.fields[key]; | ||||
|  | ||||
|     if (!fieldMetadata) { | ||||
|       if (isPlainObject(value)) { | ||||
|         newData[key] = formatData(value, objectMetadata); | ||||
|       } else { | ||||
|         newData[key] = value; | ||||
|       } | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); | ||||
|  | ||||
|     if (!compositeType) { | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     for (const compositeProperty of compositeType.properties) { | ||||
|       const compositeKey = computeCompositeColumnName( | ||||
|         fieldMetadata.name, | ||||
|         compositeProperty, | ||||
|       throw new Error( | ||||
|         `Field metadata for field "${key}" is missing in object metadata`, | ||||
|       ); | ||||
|       const value = data?.[key]?.[compositeProperty.name]; | ||||
|     } | ||||
|  | ||||
|       if (value === undefined || value === null) { | ||||
|         continue; | ||||
|       } | ||||
|     if (isCompositeFieldMetadataType(fieldMetadata.type)) { | ||||
|       const formattedCompositeField = formatCompositeField( | ||||
|         value, | ||||
|         fieldMetadata, | ||||
|       ); | ||||
|  | ||||
|       newData[compositeKey] = data[key][compositeProperty.name]; | ||||
|       Object.assign(newData, formattedCompositeField); | ||||
|     } else { | ||||
|       newData[key] = formatFieldMetadataValue(value, fieldMetadata); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return newData as T; | ||||
| } | ||||
|  | ||||
| function formatCompositeField( | ||||
|   value: any, | ||||
|   fieldMetadata: FieldMetadataInterface, | ||||
| ): Record<string, any> { | ||||
|   const compositeType = compositeTypeDefinitions.get( | ||||
|     fieldMetadata.type as CompositeFieldMetadataType, | ||||
|   ); | ||||
|  | ||||
|   if (!compositeType) { | ||||
|     throw new Error( | ||||
|       `Composite type definition not found for type: ${fieldMetadata.type}`, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   const formattedCompositeField: Record<string, any> = {}; | ||||
|  | ||||
|   for (const property of compositeType.properties) { | ||||
|     const subFieldKey = property.name; | ||||
|     const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`; | ||||
|  | ||||
|     if (value && value[subFieldKey] !== undefined) { | ||||
|       formattedCompositeField[fullFieldName] = formatFieldMetadataValue( | ||||
|         value[subFieldKey], | ||||
|         property as unknown as FieldMetadataInterface, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return formattedCompositeField; | ||||
| } | ||||
|  | ||||
| function formatFieldMetadataValue( | ||||
|   value: any, | ||||
|   fieldMetadata: FieldMetadataInterface, | ||||
| ) { | ||||
|   if (fieldMetadata.type === FieldMetadataType.RAW_JSON) { | ||||
|     return JSON.parse(value as string); | ||||
|   } | ||||
|  | ||||
|   return value; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import { isPlainObject } from '@nestjs/common/utils/shared.utils'; | ||||
|  | ||||
| import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; | ||||
|  | ||||
| import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; | ||||
| import { FieldMetadataType } 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 { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; | ||||
| import { | ||||
| @@ -81,9 +84,15 @@ export function formatResult<T>( | ||||
|     if (!compositePropertyArgs && !relationMetadata) { | ||||
|       if (isPlainObject(value)) { | ||||
|         newData[key] = formatResult(value, objectMetadata, objectMetadataMap); | ||||
|       } else if (objectMetadata.fields[key]) { | ||||
|         newData[key] = formatFieldMetadataValue( | ||||
|           value, | ||||
|           objectMetadata.fields[key], | ||||
|         ); | ||||
|       } else { | ||||
|         newData[key] = value; | ||||
|       } | ||||
|  | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
| @@ -129,3 +138,18 @@ export function formatResult<T>( | ||||
|  | ||||
|   return newData as T; | ||||
| } | ||||
|  | ||||
| function formatFieldMetadataValue( | ||||
|   value: any, | ||||
|   fieldMetadata: FieldMetadataInterface, | ||||
| ) { | ||||
|   if ( | ||||
|     typeof value === 'string' && | ||||
|     (fieldMetadata.type === FieldMetadataType.MULTI_SELECT || | ||||
|       fieldMetadata.type === FieldMetadataType.ARRAY) | ||||
|   ) { | ||||
|     return value.replace(/{|}/g, '').split(','); | ||||
|   } | ||||
|  | ||||
|   return value; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Weiko
					Weiko