Add exceptions for metadata modules (#6070)

Class exception for each metadata module + handler to map on graphql
error

TODO left :
- find a way to call handler on auto-resolvers nestjs query (probably
interceptors)
- discuss what should be done for pre-hooks errors
- discuss what should be done for Unauthorized exception
This commit is contained in:
Thomas Trompette
2024-07-01 13:49:17 +02:00
committed by GitHub
parent 4599f43b6c
commit a15884ea0a
48 changed files with 815 additions and 199 deletions
@@ -0,0 +1,13 @@
import { CustomException } from 'src/utils/custom-exception';
export class DistantTableException extends CustomException {
code: DistantTableExceptionCode;
constructor(message: string, code: DistantTableExceptionCode) {
super(message, code);
}
}
export enum DistantTableExceptionCode {
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
}
@@ -1,8 +1,4 @@
import {
BadRequestException,
Injectable,
RequestTimeoutException,
} from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
@@ -17,6 +13,10 @@ import { DistantTables } from 'src/engine/metadata-modules/remote-server/remote-
import { STRIPE_DISTANT_TABLES } from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/utils/stripe-distant-tables.util';
import { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
import {
DistantTableException,
DistantTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/distant-table/distant-table.exception';
@Injectable()
export class DistantTableService {
@@ -64,7 +64,10 @@ export class DistantTableService {
tableName?: string,
): Promise<DistantTables> {
if (!remoteServer.schema) {
throw new BadRequestException('Remote server schema is not defined');
throw new DistantTableException(
'Remote server schema is not defined',
DistantTableExceptionCode.INTERNAL_SERVER_ERROR,
);
}
const tmpSchemaId = v4();
@@ -116,8 +119,9 @@ export class DistantTableService {
return distantTables;
} catch (error) {
if (isQueryTimeoutError(error)) {
throw new RequestTimeoutException(
throw new DistantTableException(
`Could not find distant tables: ${error.message}`,
DistantTableExceptionCode.TIMEOUT_ERROR,
);
}
@@ -132,8 +136,9 @@ export class DistantTableService {
case RemoteServerType.STRIPE_FDW:
return STRIPE_DISTANT_TABLES;
default:
throw new BadRequestException(
throw new DistantTableException(
`Type ${remoteServer.foreignDataWrapperType} does not have a static schema.`,
DistantTableExceptionCode.INTERNAL_SERVER_ERROR,
);
}
}
@@ -0,0 +1,13 @@
import { CustomException } from 'src/utils/custom-exception';
export class ForeignTableException extends CustomException {
code: ForeignTableExceptionCode;
constructor(message: string, code: ForeignTableExceptionCode) {
super(message, code);
}
}
export enum ForeignTableExceptionCode {
FOREIGN_TABLE_MUTATION_NOT_ALLOWED = 'FOREIGN_TABLE_MUTATION_NOT_ALLOWED',
INVALID_FOREIGN_TABLE_INPUT = 'INVALID_FOREIGN_TABLE_INPUT',
}
@@ -1,10 +1,14 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import {
RemoteServerEntity,
RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
import {
ForeignTableException,
ForeignTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/foreign-table.exception';
import { getForeignTableColumnName } from 'src/engine/metadata-modules/remote-server/remote-table/foreign-table/utils/get-foreign-table-column-name.util';
import { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
@@ -90,8 +94,9 @@ export class ForeignTableService {
} catch (exception) {
this.workspaceMigrationService.deleteById(workspaceMigration.id);
throw new BadRequestException(
throw new ForeignTableException(
'Could not create foreign table. The table may already exists or a column type may not be supported.',
ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT,
);
}
}
@@ -130,7 +135,10 @@ export class ForeignTableService {
} catch (exception) {
this.workspaceMigrationService.deleteById(workspaceMigration.id);
throw new BadRequestException('Could not alter foreign table.');
throw new ForeignTableException(
'Could not alter foreign table.',
ForeignTableExceptionCode.FOREIGN_TABLE_MUTATION_NOT_ALLOWED,
);
}
}
@@ -167,7 +175,10 @@ export class ForeignTableService {
case RemoteServerType.STRIPE_FDW:
return { object: distantTableName };
default:
throw new BadRequestException('Foreign data wrapper not supported');
throw new ForeignTableException(
'Foreign data wrapper not supported',
ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT,
);
}
}
}
@@ -0,0 +1,17 @@
import { CustomException } from 'src/utils/custom-exception';
export class RemoteTableException extends CustomException {
code: RemoteTableExceptionCode;
constructor(message: string, code: RemoteTableExceptionCode) {
super(message, code);
}
}
export enum RemoteTableExceptionCode {
REMOTE_TABLE_NOT_FOUND = 'REMOTE_TABLE_NOT_FOUND',
INVALID_REMOTE_TABLE_INPUT = 'INVALID_REMOTE_TABLE_INPUT',
REMOTE_TABLE_ALREADY_EXISTS = 'REMOTE_TABLE_ALREADY_EXISTS',
NO_FOREIGN_TABLES_FOUND = 'NO_FOREIGN_TABLES_FOUND',
NO_OBJECT_METADATA_FOUND = 'NO_OBJECT_METADATA_FOUND',
NO_FIELD_METADATA_FOUND = 'NO_FIELD_METADATA_FOUND',
}
@@ -8,6 +8,7 @@ import { FindManyRemoteTablesInput } from 'src/engine/metadata-modules/remote-se
import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input';
import { RemoteTableDTO } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table.dto';
import { RemoteTableService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.service';
import { remoteTableGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/remote-server/remote-table/utils/remote-table-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard)
@Resolver()
@@ -19,11 +20,15 @@ export class RemoteTableResolver {
@Args('input') input: FindManyRemoteTablesInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.remoteTableService.findDistantTablesWithStatus(
input.id,
workspaceId,
input.shouldFetchPendingSchemaUpdates,
);
try {
return this.remoteTableService.findDistantTablesWithStatus(
input.id,
workspaceId,
input.shouldFetchPendingSchemaUpdates,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteTableDTO)
@@ -31,7 +36,11 @@ export class RemoteTableResolver {
@Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.remoteTableService.syncRemoteTable(input, workspaceId);
try {
return this.remoteTableService.syncRemoteTable(input, workspaceId);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteTableDTO)
@@ -39,7 +48,11 @@ export class RemoteTableResolver {
@Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.remoteTableService.unsyncRemoteTable(input, workspaceId);
try {
return this.remoteTableService.unsyncRemoteTable(input, workspaceId);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
@Mutation(() => RemoteTableDTO)
@@ -47,9 +60,13 @@ export class RemoteTableResolver {
@Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.remoteTableService.syncRemoteTableSchemaChanges(
input,
workspaceId,
);
try {
return this.remoteTableService.syncRemoteTableSchemaChanges(
input,
workspaceId,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
}
}
@@ -1,4 +1,4 @@
import { BadRequestException, Logger, NotFoundException } from '@nestjs/common';
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@@ -40,6 +40,10 @@ import {
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
RemoteTableException,
RemoteTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
export class RemoteTableService {
private readonly logger = new Logger(RemoteTableService.name);
@@ -74,7 +78,10 @@ export class RemoteTableService {
});
if (!remoteServer) {
throw new NotFoundException('Remote server does not exist');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const currentRemoteTables = await this.findRemoteTablesByServerId({
@@ -148,7 +155,10 @@ export class RemoteTableService {
});
if (!remoteServer) {
throw new NotFoundException('Remote server does not exist');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const currentRemoteTableWithSameDistantName =
@@ -161,7 +171,10 @@ export class RemoteTableService {
});
if (currentRemoteTableWithSameDistantName) {
throw new BadRequestException('Remote table already exists');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS,
);
}
const dataSourceMetatada =
@@ -200,7 +213,10 @@ export class RemoteTableService {
);
if (!distantTableColumns) {
throw new BadRequestException('Table not found');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
);
}
// We only support remote tables with an id column for now.
@@ -209,7 +225,10 @@ export class RemoteTableService {
);
if (!distantTableIdColumn) {
throw new BadRequestException('Remote table must have an id column');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
await this.foreignTableService.createForeignTable(
@@ -250,7 +269,10 @@ export class RemoteTableService {
});
if (!remoteServer) {
throw new NotFoundException('Remote server does not exist');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const remoteTable = await this.remoteTableRepository.findOne({
@@ -262,7 +284,10 @@ export class RemoteTableService {
});
if (!remoteTable) {
throw new NotFoundException('Remote table does not exist');
throw new RemoteTableException(
'Remote table does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
);
}
await this.unsyncOne(workspaceId, remoteTable, remoteServer);
@@ -302,7 +327,10 @@ export class RemoteTableService {
});
if (!remoteServer) {
throw new NotFoundException('Remote server does not exist');
throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
}
const remoteTable = await this.remoteTableRepository.findOne({
@@ -314,7 +342,10 @@ export class RemoteTableService {
});
if (!remoteTable) {
throw new NotFoundException('Remote table does not exist');
throw new RemoteTableException(
'Remote table does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND,
);
}
const distantTableColumns =
@@ -379,7 +410,10 @@ export class RemoteTableService {
);
if (!currentForeignTableNames.includes(remoteTable.localTableName)) {
throw new NotFoundException('Foreign table does not exist');
throw new RemoteTableException(
'Foreign table does not exist',
RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND,
);
}
const objectMetadata =
@@ -507,8 +541,9 @@ export class RemoteTableService {
});
if (!objectMetadata) {
throw new NotFoundException(
throw new RemoteTableException(
`Cannot find associated object for table ${foreignTableName}`,
RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND,
);
}
for (const columnUpdate of columnsUpdates) {
@@ -547,8 +582,9 @@ export class RemoteTableService {
});
if (!fieldMetadataToDelete) {
throw new NotFoundException(
throw new RemoteTableException(
`Cannot find associated field metadata for column ${columnName}`,
RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND,
);
}
@@ -1,9 +1,11 @@
import { BadRequestException } from '@nestjs/common/exceptions';
import { singular } from 'pluralize';
import { DataSource } from 'typeorm';
import { camelCase } from 'src/utils/camel-case';
import {
RemoteTableException,
RemoteTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
const MAX_SUFFIX = 10;
@@ -55,5 +57,8 @@ export const getRemoteTableLocalName = async (
}
}
throw new BadRequestException('Table name is already taken.');
throw new RemoteTableException(
'Table name is already taken',
RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT,
);
};
@@ -0,0 +1,30 @@
import {
RemoteTableException,
RemoteTableExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-table.exception';
import {
UserInputError,
ConflictError,
InternalServerError,
NotFoundError,
} from 'src/engine/utils/graphql-errors.util';
export const remoteTableGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof RemoteTableException) {
switch (error.code) {
case RemoteTableExceptionCode.REMOTE_TABLE_NOT_FOUND:
case RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND:
case RemoteTableExceptionCode.NO_FOREIGN_TABLES_FOUND:
case RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND:
throw new NotFoundError(error.message);
case RemoteTableExceptionCode.INVALID_REMOTE_TABLE_INPUT:
throw new UserInputError(error.message);
case RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS:
throw new ConflictError(error.message);
default:
throw new InternalServerError(error.message);
}
}
throw error;
};