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
17 changed files with 281 additions and 297 deletions

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) => {
const isValid =
@ -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,
);
}
};

View File

@ -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;
@ -13,7 +12,7 @@ interface PgGraphQLErrorMapping {
command: string,
objectName: string,
pgGraphqlConfig: PgGraphQLConfig,
) => HttpException;
) => WorkspaceQueryRunnerException;
}
const pgGraphQLCommandMapping = {
@ -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,
),
};
@ -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
@ -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,
);
};

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 {
BadRequestException,
Injectable,
Logger,
RequestTimeoutException,
} from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
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 { 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';
@ -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;
@ -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,
);
}
@ -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,
);
}
}
@ -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(
@ -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,
);
}
@ -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) {