Skip to content

Commit

Permalink
Handle query runner errors (#6424)
Browse files Browse the repository at this point in the history
- Throw service error from query runner
- Catch in resolver factories 
- Map to graphql errors

---------

Co-authored-by: Charles Bochet <[email protected]>
  • Loading branch information
thomtrp and charlesBochet authored Jul 27, 2024
1 parent 3ff2465 commit 3060eb4
Show file tree
Hide file tree
Showing 17 changed files with 276 additions and 292 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@ import {
YogaDriverConfig,
YogaDriverServerContext,
} from '@graphql-yoga/nestjs';
import { GraphQLSchema, GraphQLError } from 'graphql';
import * as Sentry from '@sentry/node';
import { GraphQLError, GraphQLSchema } 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 { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';

import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
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 { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.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 { 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'> {
user?: User;
Expand Down Expand Up @@ -52,7 +52,7 @@ export class GraphQLConfigService
return context.req.user?.id ?? context.req.ip ?? 'anonymous';
},
}),
useExceptionHandler({
useGraphQLErrorHandlerHook({
exceptionHandlerService: this.exceptionHandlerService,
}),
];
Expand Down
Original file line number Diff line number Diff line change
@@ -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) => {
const isValid =
Expand All @@ -7,6 +10,9 @@ export const assertIsValidUuid = (value: string) => {
);

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,
);
}
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {
BadRequestException,
HttpException,
InternalServerErrorException,
} from '@nestjs/common';
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';

export type PgGraphQLConfig = {
atMost: number;
Expand All @@ -13,7 +12,7 @@ interface PgGraphQLErrorMapping {
command: string,
objectName: string,
pgGraphqlConfig: PgGraphQLConfig,
) => HttpException;
) => WorkspaceQueryRunnerException;
}

const pgGraphQLCommandMapping = {
Expand All @@ -24,18 +23,28 @@ const pgGraphQLCommandMapping = {

const pgGraphQLErrorMapping: PgGraphQLErrorMapping = {
'delete impacts too many records': (_, objectName, pgGraphqlConfig) =>
new BadRequestException(
new WorkspaceQueryRunnerException(
`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) =>
new BadRequestException(
new WorkspaceQueryRunnerException(
`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, _) =>
new BadRequestException(
new WorkspaceQueryRunnerException(
`Cannot ${
pgGraphQLCommandMapping[command] ?? command
} ${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,
),
};

Expand All @@ -49,7 +58,7 @@ export const computePgGraphQLError = (
const errorMessage = error?.message;

const mappedErrorKey = Object.keys(pgGraphQLErrorMapping).find(
(key) => errorMessage?.startsWith(key),
(key) => errorMessage?.includes(key),
);

const mappedError = mappedErrorKey
Expand All @@ -60,7 +69,8 @@ export const computePgGraphQLError = (
return mappedError(command, objectName, pgGraphqlConfig);
}

return new InternalServerErrorException(
return new WorkspaceQueryRunnerException(
`GraphQL errors on ${command}${objectName}: ${JSON.stringify(error)}`,
WorkspaceQueryRunnerExceptionCode.INTERNAL_SERVER_ERROR,
);
};
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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',
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
BadRequestException,
Injectable,
Logger,
RequestTimeoutException,
} from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

import isEmpty from 'lodash.isempty';
Expand Down Expand Up @@ -40,8 +35,11 @@ import {
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 { 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 { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
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';
Expand Down Expand Up @@ -138,7 +136,10 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
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;

Expand Down Expand Up @@ -176,14 +177,16 @@ export class WorkspaceQueryRunnerService {
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<TRecord> | undefined> {
if (!args.data && !args.ids) {
throw new BadRequestException(
throw new WorkspaceQueryRunnerException(
'You have to provide either "data" or "id" argument',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}

if (!args.ids && isEmpty(args.data)) {
throw new BadRequestException(
throw new WorkspaceQueryRunnerException(
'The "data" condition can not be empty when ID input not provided',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
);
}

Expand All @@ -205,7 +208,10 @@ export class WorkspaceQueryRunnerService {
);

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,
);
}
}

Expand Down Expand Up @@ -386,7 +392,10 @@ export class WorkspaceQueryRunnerService {
});

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(
Expand Down Expand Up @@ -681,8 +690,9 @@ export class WorkspaceQueryRunnerService {
);
} catch (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.',
WorkspaceQueryRunnerExceptionCode.QUERY_TIMEOUT,
);
}

Expand Down Expand Up @@ -733,7 +743,10 @@ export class WorkspaceQueryRunnerService {
['update', 'deleteFrom'].includes(command) &&
!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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';

import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
CreateManyResolverArgs,
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 { 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';

@Injectable()
Expand All @@ -24,15 +25,19 @@ export class CreateManyResolverFactory
): Resolver<CreateManyResolverArgs> {
const internalContext = context;

return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.createMany(args, {
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.createMany(args, {
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';

import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
import {
CreateOneResolverArgs,
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 { 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';

@Injectable()
Expand All @@ -24,15 +25,19 @@ export class CreateOneResolverFactory
): Resolver<CreateOneResolverArgs> {
const internalContext = context;

return (_source, args, context, info) => {
return this.workspaceQueryRunnerService.createOne(args, {
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
return async (_source, args, context, info) => {
try {
return await this.workspaceQueryRunnerService.createOne(args, {
objectMetadataItem: internalContext.objectMetadataItem,
workspaceId: internalContext.workspaceId,
userId: internalContext.userId,
info,
fieldMetadataCollection: internalContext.fieldMetadataCollection,
objectMetadataCollection: internalContext.objectMetadataCollection,
});
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
};
}
}
Loading

0 comments on commit 3060eb4

Please sign in to comment.