Handle query runner errors (#6424)

- Throw service error from query runner
- Catch in resolver factories 
- Map to graphql errors

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette 2024-07-27 12:27:04 +02:00 committed by GitHub
parent 3ff24658e1
commit 3060eb4e1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 281 additions and 297 deletions

View File

@ -6,25 +6,25 @@ import {
YogaDriverConfig, YogaDriverConfig,
YogaDriverServerContext, YogaDriverServerContext,
} from '@graphql-yoga/nestjs'; } from '@graphql-yoga/nestjs';
import { GraphQLSchema, GraphQLError } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { GraphQLError, GraphQLSchema } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory'; import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing';
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util'; import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util'; import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { useExceptionHandler } from 'src/engine/integrations/exception-handler/hooks/use-exception-handler.hook';
import { User } from 'src/engine/core-modules/user/user.entity';
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing';
export interface GraphQLContext extends YogaDriverServerContext<'express'> { export interface GraphQLContext extends YogaDriverServerContext<'express'> {
user?: User; user?: User;
@ -52,7 +52,7 @@ export class GraphQLConfigService
return context.req.user?.id ?? context.req.ip ?? 'anonymous'; return context.req.user?.id ?? context.req.ip ?? 'anonymous';
}, },
}), }),
useExceptionHandler({ useGraphQLErrorHandlerHook({
exceptionHandlerService: this.exceptionHandlerService, exceptionHandlerService: this.exceptionHandlerService,
}), }),
]; ];

View File

@ -1,4 +1,7 @@
import { BadRequestException } from '@nestjs/common'; import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
export const assertIsValidUuid = (value: string) => { export const assertIsValidUuid = (value: string) => {
const isValid = const isValid =
@ -7,6 +10,9 @@ export const assertIsValidUuid = (value: string) => {
); );
if (!isValid) { if (!isValid) {
throw new BadRequestException(`Value "${value}" is not a valid UUID`); throw new WorkspaceQueryRunnerException(
`Value "${value}" is not a valid UUID`,
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
} }
}; };

View File

@ -1,8 +1,7 @@
import { import {
BadRequestException, WorkspaceQueryRunnerException,
HttpException, WorkspaceQueryRunnerExceptionCode,
InternalServerErrorException, } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
} from '@nestjs/common';
export type PgGraphQLConfig = { export type PgGraphQLConfig = {
atMost: number; atMost: number;
@ -13,7 +12,7 @@ interface PgGraphQLErrorMapping {
command: string, command: string,
objectName: string, objectName: string,
pgGraphqlConfig: PgGraphQLConfig, pgGraphqlConfig: PgGraphQLConfig,
) => HttpException; ) => WorkspaceQueryRunnerException;
} }
const pgGraphQLCommandMapping = { const pgGraphQLCommandMapping = {
@ -24,18 +23,28 @@ const pgGraphQLCommandMapping = {
const pgGraphQLErrorMapping: PgGraphQLErrorMapping = { const pgGraphQLErrorMapping: PgGraphQLErrorMapping = {
'delete impacts too many records': (_, objectName, pgGraphqlConfig) => 'delete impacts too many records': (_, objectName, pgGraphqlConfig) =>
new BadRequestException( new WorkspaceQueryRunnerException(
`Cannot delete ${objectName} because it impacts too many records (more than ${pgGraphqlConfig?.atMost}).`, `Cannot delete ${objectName} because it impacts too many records (more than ${pgGraphqlConfig?.atMost}).`,
WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED,
), ),
'update impacts too many records': (_, objectName, pgGraphqlConfig) => 'update impacts too many records': (_, objectName, pgGraphqlConfig) =>
new BadRequestException( new WorkspaceQueryRunnerException(
`Cannot update ${objectName} because it impacts too many records (more than ${pgGraphqlConfig?.atMost}).`, `Cannot update ${objectName} because it impacts too many records (more than ${pgGraphqlConfig?.atMost}).`,
WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED,
), ),
'duplicate key value violates unique constraint': (command, objectName, _) => 'duplicate key value violates unique constraint': (command, objectName, _) =>
new BadRequestException( new WorkspaceQueryRunnerException(
`Cannot ${ `Cannot ${
pgGraphQLCommandMapping[command] ?? command pgGraphQLCommandMapping[command] ?? command
} ${objectName} because it violates a uniqueness constraint.`, } ${objectName} because it violates a uniqueness constraint.`,
WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT,
),
'violates foreign key constraint': (command, objectName, _) =>
new WorkspaceQueryRunnerException(
`Cannot ${
pgGraphQLCommandMapping[command] ?? command
} ${objectName} because it violates a foreign key constraint.`,
WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT,
), ),
}; };
@ -49,7 +58,7 @@ export const computePgGraphQLError = (
const errorMessage = error?.message; const errorMessage = error?.message;
const mappedErrorKey = Object.keys(pgGraphQLErrorMapping).find( const mappedErrorKey = Object.keys(pgGraphQLErrorMapping).find(
(key) => errorMessage?.startsWith(key), (key) => errorMessage?.includes(key),
); );
const mappedError = mappedErrorKey const mappedError = mappedErrorKey
@ -60,7 +69,8 @@ export const computePgGraphQLError = (
return mappedError(command, objectName, pgGraphqlConfig); return mappedError(command, objectName, pgGraphqlConfig);
} }
return new InternalServerErrorException( return new WorkspaceQueryRunnerException(
`GraphQL errors on ${command}${objectName}: ${JSON.stringify(error)}`, `GraphQL errors on ${command}${objectName}: ${JSON.stringify(error)}`,
WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,
); );
}; };

View File

@ -0,0 +1,36 @@
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import {
ForbiddenError,
InternalServerError,
NotFoundError,
TimeoutError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
export const workspaceQueryRunnerGraphqlApiExceptionHandler = (
error: Error,
) => {
if (error instanceof WorkspaceQueryRunnerException) {
switch (error.code) {
case WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND:
throw new NotFoundError(error.message);
case WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
throw new UserInputError(error.message);
case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_UNIQUE_CONSTRAINT:
case WorkspaceQueryRunnerExceptionCode.QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT:
case WorkspaceQueryRunnerExceptionCode.TOO_MANY_ROWS_AFFECTED:
case WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED:
throw new ForbiddenError(error.message);
case WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT:
throw new TimeoutError(error.message);
case WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerError(error.message);
}
}
throw error;
};

View File

@ -0,0 +1,19 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceQueryRunnerException extends CustomException {
code: WorkspaceQueryRunnerExceptionCode;
constructor(message: string, code: WorkspaceQueryRunnerExceptionCode) {
super(message, code);
}
}
export enum WorkspaceQueryRunnerExceptionCode {
INVALID_QUERY_INPUT = 'INVALID_FIELD_INPUT',
DATA_NOT_FOUND = 'DATA_NOT_FOUND',
QUERY_TIMEOUT = 'QUERY_TIMEOUT',
QUERY_VIOLATES_UNIQUE_CONSTRAINT = 'QUERY_VIOLATES_UNIQUE_CONSTRAINT',
QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT = 'QUERY_VIOLATES_FOREIGN_KEY_CONSTRAINT',
TOO_MANY_ROWS_AFFECTED = 'TOO_MANY_ROWS_AFFECTED',
NO_ROWS_AFFECTED = 'NO_ROWS_AFFECTED',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
}

View File

@ -1,9 +1,4 @@
import { import { Injectable, Logger } from '@nestjs/common';
BadRequestException,
Injectable,
Logger,
RequestTimeoutException,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
@ -40,8 +35,11 @@ import {
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util'; import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; 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 { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service'; import { DuplicateService } from 'src/engine/core-modules/duplicate/duplicate.service';
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
@ -138,7 +136,10 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
if (!args.filter || Object.keys(args.filter).length === 0) { if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument'); throw new WorkspaceQueryRunnerException(
'Missing filter argument',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
} }
const { workspaceId, userId, objectMetadataItem } = options; const { workspaceId, userId, objectMetadataItem } = options;
@ -176,14 +177,16 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<TRecord> | undefined> { ): Promise<IConnection<TRecord> | undefined> {
if (!args.data && !args.ids) { if (!args.data && !args.ids) {
throw new BadRequestException( throw new WorkspaceQueryRunnerException(
'You have to provide either "data" or "id" argument', 'You have to provide either "data" or "id" argument',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
); );
} }
if (!args.ids && isEmpty(args.data)) { if (!args.ids && isEmpty(args.data)) {
throw new BadRequestException( throw new WorkspaceQueryRunnerException(
'The "data" condition can not be empty when ID input not provided', 'The "data" condition can not be empty when ID input not provided',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
); );
} }
@ -205,7 +208,10 @@ export class WorkspaceQueryRunnerService {
); );
if (!existingRecords || existingRecords.length === 0) { if (!existingRecords || existingRecords.length === 0) {
throw new NotFoundError(`Object with id ${args.ids} not found`); throw new WorkspaceQueryRunnerException(
`Object with id ${args.ids} not found`,
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
);
} }
} }
@ -386,7 +392,10 @@ export class WorkspaceQueryRunnerService {
}); });
if (!existingRecord) { if (!existingRecord) {
throw new NotFoundError(`Object with id ${args.id} not found`); throw new WorkspaceQueryRunnerException(
`Object with id ${args.id} not found`,
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
);
} }
const query = await this.workspaceQueryBuilderFactory.updateOne( const query = await this.workspaceQueryBuilderFactory.updateOne(
@ -681,8 +690,9 @@ export class WorkspaceQueryRunnerService {
); );
} catch (error) { } catch (error) {
if (isQueryTimeoutError(error)) { if (isQueryTimeoutError(error)) {
throw new RequestTimeoutException( throw new WorkspaceQueryRunnerException(
'The SQL request took too long to process, resulting in a query read timeout. To resolve this issue, consider modifying your query by reducing the depth of relationships or limiting the number of records being fetched.', 'The SQL request took too long to process, resulting in a query read timeout. To resolve this issue, consider modifying your query by reducing the depth of relationships or limiting the number of records being fetched.',
WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT,
); );
} }
@ -733,7 +743,10 @@ export class WorkspaceQueryRunnerService {
['update', 'deleteFrom'].includes(command) && ['update', 'deleteFrom'].includes(command) &&
!result.affectedCount !result.affectedCount
) { ) {
throw new BadRequestException('No rows were affected.'); throw new WorkspaceQueryRunnerException(
'No rows were affected.',
WorkspaceQueryRunnerExceptionCode.NO_ROWS_AFFECTED,
);
} }
if (errors && errors.length > 0) { if (errors && errors.length > 0) {

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
CreateManyResolverArgs, CreateManyResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class CreateManyResolverFactory
): Resolver<CreateManyResolverArgs> { ): Resolver<CreateManyResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.createMany(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.createMany(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
CreateOneResolverArgs, CreateOneResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class CreateOneResolverFactory
): Resolver<CreateOneResolverArgs> { ): Resolver<CreateOneResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.createOne(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.createOne(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
DeleteManyResolverArgs, DeleteManyResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class DeleteManyResolverFactory
): Resolver<DeleteManyResolverArgs> { ): Resolver<DeleteManyResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.deleteMany(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.deleteMany(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
DeleteOneResolverArgs, DeleteOneResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class DeleteOneResolverFactory
): Resolver<DeleteOneResolverArgs> { ): Resolver<DeleteOneResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.deleteOne(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.deleteOne(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,16 +1,17 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import {
Resolver,
FindOneResolverArgs,
ExecuteQuickActionOnOneResolverArgs,
DeleteOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; 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,
ExecuteQuickActionOnOneResolverArgs,
FindOneResolverArgs,
Resolver,
} 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 { 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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
import { QuickActionsService } from 'src/engine/core-modules/quick-actions/quick-actions.service'; import { QuickActionsService } from 'src/engine/core-modules/quick-actions/quick-actions.service';
@ -30,15 +31,19 @@ export class ExecuteQuickActionOnOneResolverFactory
): Resolver<ExecuteQuickActionOnOneResolverArgs> { ): Resolver<ExecuteQuickActionOnOneResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.executeQuickActionOnOne(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.executeQuickActionOnOne(args, {
userId: internalContext.userId, objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId, userId: internalContext.userId,
info, workspaceId: internalContext.workspaceId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
FindDuplicatesResolverArgs, FindDuplicatesResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class FindDuplicatesResolverFactory
): Resolver<FindDuplicatesResolverArgs> { ): Resolver<FindDuplicatesResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.findDuplicates(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.findDuplicates(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
FindManyResolverArgs, FindManyResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class FindManyResolverFactory
): Resolver<FindManyResolverArgs> { ): Resolver<FindManyResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.findMany(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.findMany(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
FindOneResolverArgs, FindOneResolverArgs,
Resolver, Resolver,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class FindOneResolverFactory
): Resolver<FindOneResolverArgs> { ): Resolver<FindOneResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.findOne(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.findOne(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
Resolver, Resolver,
UpdateManyResolverArgs, UpdateManyResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class UpdateManyResolverFactory
): Resolver<UpdateManyResolverArgs> { ): Resolver<UpdateManyResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.updateMany(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.updateMany(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import { import {
Resolver, Resolver,
UpdateOneResolverArgs, UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } 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 { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
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 { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
@Injectable() @Injectable()
@ -24,15 +25,19 @@ export class UpdateOneResolverFactory
): Resolver<UpdateOneResolverArgs> { ): Resolver<UpdateOneResolverArgs> {
const internalContext = context; const internalContext = context;
return (_source, args, context, info) => { return async (_source, args, context, info) => {
return this.workspaceQueryRunnerService.updateOne(args, { try {
objectMetadataItem: internalContext.objectMetadataItem, return await this.workspaceQueryRunnerService.updateOne(args, {
workspaceId: internalContext.workspaceId, objectMetadataItem: internalContext.objectMetadataItem,
userId: internalContext.userId, workspaceId: internalContext.workspaceId,
info, userId: internalContext.userId,
fieldMetadataCollection: internalContext.fieldMetadataCollection, info,
objectMetadataCollection: internalContext.objectMetadataCollection, fieldMetadataCollection: internalContext.fieldMetadataCollection,
}); objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
}; };
} }
} }

View File

@ -1,150 +0,0 @@
import {
OnExecuteDoneHookResultOnNextHook,
Plugin,
getDocumentString,
handleStreamOrSingleExecutionResult,
} from '@envelop/core';
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
import {
convertExceptionToGraphQLError,
shouldFilterException,
} from 'src/engine/utils/global-exception-handler.util';
export type ExceptionHandlerPluginOptions = {
/**
* The exception handler service to use.
*/
exceptionHandlerService: ExceptionHandlerService;
/**
* The key of the event id in the error's extension. `null` to disable.
* @default exceptionEventId
*/
eventIdKey?: string | null;
};
// This hook is deprecated.
// We should either handle exception in the context of graphql, controller or command
// @deprecated
export const useExceptionHandler = <PluginContext extends GraphQLContext>(
options: ExceptionHandlerPluginOptions,
): Plugin<PluginContext> => {
const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId';
function addEventId(
err: GraphQLError,
eventId: string | undefined | null,
): GraphQLError {
if (eventIdKey !== null && eventId) {
err.extensions[eventIdKey] = eventId;
}
return err;
}
return {
async onExecute({ args }) {
const exceptionHandlerService = options.exceptionHandlerService;
const rootOperation = args.document.definitions.find(
(o) => o.kind === Kind.OPERATION_DEFINITION,
) as OperationDefinitionNode;
const operationType = rootOperation.operation;
const user = args.contextValue.req.user;
const document = getDocumentString(args.document, print);
const opName =
args.operationName ||
rootOperation.name?.value ||
'Anonymous Operation';
return {
onExecuteDone(payload) {
const handleResult: OnExecuteDoneHookResultOnNextHook<object> = ({
result,
setResult,
}) => {
if (result.errors && result.errors.length > 0) {
const exceptions = result.errors.reduce<{
filtered: any[];
unfiltered: any[];
}>(
(acc, err) => {
// Filter out exceptions that we don't want to be captured by exception handler
if (shouldFilterException(err?.originalError ?? err)) {
acc.filtered.push(err);
} else {
acc.unfiltered.push(err);
}
return acc;
},
{
filtered: [],
unfiltered: [],
},
);
if (exceptions.unfiltered.length > 0) {
const eventIds = exceptionHandlerService.captureExceptions(
exceptions.unfiltered,
{
operation: {
name: opName,
type: operationType,
},
document,
user,
},
);
exceptions.unfiltered.map((err, i) =>
addEventId(err, eventIds?.[i]),
);
}
const concatenatedErrors = [
...exceptions.filtered,
...exceptions.unfiltered,
];
const errors = concatenatedErrors.map((err) => {
if (!err.originalError) {
return err;
}
return convertExceptionToGraphQLError(err.originalError);
});
setResult({
...result,
errors,
});
}
};
return handleStreamOrSingleExecutionResult(payload, handleResult);
},
};
},
onValidate: ({ context, validateFn, params: { documentAST, schema } }) => {
const errors = validateFn(schema, documentAST);
if (Array.isArray(errors) && errors.length > 0) {
const headers = context.req.headers;
const currentSchemaVersion = context.req.cacheVersion;
const requestSchemaVersion = headers['x-schema-version'];
if (
requestSchemaVersion &&
requestSchemaVersion !== currentSchemaVersion
) {
throw new GraphQLError(
`Schema version mismatch, please refresh the page.`,
);
}
}
},
};
};