import { Injectable } from '@nestjs/common'; import { type MessageDescriptor } from '@lingui/core'; import { type Request } from 'express'; import { GraphQLError, buildSchema, execute, type DocumentNode, type FieldNode, type GraphQLFormattedError, type GraphQLResolveInfo, } from 'graphql'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import graphqlFields from 'graphql-fields'; import { STANDARD_ERROR_MESSAGE } from 'src/engine/api/common/common-query-runners/errors/standard-error-message.constant'; import { GraphqlDirectExecutionException, GraphqlDirectExecutionExceptionCode, } from 'src/engine/api/graphql/direct-execution/errors/graphql-direct-execution.exception'; import { assertCreateManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-create-many-args.util'; import { assertCreateOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-create-one-args.util'; import { assertDeleteManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-delete-many-args.util'; import { assertDeleteOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-delete-one-args.util'; import { assertDestroyManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-destroy-many-args.util'; import { assertDestroyOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-destroy-one-args.util'; import { assertFindDuplicatesArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-find-duplicates-args.util'; import { assertFindManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-find-many-args.util'; import { assertFindOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-find-one-args.util'; import { assertGroupByArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-group-by-args.util'; import { assertMergeManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-merge-many-args.util'; import { assertRestoreManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-restore-many-args.util'; import { assertRestoreOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-restore-one-args.util'; import { assertUpdateManyArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-update-many-args.util'; import { assertUpdateOneArgs } from 'src/engine/api/graphql/direct-execution/utils/assert-update-one-args.util'; import { type ResolverNameMapEntry } from 'src/engine/api/graphql/direct-execution/utils/build-resolver-name-map.util'; import { buildWorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/direct-execution/utils/build-workspace-schema-builder-context.util'; import { extractArgumentsFromAst } from 'src/engine/api/graphql/direct-execution/utils/extract-arguments-from-ast.util'; import { graphQLBuildFragmentMap } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-fragment-map.util'; import { graphQLBuildPartialResolveInfo } from 'src/engine/api/graphql/direct-execution/utils/graphql-build-partial-resolve-info.util'; import { graphQLExtractTopLevelFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-extract-top-level-fields.util'; import { graphQLFormatResultFromSelectedFields } from 'src/engine/api/graphql/direct-execution/utils/graphql-format-result-from-selected-fields.util'; import { WorkspaceGraphqlSchemaSDLService } from 'src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service'; import { ResolverOutput } from 'src/engine/api/graphql/workspace-query-runner/interfaces/resolver-output'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { RESOLVER_METHOD_NAMES } from 'src/engine/api/graphql/workspace-resolver-builder/constants/resolver-method-names'; import { CreateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory'; import { CreateOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory'; import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory'; import { DeleteOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory'; import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory'; import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory'; import { FindDuplicatesResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory'; import { FindManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory'; import { FindOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory'; import { GroupByResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/group-by-resolver.factory'; import { MergeManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/merge-many-resolver.factory'; import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory'; import { RestoreOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-one-resolver.factory'; import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; import { UpdateOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory'; import { type WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { type WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; type DirectExecutionResult = { data?: Record; errors?: GraphQLFormattedError[]; }; @Injectable() export class DirectExecutionService { private readonly factoryMap: Map< string, WorkspaceResolverBuilderFactoryInterface >; private readonly argsAssertionMap: Map void>; constructor( private readonly workspaceFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceCacheService: WorkspaceCacheService, private readonly workspaceGraphqlSchemaSDLService: WorkspaceGraphqlSchemaSDLService, private readonly twentyConfigService: TwentyConfigService, private readonly i18nService: I18nService, private readonly metricsService: MetricsService, private readonly findManyResolverFactory: FindManyResolverFactory, private readonly findOneResolverFactory: FindOneResolverFactory, private readonly findDuplicatesResolverFactory: FindDuplicatesResolverFactory, private readonly groupByResolverFactory: GroupByResolverFactory, private readonly createOneResolverFactory: CreateOneResolverFactory, private readonly createManyResolverFactory: CreateManyResolverFactory, private readonly updateOneResolverFactory: UpdateOneResolverFactory, private readonly updateManyResolverFactory: UpdateManyResolverFactory, private readonly deleteOneResolverFactory: DeleteOneResolverFactory, private readonly deleteManyResolverFactory: DeleteManyResolverFactory, private readonly destroyOneResolverFactory: DestroyOneResolverFactory, private readonly destroyManyResolverFactory: DestroyManyResolverFactory, private readonly restoreOneResolverFactory: RestoreOneResolverFactory, private readonly restoreManyResolverFactory: RestoreManyResolverFactory, private readonly mergeManyResolverFactory: MergeManyResolverFactory, ) { this.factoryMap = new Map( [ [RESOLVER_METHOD_NAMES.FIND_MANY, this.findManyResolverFactory], [RESOLVER_METHOD_NAMES.FIND_ONE, this.findOneResolverFactory], [ RESOLVER_METHOD_NAMES.FIND_DUPLICATES, this.findDuplicatesResolverFactory, ], [RESOLVER_METHOD_NAMES.GROUP_BY, this.groupByResolverFactory], [RESOLVER_METHOD_NAMES.CREATE_ONE, this.createOneResolverFactory], [RESOLVER_METHOD_NAMES.CREATE_MANY, this.createManyResolverFactory], [RESOLVER_METHOD_NAMES.UPDATE_ONE, this.updateOneResolverFactory], [RESOLVER_METHOD_NAMES.UPDATE_MANY, this.updateManyResolverFactory], [RESOLVER_METHOD_NAMES.DELETE_ONE, this.deleteOneResolverFactory], [RESOLVER_METHOD_NAMES.DELETE_MANY, this.deleteManyResolverFactory], [RESOLVER_METHOD_NAMES.DESTROY_ONE, this.destroyOneResolverFactory], [RESOLVER_METHOD_NAMES.DESTROY_MANY, this.destroyManyResolverFactory], [RESOLVER_METHOD_NAMES.RESTORE_ONE, this.restoreOneResolverFactory], [RESOLVER_METHOD_NAMES.RESTORE_MANY, this.restoreManyResolverFactory], [RESOLVER_METHOD_NAMES.MERGE_MANY, this.mergeManyResolverFactory], ], ); this.argsAssertionMap = new Map void>([ [RESOLVER_METHOD_NAMES.FIND_MANY, assertFindManyArgs], [RESOLVER_METHOD_NAMES.FIND_ONE, assertFindOneArgs], [RESOLVER_METHOD_NAMES.FIND_DUPLICATES, assertFindDuplicatesArgs], [RESOLVER_METHOD_NAMES.GROUP_BY, assertGroupByArgs], [RESOLVER_METHOD_NAMES.CREATE_ONE, assertCreateOneArgs], [RESOLVER_METHOD_NAMES.CREATE_MANY, assertCreateManyArgs], [RESOLVER_METHOD_NAMES.UPDATE_ONE, assertUpdateOneArgs], [RESOLVER_METHOD_NAMES.UPDATE_MANY, assertUpdateManyArgs], [RESOLVER_METHOD_NAMES.DELETE_ONE, assertDeleteOneArgs], [RESOLVER_METHOD_NAMES.DELETE_MANY, assertDeleteManyArgs], [RESOLVER_METHOD_NAMES.DESTROY_ONE, assertDestroyOneArgs], [RESOLVER_METHOD_NAMES.DESTROY_MANY, assertDestroyManyArgs], [RESOLVER_METHOD_NAMES.RESTORE_ONE, assertRestoreOneArgs], [RESOLVER_METHOD_NAMES.RESTORE_MANY, assertRestoreManyArgs], [RESOLVER_METHOD_NAMES.MERGE_MANY, assertMergeManyArgs], ]); } async getWorkspaceResolverNames( workspaceId: string, ): Promise | null> { const { graphQLResolverNameMap } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ 'graphQLResolverNameMap', ]); return new Set(Object.keys(graphQLResolverNameMap)); } async execute( req: Request, document: DocumentNode, hasIntrospectionFields: boolean, hasWorkspaceFields: boolean, ): Promise { const [introspectionResult, workspaceResult] = await Promise.all([ hasIntrospectionFields ? this.executeIntrospectionQuery(req, document) : null, hasWorkspaceFields ? this.executeWorkspaceQuery(req, document) : null, ]); return this.mergeDirectExecutionResults( introspectionResult, workspaceResult, ); } private async executeWorkspaceQuery( req: Request, document: DocumentNode, ): Promise { try { const workspaceId = req.workspace?.id; if (!isDefined(workspaceId)) { return null; } const topLevelFields = graphQLExtractTopLevelFields( document, req.body.operationName, ); this.checkRootResolverLimitsOrThrow(topLevelFields); const fragmentMap = graphQLBuildFragmentMap(document); const variables = req.body.variables ?? {}; const data: Record = {}; const { graphQLResolverNameMap, flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps, } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ 'graphQLResolverNameMap', 'flatObjectMetadataMaps', 'flatFieldMetadataMaps', 'flatIndexMaps', ]); const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(flatObjectMetadataMaps); const errors: GraphQLFormattedError[] = []; await Promise.all( topLevelFields.map(async (field) => { const entry = graphQLResolverNameMap[field.name.value]; const responseKey = field.alias?.value ?? field.name.value; try { const args = extractArgumentsFromAst(field.arguments, variables); const graphqlPartialResolveInfo = graphQLBuildPartialResolveInfo( field, fragmentMap, ); const workspaceSchemaBuilderContext = buildWorkspaceSchemaBuilderContext( entry, flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps, objectIdByNameSingular, ); const result = (await this.executeField({ entry, args, graphqlPartialResolveInfo, workspaceSchemaBuilderContext, })) as ResolverOutput; const formattedResult = graphQLFormatResultFromSelectedFields( result, graphqlFields( graphqlPartialResolveInfo as GraphQLResolveInfo, {}, { excludedFields: [] }, ), workspaceSchemaBuilderContext.flatObjectMetadata.nameSingular, { flatObjectMetadataMaps, flatFieldMetadataMaps, objectIdByNameSingular, method: entry.method, }, ); data[responseKey] = formattedResult; } catch (error) { data[responseKey] = null; errors.push(this.formatError(error, req)); } }), ); if (errors.length > 0) { return { data, errors }; } return { data }; } catch (error) { return { errors: [this.formatError(error, req)] }; } } private async executeIntrospectionQuery( req: Request, document: DocumentNode, ): Promise { try { if (!isDefined(req.workspace)) { return null; } const schemaSDLResult = await this.workspaceGraphqlSchemaSDLService.getOrComputeSchemaSDL( req.workspace, req.application?.id ?? undefined, ); if (!isDefined(schemaSDLResult)) { return null; } const schema = buildSchema(schemaSDLResult.sdl); const result = await execute({ schema, document, operationName: req.body?.operationName as string | undefined, variableValues: (req.body?.variables as Record) ?? {}, }); return { data: result.data as Record | undefined, errors: result.errors?.map((error) => error.toJSON()), }; } catch (error) { return { errors: [this.formatError(error, req)] }; } } private mergeDirectExecutionResults( introspectionResult: DirectExecutionResult | null, workspaceResult: DirectExecutionResult | null, ): DirectExecutionResult | null { if (!introspectionResult && !workspaceResult) { return null; } const errors = [ ...(introspectionResult?.errors ?? []), ...(workspaceResult?.errors ?? []), ]; return { data: { ...introspectionResult?.data, ...workspaceResult?.data, }, ...(errors.length > 0 ? { errors } : {}), }; } private async executeField({ entry, args, graphqlPartialResolveInfo, workspaceSchemaBuilderContext, }: { entry: ResolverNameMapEntry; args: Record; graphqlPartialResolveInfo: Pick< GraphQLResolveInfo, 'fieldNodes' | 'fragments' >; workspaceSchemaBuilderContext: WorkspaceSchemaBuilderContext; }): Promise { const factory = this.factoryMap.get(entry.method); const assertFunction = this.argsAssertionMap.get(entry.method); if (!isDefined(factory) || !isDefined(assertFunction)) { throw new GraphqlDirectExecutionException( `Unknown method: ${entry.method}`, GraphqlDirectExecutionExceptionCode.UNKNOWN_METHOD, { userFriendlyMessage: STANDARD_ERROR_MESSAGE }, ); } assertFunction(args); const resolver = factory.create(workspaceSchemaBuilderContext); return resolver( null, args, null, graphqlPartialResolveInfo as GraphQLResolveInfo, ); } private formatError(error: unknown, req: Request): GraphQLFormattedError { if (!(error instanceof Error)) { return { message: 'Internal server error', extensions: { code: 'INTERNAL_SERVER_ERROR' }, }; } try { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } catch (graphqlError) { if (graphqlError instanceof GraphQLError) { const json = graphqlError.toJSON(); if (json.extensions?.userFriendlyMessage) { const userLocale = req.locale ?? SOURCE_LOCALE; const i18n = this.i18nService.getI18nInstance(userLocale); json.extensions.userFriendlyMessage = i18n._( json.extensions.userFriendlyMessage as MessageDescriptor, ); } return json; } } return { message: error.message, extensions: { code: 'INTERNAL_SERVER_ERROR' }, }; } private checkRootResolverLimitsOrThrow(topLevelFields: FieldNode[]): void { const maxRootResolvers = this.twentyConfigService.get( 'GRAPHQL_MAX_ROOT_RESOLVERS', ); if ( isDefined(maxRootResolvers) && topLevelFields.length > maxRootResolvers ) { throw new UserInputError( `Query too complex - Too many root resolvers requested: ${topLevelFields.length} - Maximum allowed root resolvers: ${maxRootResolvers}`, ); } const seen = new Set(); for (const field of topLevelFields) { const name = field.name.value; if (seen.has(name)) { throw new UserInputError(`Duplicate root resolver: "${name}"`); } seen.add(name); } } }