twenty / packages /twenty-server /src /engine /api /graphql /direct-execution /direct-execution.service.ts
Jules
Initial clean CRM deployment with prebuilt assets
d9494a5
Raw
History Blame Contribute Delete
19.3 kB
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<string, unknown>;
errors?: GraphQLFormattedError[];
};
@Injectable()
export class DirectExecutionService {
private readonly factoryMap: Map<
string,
WorkspaceResolverBuilderFactoryInterface
>;
private readonly argsAssertionMap: Map<string, (args: unknown) => 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<string, WorkspaceResolverBuilderFactoryInterface>(
[
[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<string, (args: unknown) => 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<Set<string> | 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<DirectExecutionResult | null> {
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<DirectExecutionResult | null> {
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<string, unknown> = {};
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<DirectExecutionResult | null> {
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<string, unknown>) ?? {},
});
return {
data: result.data as Record<string, unknown> | 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<string, unknown>;
graphqlPartialResolveInfo: Pick<
GraphQLResolveInfo,
'fieldNodes' | 'fragments'
>;
workspaceSchemaBuilderContext: WorkspaceSchemaBuilderContext;
}): Promise<unknown> {
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<string>();
for (const field of topLevelFields) {
const name = field.name.value;
if (seen.has(name)) {
throw new UserInputError(`Duplicate root resolver: "${name}"`);
}
seen.add(name);
}
}
}