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

View File

@ -0,0 +1,11 @@
import { CustomException } from 'src/utils/custom-exception';
export class DataSourceException extends CustomException {
constructor(message: string, code: DataSourceExceptionCode) {
super(message, code);
}
}
export enum DataSourceExceptionCode {
DATA_SOURCE_NOT_FOUND = 'DATA_SOURCE_NOT_FOUND',
}

View File

@ -3,6 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { FindManyOptions, Repository } from 'typeorm'; import { FindManyOptions, Repository } from 'typeorm';
import {
DataSourceException,
DataSourceExceptionCode,
} from 'src/engine/metadata-modules/data-source/data-source.exception';
import { DataSourceEntity } from './data-source.entity'; import { DataSourceEntity } from './data-source.entity';
@Injectable() @Injectable()
@ -58,10 +63,17 @@ export class DataSourceService {
async getLastDataSourceMetadataFromWorkspaceIdOrFail( async getLastDataSourceMetadataFromWorkspaceIdOrFail(
workspaceId: string, workspaceId: string,
): Promise<DataSourceEntity> { ): Promise<DataSourceEntity> {
return this.dataSourceMetadataRepository.findOneOrFail({ try {
where: { workspaceId }, return this.dataSourceMetadataRepository.findOneOrFail({
order: { createdAt: 'DESC' }, where: { workspaceId },
}); order: { createdAt: 'DESC' },
});
} catch (error) {
throw new DataSourceException(
`Data source not found for workspace ${workspaceId}: ${error}`,
DataSourceExceptionCode.DATA_SOURCE_NOT_FOUND,
);
}
} }
async delete(workspaceId: string): Promise<void> { async delete(workspaceId: string): Promise<void> {

View File

@ -1,9 +0,0 @@
import { BadRequestException } from '@nestjs/common';
export class InvalidStringException extends BadRequestException {
constructor(string: string) {
const message = `String "${string}" is not valid`;
super(message);
}
}

View File

@ -0,0 +1,17 @@
import { CustomException } from 'src/utils/custom-exception';
export class FieldMetadataException extends CustomException {
code: FieldMetadataExceptionCode;
constructor(message: string, code: FieldMetadataExceptionCode) {
super(message, code);
}
}
export enum FieldMetadataExceptionCode {
FIELD_METADATA_NOT_FOUND = 'FIELD_METADATA_NOT_FOUND',
INVALID_FIELD_INPUT = 'INVALID_FIELD_INPUT',
FIELD_MUTATION_NOT_ALLOWED = 'FIELD_MUTATION_NOT_ALLOWED',
FIELD_ALREADY_EXISTS = 'FIELD_ALREADY_EXISTS',
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
}

View File

@ -23,6 +23,7 @@ import { RelationDefinitionDTO } from 'src/engine/metadata-modules/field-metadat
import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input'; import { UpdateOneFieldMetadataInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Resolver(() => FieldMetadataDTO) @Resolver(() => FieldMetadataDTO)
@ -34,10 +35,14 @@ export class FieldMetadataResolver {
@Args('input') input: CreateOneFieldMetadataInput, @Args('input') input: CreateOneFieldMetadataInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.fieldMetadataService.createOne({ try {
...input.field, return this.fieldMetadataService.createOne({
workspaceId, ...input.field,
}); workspaceId,
});
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => FieldMetadataDTO) @Mutation(() => FieldMetadataDTO)
@ -45,10 +50,14 @@ export class FieldMetadataResolver {
@Args('input') input: UpdateOneFieldMetadataInput, @Args('input') input: UpdateOneFieldMetadataInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.fieldMetadataService.updateOne(input.id, { try {
...input.update, return this.fieldMetadataService.updateOne(input.id, {
workspaceId, ...input.update,
}); workspaceId,
});
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => FieldMetadataDTO) @Mutation(() => FieldMetadataDTO)
@ -85,27 +94,32 @@ export class FieldMetadataResolver {
); );
} }
return this.fieldMetadataService.deleteOneField(input, workspaceId); try {
return this.fieldMetadataService.deleteOneField(input, workspaceId);
} catch (error) {
fieldMetadataGraphqlApiExceptionHandler(error);
}
} }
@ResolveField(() => RelationDefinitionDTO, { nullable: true }) @ResolveField(() => RelationDefinitionDTO, { nullable: true })
async relationDefinition( async relationDefinition(
@Parent() fieldMetadata: FieldMetadataDTO, @Parent() fieldMetadata: FieldMetadataDTO,
@Context() context: { loaders: IDataloaders }, @Context() context: { loaders: IDataloaders },
): Promise<RelationDefinitionDTO | null> { ): Promise<RelationDefinitionDTO | null | undefined> {
if (fieldMetadata.type !== FieldMetadataType.RELATION) { if (fieldMetadata.type !== FieldMetadataType.RELATION) {
return null; return null;
} }
const relationMetadataItem = try {
await context.loaders.relationMetadataLoader.load(fieldMetadata.id); const relationMetadataItem =
await context.loaders.relationMetadataLoader.load(fieldMetadata.id);
const relationDefinition = return this.fieldMetadataService.getRelationDefinitionFromRelationMetadata(
await this.fieldMetadataService.getRelationDefinitionFromRelationMetadata(
fieldMetadata, fieldMetadata,
relationMetadataItem, relationMetadataItem,
); );
} catch (error) {
return relationDefinition; fieldMetadataGraphqlApiExceptionHandler(error);
}
} }
} }

View File

@ -1,9 +1,4 @@
import { import { Injectable } from '@nestjs/common';
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
@ -39,9 +34,15 @@ import {
import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input'; import { DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException'; import {
import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; validateMetadataName,
InvalidStringException,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { import {
FieldMetadataEntity, FieldMetadataEntity,
@ -94,7 +95,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
if (!objectMetadata) { if (!objectMetadata) {
throw new NotFoundException('Object does not exist'); throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
} }
if (!fieldMetadataInput.isRemoteCreation) { if (!fieldMetadataInput.isRemoteCreation) {
@ -107,7 +111,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
!fieldMetadataInput.options && !fieldMetadataInput.options &&
fieldMetadataInput.type !== FieldMetadataType.RATING fieldMetadataInput.type !== FieldMetadataType.RATING
) { ) {
throw new BadRequestException('Options are required for enum fields'); throw new FieldMetadataException(
'Options are required for enum fields',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} }
} }
@ -127,7 +134,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}); });
if (fieldAlreadyExists) { if (fieldAlreadyExists) {
throw new ConflictException('Field already exists'); throw new FieldMetadataException(
'Field already exists',
FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS,
);
} }
const createdFieldMetadata = await fieldMetadataRepository.save({ const createdFieldMetadata = await fieldMetadataRepository.save({
@ -183,7 +193,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
const workspaceQueryRunner = workspaceDataSource?.createQueryRunner(); const workspaceQueryRunner = workspaceDataSource?.createQueryRunner();
if (!workspaceQueryRunner) { if (!workspaceQueryRunner) {
throw new Error('Could not create workspace query runner'); throw new FieldMetadataException(
'Could not create workspace query runner',
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
);
} }
await workspaceQueryRunner.connect(); await workspaceQueryRunner.connect();
@ -263,7 +276,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}); });
if (!existingFieldMetadata) { if (!existingFieldMetadata) {
throw new NotFoundException('Field does not exist'); throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
} }
const objectMetadata = const objectMetadata =
@ -277,7 +293,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
); );
if (!objectMetadata) { if (!objectMetadata) {
throw new NotFoundException('Object does not exist'); throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
} }
assertMutationNotOnRemoteObject(objectMetadata); assertMutationNotOnRemoteObject(objectMetadata);
@ -287,15 +306,19 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
existingFieldMetadata.id && existingFieldMetadata.id &&
fieldMetadataInput.isActive === false fieldMetadataInput.isActive === false
) { ) {
throw new BadRequestException( throw new FieldMetadataException(
'Cannot deactivate label identifier field', 'Cannot deactivate label identifier field',
FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
); );
} }
if (fieldMetadataInput.options) { if (fieldMetadataInput.options) {
for (const option of fieldMetadataInput.options) { for (const option of fieldMetadataInput.options) {
if (!option.id) { if (!option.id) {
throw new BadRequestException('Option id is required'); throw new FieldMetadataException(
'Option id is required',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} }
} }
} }
@ -325,10 +348,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
? updatableFieldInput.defaultValue ? updatableFieldInput.defaultValue
: null, : null,
}); });
const updatedFieldMetadata = await fieldMetadataRepository.findOneOrFail({
const updatedFieldMetadata = await fieldMetadataRepository.findOne({
where: { id }, where: { id },
}); });
if (!updatedFieldMetadata) {
throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
}
if ( if (
fieldMetadataInput.name || fieldMetadataInput.name ||
updatableFieldInput.options || updatableFieldInput.options ||
@ -392,7 +423,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}); });
if (!fieldMetadata) { if (!fieldMetadata) {
throw new NotFoundException('Field does not exist'); throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
} }
const objectMetadata = const objectMetadata =
@ -403,7 +437,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}); });
if (!objectMetadata) { if (!objectMetadata) {
throw new NotFoundException('Object does not exist'); throw new FieldMetadataException(
'Object metadata does not exist',
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
} }
await fieldMetadataRepository.delete(fieldMetadata.id); await fieldMetadataRepository.delete(fieldMetadata.id);
@ -454,7 +491,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}); });
if (!fieldMetadata) { if (!fieldMetadata) {
throw new NotFoundException('Field does not exist'); throw new FieldMetadataException(
'Field does not exist',
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
);
} }
return fieldMetadata; return fieldMetadata;
@ -517,9 +557,12 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
relationMetadata.relationType === RelationMetadataType.MANY_TO_MANY || relationMetadata.relationType === RelationMetadataType.MANY_TO_MANY ||
relationMetadata.relationType === RelationMetadataType.MANY_TO_ONE relationMetadata.relationType === RelationMetadataType.MANY_TO_ONE
) { ) {
throw new Error(` throw new FieldMetadataException(
`
Relation type ${relationMetadata.relationType} not supported Relation type ${relationMetadata.relationType} not supported
`); `,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} }
if (isRelationFromSource) { if (isRelationFromSource) {
@ -561,8 +604,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
validateMetadataName(fieldMetadataInput.name); validateMetadataName(fieldMetadataInput.name);
} catch (error) { } catch (error) {
if (error instanceof InvalidStringException) { if (error instanceof InvalidStringException) {
throw new BadRequestException( throw new FieldMetadataException(
`Characters used in name "${fieldMetadataInput.name}" are not supported`, `Characters used in name "${fieldMetadataInput.name}" are not supported`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
); );
} else { } else {
throw error; throw error;

View File

@ -1,5 +1,4 @@
import { BadRequestException } from '@nestjs/common'; import { FieldMetadataException } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value'; import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-default-value';
describe('serializeDefaultValue', () => { describe('serializeDefaultValue', () => {
@ -15,8 +14,10 @@ describe('serializeDefaultValue', () => {
expect(serializeDefaultValue('now')).toBe('now()'); expect(serializeDefaultValue('now')).toBe('now()');
}); });
it('should throw BadRequestException for invalid dynamic default value type', () => { it('should throw FieldMetadataException for invalid dynamic default value type', () => {
expect(() => serializeDefaultValue('invalid')).toThrow(BadRequestException); expect(() => serializeDefaultValue('invalid')).toThrow(
FieldMetadataException,
);
}); });
it('should handle string static default value', () => { it('should handle string static default value', () => {

View File

@ -4,6 +4,10 @@ import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/in
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
import { pascalCase } from 'src/utils/pascal-case'; import { pascalCase } from 'src/utils/pascal-case';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
type ComputeColumnNameOptions = { isForeignKey?: boolean }; type ComputeColumnNameOptions = { isForeignKey?: boolean };
@ -29,8 +33,9 @@ export function computeColumnName<T extends FieldMetadataType | 'default'>(
} }
if (isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) { if (isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) {
throw new Error( throw new FieldMetadataException(
`Cannot compute column name for composite field metadata type: ${fieldMetadataOrFieldName.type}`, `Cannot compute composite column name for non-composite field metadata type: ${fieldMetadataOrFieldName.type}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
); );
} }
@ -61,8 +66,9 @@ export function computeCompositeColumnName<
} }
if (!isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) { if (!isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) {
throw new Error( throw new FieldMetadataException(
`Cannot compute composite column name for non-composite field metadata type: ${fieldMetadataOrFieldName.type}`, `Cannot compute composite column name for non-composite field metadata type: ${fieldMetadataOrFieldName.type}`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
); );
} }

View File

@ -0,0 +1,32 @@
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import {
UserInputError,
ForbiddenError,
ConflictError,
InternalServerError,
NotFoundError,
} from 'src/engine/utils/graphql-errors.util';
export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof FieldMetadataException) {
switch (error.code) {
case FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
case FieldMetadataExceptionCode.INVALID_FIELD_INPUT:
throw new UserInputError(error.message);
case FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED:
throw new ForbiddenError(error.message);
case FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS:
throw new ConflictError(error.message);
case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerError(error.message);
}
}
throw error;
};

View File

@ -1,7 +1,9 @@
import { BadRequestException } from '@nestjs/common';
import { FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface'; import { FieldMetadataDefaultSerializableValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util'; import { isFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/is-function-default-value.util';
import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util'; import { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util';
@ -18,7 +20,10 @@ export const serializeDefaultValue = (
serializeFunctionDefaultValue(defaultValue); serializeFunctionDefaultValue(defaultValue);
if (!serializedTypeDefaultValue) { if (!serializedTypeDefaultValue) {
throw new BadRequestException('Invalid default value'); throw new FieldMetadataException(
'Invalid default value',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} }
return serializedTypeDefaultValue; return serializedTypeDefaultValue;
@ -51,5 +56,8 @@ export const serializeDefaultValue = (
return `'${JSON.stringify(defaultValue)}'`; return `'${JSON.stringify(defaultValue)}'`;
} }
throw new BadRequestException(`Invalid default value "${defaultValue}"`); throw new FieldMetadataException(
`Invalid default value "${defaultValue}"`,
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
}; };

View File

@ -8,6 +8,10 @@ import {
FieldMetadataComplexOption, FieldMetadataComplexOption,
FieldMetadataDefaultOption, FieldMetadataDefaultOption,
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import {
FieldMetadataException,
FieldMetadataExceptionCode,
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
import { isEnumFieldMetadataType } from './is-enum-field-metadata-type.util'; import { isEnumFieldMetadataType } from './is-enum-field-metadata-type.util';
@ -24,7 +28,10 @@ export const validateOptionsForType = (
if (options === null) return true; if (options === null) return true;
if (!Array.isArray(options)) { if (!Array.isArray(options)) {
throw new Error('Options must be an array'); throw new FieldMetadataException(
'Options must be an array',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} }
if (!isEnumFieldMetadataType(type)) { if (!isEnumFieldMetadataType(type)) {
@ -39,7 +46,10 @@ export const validateOptionsForType = (
// Check if all options are unique // Check if all options are unique
if (new Set(values).size !== options.length) { if (new Set(values).size !== options.length) {
throw new Error('Options must be unique'); throw new FieldMetadataException(
'Options must be unique',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
);
} }
const validators = optionsValidatorsMap[type]; const validators = optionsValidatorsMap[type];

View File

@ -0,0 +1,15 @@
import { CustomException } from 'src/utils/custom-exception';
export class ObjectMetadataException extends CustomException {
code: ObjectMetadataExceptionCode;
constructor(message: string, code: ObjectMetadataExceptionCode) {
super(message, code);
}
}
export enum ObjectMetadataExceptionCode {
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
INVALID_OBJECT_INPUT = 'INVALID_OBJECT_INPUT',
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
}

View File

@ -12,6 +12,7 @@ import {
UpdateOneObjectInput, UpdateOneObjectInput,
} from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Resolver(() => ObjectMetadataDTO) @Resolver(() => ObjectMetadataDTO)
@ -26,7 +27,11 @@ export class ObjectMetadataResolver {
@Args('input') input: DeleteOneObjectInput, @Args('input') input: DeleteOneObjectInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.objectMetadataService.deleteOneObject(input, workspaceId); try {
return this.objectMetadataService.deleteOneObject(input, workspaceId);
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => ObjectMetadataDTO) @Mutation(() => ObjectMetadataDTO)
@ -34,8 +39,12 @@ export class ObjectMetadataResolver {
@Args('input') input: UpdateOneObjectInput, @Args('input') input: UpdateOneObjectInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
await this.beforeUpdateOneObject.run(input, workspaceId); try {
await this.beforeUpdateOneObject.run(input, workspaceId);
return this.objectMetadataService.updateOneObject(input, workspaceId); return this.objectMetadataService.updateOneObject(input, workspaceId);
} catch (error) {
objectMetadataGraphqlApiExceptionHandler(error);
}
} }
} }

View File

@ -1,9 +1,4 @@
import { import { Injectable } from '@nestjs/common';
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import console from 'console'; import console from 'console';
@ -56,6 +51,10 @@ import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import { ObjectMetadataEntity } from './object-metadata.entity'; import { ObjectMetadataEntity } from './object-metadata.entity';
@ -121,7 +120,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}); });
if (!objectMetadata) { if (!objectMetadata) {
throw new NotFoundException('Object does not exist'); throw new ObjectMetadataException(
'Object does not exist',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
} }
// DELETE RELATIONS // DELETE RELATIONS
@ -159,8 +161,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
objectMetadataInput.nameSingular.toLowerCase() === objectMetadataInput.nameSingular.toLowerCase() ===
objectMetadataInput.namePlural.toLowerCase() objectMetadataInput.namePlural.toLowerCase()
) { ) {
throw new BadRequestException( throw new ObjectMetadataException(
'The singular and plural name cannot be the same for an object', 'The singular and plural name cannot be the same for an object',
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
); );
} }
@ -186,7 +189,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}); });
if (objectAlreadyExists) { if (objectAlreadyExists) {
throw new ConflictException('Object already exists'); throw new ObjectMetadataException(
'Object already exists',
ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS,
);
} }
const isCustom = !objectMetadataInput.isRemote; const isCustom = !objectMetadataInput.isRemote;
@ -372,18 +378,25 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: string, workspaceId: string,
options: FindOneOptions<ObjectMetadataEntity>, options: FindOneOptions<ObjectMetadataEntity>,
): Promise<ObjectMetadataEntity> { ): Promise<ObjectMetadataEntity> {
return this.objectMetadataRepository.findOneOrFail({ try {
relations: [ return this.objectMetadataRepository.findOneOrFail({
'fields', relations: [
'fields.fromRelationMetadata', 'fields',
'fields.toRelationMetadata', 'fields.fromRelationMetadata',
], 'fields.toRelationMetadata',
...options, ],
where: { ...options,
...options.where, where: {
workspaceId, ...options.where,
}, workspaceId,
}); },
});
} catch (error) {
throw new ObjectMetadataException(
'Object does not exist',
ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
} }
public async findManyWithinWorkspace( public async findManyWithinWorkspace(

View File

@ -1,11 +1,17 @@
import { BadRequestException } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
export const assertMutationNotOnRemoteObject = ( export const assertMutationNotOnRemoteObject = (
objectMetadataItem: ObjectMetadataInterface, objectMetadataItem: ObjectMetadataInterface,
) => { ) => {
if (objectMetadataItem.isRemote) { if (objectMetadataItem.isRemote) {
throw new BadRequestException('Remote objects are read-only'); throw new ObjectMetadataException(
'Remote objects are read-only',
ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED,
);
} }
}; };

View File

@ -0,0 +1,30 @@
import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import {
UserInputError,
ForbiddenError,
ConflictError,
InternalServerError,
NotFoundError,
} from 'src/engine/utils/graphql-errors.util';
export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof ObjectMetadataException) {
switch (error.code) {
case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT:
throw new UserInputError(error.message);
case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED:
throw new ForbiddenError(error.message);
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
throw new ConflictError(error.message);
default:
throw new InternalServerError(error.message);
}
}
throw error;
};

View File

@ -1,9 +1,13 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException';
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input'; import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; import {
ObjectMetadataException,
ObjectMetadataExceptionCode,
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
import {
validateMetadataName,
InvalidStringException,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { camelCase } from 'src/utils/camel-case'; import { camelCase } from 'src/utils/camel-case';
const coreObjectNames = [ const coreObjectNames = [
@ -55,7 +59,10 @@ export const validateObjectMetadataInputOrThrow = <
const validateNameIsNotReservedKeywordOrThrow = (name?: string) => { const validateNameIsNotReservedKeywordOrThrow = (name?: string) => {
if (name) { if (name) {
if (reservedKeywords.includes(name)) { if (reservedKeywords.includes(name)) {
throw new ForbiddenException(`The name "${name}" is not available`); throw new ObjectMetadataException(
`The name "${name}" is not available`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
} }
} }
}; };
@ -63,7 +70,10 @@ const validateNameIsNotReservedKeywordOrThrow = (name?: string) => {
const validateNameCamelCasedOrThrow = (name?: string) => { const validateNameCamelCasedOrThrow = (name?: string) => {
if (name) { if (name) {
if (name !== camelCase(name)) { if (name !== camelCase(name)) {
throw new ForbiddenException(`Name should be in camelCase: ${name}`); throw new ObjectMetadataException(
`Name should be in camelCase: ${name}`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
);
} }
} }
}; };
@ -75,8 +85,9 @@ const validateNameCharactersOrThrow = (name?: string) => {
} }
} catch (error) { } catch (error) {
if (error instanceof InvalidStringException) { if (error instanceof InvalidStringException) {
throw new BadRequestException( throw new ObjectMetadataException(
`Characters used in name "${name}" are not supported`, `Characters used in name "${name}" are not supported`,
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
); );
} else { } else {
throw error; throw error;

View File

@ -0,0 +1,15 @@
import { CustomException } from 'src/utils/custom-exception';
export class RelationMetadataException extends CustomException {
code: RelationMetadataExceptionCode;
constructor(message: string, code: RelationMetadataExceptionCode) {
super(message, code);
}
}
export enum RelationMetadataExceptionCode {
RELATION_METADATA_NOT_FOUND = 'RELATION_METADATA_NOT_FOUND',
INVALID_RELATION_INPUT = 'INVALID_RELATION_INPUT',
RELATION_ALREADY_EXISTS = 'RELATION_ALREADY_EXISTS',
FOREIGN_KEY_NOT_FOUND = 'FOREIGN_KEY_NOT_FOUND',
}

View File

@ -7,6 +7,7 @@ import { RelationMetadataService } from 'src/engine/metadata-modules/relation-me
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto'; import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
import { DeleteOneRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/delete-relation.input'; import { DeleteOneRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/delete-relation.input';
import { relationMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/relation-metadata/utils/relation-metadata-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Resolver() @Resolver()
@ -20,9 +21,13 @@ export class RelationMetadataResolver {
@Args('input') input: DeleteOneRelationInput, @Args('input') input: DeleteOneRelationInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.relationMetadataService.deleteOneRelation( try {
input.id, return this.relationMetadataService.deleteOneRelation(
workspaceId, input.id,
); workspaceId,
);
} catch (error) {
relationMetadataGraphqlApiExceptionHandler(error);
}
} }
} }

View File

@ -1,9 +1,4 @@
import { import { Injectable, NotFoundException } from '@nestjs/common';
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
@ -28,9 +23,15 @@ import {
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException'; import {
import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; validateMetadataName,
InvalidStringException,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import {
RelationMetadataException,
RelationMetadataExceptionCode,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
import { import {
RelationMetadataEntity, RelationMetadataEntity,
@ -66,8 +67,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
validateMetadataName(relationMetadataInput.toName); validateMetadataName(relationMetadataInput.toName);
} catch (error) { } catch (error) {
if (error instanceof InvalidStringException) { if (error instanceof InvalidStringException) {
throw new BadRequestException( throw new RelationMetadataException(
`Characters used in name "${relationMetadataInput.fromName}" or "${relationMetadataInput.toName}" are not supported`, `Characters used in name "${relationMetadataInput.fromName}" or "${relationMetadataInput.toName}" are not supported`,
RelationMetadataExceptionCode.INVALID_RELATION_INPUT,
); );
} else { } else {
throw error; throw error;
@ -132,8 +134,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
if ( if (
relationMetadataInput.relationType === RelationMetadataType.MANY_TO_MANY relationMetadataInput.relationType === RelationMetadataType.MANY_TO_MANY
) { ) {
throw new BadRequestException( throw new RelationMetadataException(
'Many to many relations are not supported yet', 'Many to many relations are not supported yet',
RelationMetadataExceptionCode.INVALID_RELATION_INPUT,
); );
} }
@ -142,8 +145,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
undefined || undefined ||
objectMetadataMap[relationMetadataInput.toObjectMetadataId] === undefined objectMetadataMap[relationMetadataInput.toObjectMetadataId] === undefined
) { ) {
throw new NotFoundException( throw new RelationMetadataException(
'Can\t find an existing object matching with fromObjectMetadataId or toObjectMetadataId', 'Can\t find an existing object matching with fromObjectMetadataId or toObjectMetadataId',
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
); );
} }
@ -177,12 +181,13 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
); );
if (fieldAlreadyExists) { if (fieldAlreadyExists) {
throw new ConflictException( throw new RelationMetadataException(
`Field on ${ `Field on ${
objectMetadataMap[ objectMetadataMap[
relationMetadataInput[`${relationDirection}ObjectMetadataId`] relationMetadataInput[`${relationDirection}ObjectMetadataId`]
].nameSingular ].nameSingular
} already exists`, } already exists`,
RelationMetadataExceptionCode.RELATION_ALREADY_EXISTS,
); );
} }
} }
@ -335,7 +340,10 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
}); });
if (!relationMetadata) { if (!relationMetadata) {
throw new NotFoundException('Relation does not exist'); throw new RelationMetadataException(
'Relation does not exist',
RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND,
);
} }
const foreignKeyFieldMetadataName = `${camelCase( const foreignKeyFieldMetadataName = `${camelCase(
@ -351,8 +359,9 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
}); });
if (!foreignKeyFieldMetadata) { if (!foreignKeyFieldMetadata) {
throw new NotFoundException( throw new RelationMetadataException(
`Foreign key fieldMetadata not found (${foreignKeyFieldMetadataName}) for relation ${relationMetadata.id}`, `Foreign key fieldMetadata not found (${foreignKeyFieldMetadataName}) for relation ${relationMetadata.id}`,
RelationMetadataExceptionCode.FOREIGN_KEY_NOT_FOUND,
); );
} }
@ -420,6 +429,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
return ( return (
foundRelationMetadataItem ?? foundRelationMetadataItem ??
// TODO: return a relation metadata not found exception
new NotFoundException( new NotFoundException(
`RelationMetadata with fieldMetadataId ${fieldMetadataId} not found`, `RelationMetadata with fieldMetadataId ${fieldMetadataId} not found`,
) )

View File

@ -0,0 +1,28 @@
import {
RelationMetadataException,
RelationMetadataExceptionCode,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.exception';
import {
UserInputError,
ConflictError,
InternalServerError,
NotFoundError,
} from 'src/engine/utils/graphql-errors.util';
export const relationMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof RelationMetadataException) {
switch (error.code) {
case RelationMetadataExceptionCode.RELATION_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
case RelationMetadataExceptionCode.INVALID_RELATION_INPUT:
throw new UserInputError(error.message);
case RelationMetadataExceptionCode.RELATION_ALREADY_EXISTS:
throw new ConflictError(error.message);
case RelationMetadataExceptionCode.FOREIGN_KEY_NOT_FOUND:
default:
throw new InternalServerError(error.message);
}
}
throw error;
};

View File

@ -0,0 +1,16 @@
import { CustomException } from 'src/utils/custom-exception';
export class RemoteServerException extends CustomException {
code: RemoteServerExceptionCode;
constructor(message: string, code: RemoteServerExceptionCode) {
super(message, code);
}
}
export enum RemoteServerExceptionCode {
REMOTE_SERVER_NOT_FOUND = 'REMOTE_SERVER_NOT_FOUND',
REMOTE_SERVER_ALREADY_EXISTS = 'REMOTE_SERVER_ALREADY_EXISTS',
REMOTE_SERVER_MUTATION_NOT_ALLOWED = 'REMOTE_SERVER_MUTATION_NOT_ALLOWED',
REMOTE_SERVER_CONNECTION_ERROR = 'REMOTE_SERVER_CONNECTION_ERROR',
INVALID_REMOTE_SERVER_INPUT = 'INVALID_REMOTE_SERVER_INPUT',
}

View File

@ -11,6 +11,7 @@ import { RemoteServerDTO } from 'src/engine/metadata-modules/remote-server/dtos/
import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input'; import { UpdateRemoteServerInput } from 'src/engine/metadata-modules/remote-server/dtos/update-remote-server.input';
import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service'; import { RemoteServerService } from 'src/engine/metadata-modules/remote-server/remote-server.service';
import { remoteServerGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/remote-server/utils/remote-server-graphql-api-exception-handler.util';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Resolver() @Resolver()
@ -24,7 +25,11 @@ export class RemoteServerResolver {
@Args('input') input: CreateRemoteServerInput<RemoteServerType>, @Args('input') input: CreateRemoteServerInput<RemoteServerType>,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteServerService.createOneRemoteServer(input, workspaceId); try {
return this.remoteServerService.createOneRemoteServer(input, workspaceId);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => RemoteServerDTO) @Mutation(() => RemoteServerDTO)
@ -32,7 +37,11 @@ export class RemoteServerResolver {
@Args('input') input: UpdateRemoteServerInput<RemoteServerType>, @Args('input') input: UpdateRemoteServerInput<RemoteServerType>,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteServerService.updateOneRemoteServer(input, workspaceId); try {
return this.remoteServerService.updateOneRemoteServer(input, workspaceId);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => RemoteServerDTO) @Mutation(() => RemoteServerDTO)
@ -40,7 +49,11 @@ export class RemoteServerResolver {
@Args('input') { id }: RemoteServerIdInput, @Args('input') { id }: RemoteServerIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteServerService.deleteOneRemoteServer(id, workspaceId); try {
return this.remoteServerService.deleteOneRemoteServer(id, workspaceId);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
} }
@Query(() => RemoteServerDTO) @Query(() => RemoteServerDTO)
@ -48,7 +61,14 @@ export class RemoteServerResolver {
@Args('input') { id }: RemoteServerIdInput, @Args('input') { id }: RemoteServerIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteServerService.findOneByIdWithinWorkspace(id, workspaceId); try {
return this.remoteServerService.findOneByIdWithinWorkspace(
id,
workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
} }
@Query(() => [RemoteServerDTO]) @Query(() => [RemoteServerDTO])
@ -57,9 +77,13 @@ export class RemoteServerResolver {
{ foreignDataWrapperType }: RemoteServerTypeInput<RemoteServerType>, { foreignDataWrapperType }: RemoteServerTypeInput<RemoteServerType>,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteServerService.findManyByTypeWithinWorkspace( try {
foreignDataWrapperType, return this.remoteServerService.findManyByTypeWithinWorkspace(
workspaceId, foreignDataWrapperType,
); workspaceId,
);
} catch (error) {
remoteServerGraphqlApiExceptionHandler(error);
}
} }
} }

View File

@ -1,8 +1,4 @@
import { import { Injectable } from '@nestjs/common';
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import isEmpty from 'lodash.isempty'; import isEmpty from 'lodash.isempty';
@ -27,6 +23,10 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
import { buildUpdateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils'; import { buildUpdateRemoteServerRawQuery } from 'src/engine/metadata-modules/remote-server/utils/build-update-remote-server-raw-query.utils';
import { validateRemoteServerType } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util'; import { validateRemoteServerType } from 'src/engine/metadata-modules/remote-server/utils/validate-remote-server-type.util';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
@Injectable() @Injectable()
export class RemoteServerService<T extends RemoteServerType> { export class RemoteServerService<T extends RemoteServerType> {
@ -122,7 +122,10 @@ export class RemoteServerService<T extends RemoteServerType> {
); );
if (!remoteServer) { if (!remoteServer) {
throw new NotFoundException('Remote server does not exist'); throw new RemoteServerException(
'Remote server does not exist',
RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND,
);
} }
const currentRemoteTablesForServer = const currentRemoteTablesForServer =
@ -132,8 +135,9 @@ export class RemoteServerService<T extends RemoteServerType> {
}); });
if (currentRemoteTablesForServer.length > 0) { if (currentRemoteTablesForServer.length > 0) {
throw new ForbiddenException( throw new RemoteServerException(
'Cannot update remote server with synchronized tables', 'Cannot update remote server with synchronized tables',
RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED,
); );
} }
@ -207,7 +211,10 @@ export class RemoteServerService<T extends RemoteServerType> {
}); });
if (!remoteServer) { if (!remoteServer) {
throw new NotFoundException('Remote server does not exist'); throw new RemoteServerException(
'Remote server does not exist',
RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND,
);
} }
await this.remoteTableService.unsyncAll(workspaceId, remoteServer); await this.remoteTableService.unsyncAll(workspaceId, remoteServer);

View File

@ -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',
}

View File

@ -1,8 +1,4 @@
import { import { Injectable } from '@nestjs/common';
BadRequestException,
Injectable,
RequestTimeoutException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from '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 { 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 { PostgresTableSchemaColumn } from 'src/engine/metadata-modules/remote-server/types/postgres-table-schema-column';
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util'; 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() @Injectable()
export class DistantTableService { export class DistantTableService {
@ -64,7 +64,10 @@ export class DistantTableService {
tableName?: string, tableName?: string,
): Promise<DistantTables> { ): Promise<DistantTables> {
if (!remoteServer.schema) { 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(); const tmpSchemaId = v4();
@ -116,8 +119,9 @@ export class DistantTableService {
return distantTables; return distantTables;
} catch (error) { } catch (error) {
if (isQueryTimeoutError(error)) { if (isQueryTimeoutError(error)) {
throw new RequestTimeoutException( throw new DistantTableException(
`Could not find distant tables: ${error.message}`, `Could not find distant tables: ${error.message}`,
DistantTableExceptionCode.TIMEOUT_ERROR,
); );
} }
@ -132,8 +136,9 @@ export class DistantTableService {
case RemoteServerType.STRIPE_FDW: case RemoteServerType.STRIPE_FDW:
return STRIPE_DISTANT_TABLES; return STRIPE_DISTANT_TABLES;
default: default:
throw new BadRequestException( throw new DistantTableException(
`Type ${remoteServer.foreignDataWrapperType} does not have a static schema.`, `Type ${remoteServer.foreignDataWrapperType} does not have a static schema.`,
DistantTableExceptionCode.INTERNAL_SERVER_ERROR,
); );
} }
} }

View File

@ -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',
}

View File

@ -1,10 +1,14 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
RemoteServerEntity, RemoteServerEntity,
RemoteServerType, RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; } 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 { 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 { 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 { 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'; import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
@ -90,8 +94,9 @@ export class ForeignTableService {
} catch (exception) { } catch (exception) {
this.workspaceMigrationService.deleteById(workspaceMigration.id); 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.', '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) { } catch (exception) {
this.workspaceMigrationService.deleteById(workspaceMigration.id); 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: case RemoteServerType.STRIPE_FDW:
return { object: distantTableName }; return { object: distantTableName };
default: default:
throw new BadRequestException('Foreign data wrapper not supported'); throw new ForeignTableException(
'Foreign data wrapper not supported',
ForeignTableExceptionCode.INVALID_FOREIGN_TABLE_INPUT,
);
} }
} }
} }

View File

@ -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',
}

View File

@ -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 { 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 { 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 { 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) @UseGuards(JwtAuthGuard)
@Resolver() @Resolver()
@ -19,11 +20,15 @@ export class RemoteTableResolver {
@Args('input') input: FindManyRemoteTablesInput, @Args('input') input: FindManyRemoteTablesInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteTableService.findDistantTablesWithStatus( try {
input.id, return this.remoteTableService.findDistantTablesWithStatus(
workspaceId, input.id,
input.shouldFetchPendingSchemaUpdates, workspaceId,
); input.shouldFetchPendingSchemaUpdates,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => RemoteTableDTO) @Mutation(() => RemoteTableDTO)
@ -31,7 +36,11 @@ export class RemoteTableResolver {
@Args('input') input: RemoteTableInput, @Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteTableService.syncRemoteTable(input, workspaceId); try {
return this.remoteTableService.syncRemoteTable(input, workspaceId);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => RemoteTableDTO) @Mutation(() => RemoteTableDTO)
@ -39,7 +48,11 @@ export class RemoteTableResolver {
@Args('input') input: RemoteTableInput, @Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteTableService.unsyncRemoteTable(input, workspaceId); try {
return this.remoteTableService.unsyncRemoteTable(input, workspaceId);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
} }
@Mutation(() => RemoteTableDTO) @Mutation(() => RemoteTableDTO)
@ -47,9 +60,13 @@ export class RemoteTableResolver {
@Args('input') input: RemoteTableInput, @Args('input') input: RemoteTableInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
return this.remoteTableService.syncRemoteTableSchemaChanges( try {
input, return this.remoteTableService.syncRemoteTableSchemaChanges(
workspaceId, input,
); workspaceId,
);
} catch (error) {
remoteTableGraphqlApiExceptionHandler(error);
}
} }
} }

View File

@ -1,4 +1,4 @@
import { BadRequestException, Logger, NotFoundException } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -40,6 +40,10 @@ import {
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; 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 { 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 { export class RemoteTableService {
private readonly logger = new Logger(RemoteTableService.name); private readonly logger = new Logger(RemoteTableService.name);
@ -74,7 +78,10 @@ export class RemoteTableService {
}); });
if (!remoteServer) { 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({ const currentRemoteTables = await this.findRemoteTablesByServerId({
@ -148,7 +155,10 @@ export class RemoteTableService {
}); });
if (!remoteServer) { 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 = const currentRemoteTableWithSameDistantName =
@ -161,7 +171,10 @@ export class RemoteTableService {
}); });
if (currentRemoteTableWithSameDistantName) { if (currentRemoteTableWithSameDistantName) {
throw new BadRequestException('Remote table already exists'); throw new RemoteTableException(
'Remote server does not exist',
RemoteTableExceptionCode.REMOTE_TABLE_ALREADY_EXISTS,
);
} }
const dataSourceMetatada = const dataSourceMetatada =
@ -200,7 +213,10 @@ export class RemoteTableService {
); );
if (!distantTableColumns) { 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. // We only support remote tables with an id column for now.
@ -209,7 +225,10 @@ export class RemoteTableService {
); );
if (!distantTableIdColumn) { 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( await this.foreignTableService.createForeignTable(
@ -250,7 +269,10 @@ export class RemoteTableService {
}); });
if (!remoteServer) { 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({ const remoteTable = await this.remoteTableRepository.findOne({
@ -262,7 +284,10 @@ export class RemoteTableService {
}); });
if (!remoteTable) { 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); await this.unsyncOne(workspaceId, remoteTable, remoteServer);
@ -302,7 +327,10 @@ export class RemoteTableService {
}); });
if (!remoteServer) { 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({ const remoteTable = await this.remoteTableRepository.findOne({
@ -314,7 +342,10 @@ export class RemoteTableService {
}); });
if (!remoteTable) { 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 = const distantTableColumns =
@ -379,7 +410,10 @@ export class RemoteTableService {
); );
if (!currentForeignTableNames.includes(remoteTable.localTableName)) { 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 = const objectMetadata =
@ -507,8 +541,9 @@ export class RemoteTableService {
}); });
if (!objectMetadata) { if (!objectMetadata) {
throw new NotFoundException( throw new RemoteTableException(
`Cannot find associated object for table ${foreignTableName}`, `Cannot find associated object for table ${foreignTableName}`,
RemoteTableExceptionCode.NO_OBJECT_METADATA_FOUND,
); );
} }
for (const columnUpdate of columnsUpdates) { for (const columnUpdate of columnsUpdates) {
@ -547,8 +582,9 @@ export class RemoteTableService {
}); });
if (!fieldMetadataToDelete) { if (!fieldMetadataToDelete) {
throw new NotFoundException( throw new RemoteTableException(
`Cannot find associated field metadata for column ${columnName}`, `Cannot find associated field metadata for column ${columnName}`,
RemoteTableExceptionCode.NO_FIELD_METADATA_FOUND,
); );
} }

View File

@ -1,9 +1,11 @@
import { BadRequestException } from '@nestjs/common/exceptions';
import { singular } from 'pluralize'; import { singular } from 'pluralize';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
import { camelCase } from 'src/utils/camel-case'; 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; 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,
);
}; };

View File

@ -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;
};

View File

@ -1,10 +1,12 @@
import { BadRequestException } from '@nestjs/common';
import { import {
ForeignDataWrapperOptions, ForeignDataWrapperOptions,
RemoteServerEntity, RemoteServerEntity,
RemoteServerType, RemoteServerType,
} from 'src/engine/metadata-modules/remote-server/remote-server.entity'; } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options'; import { UserMappingOptions } from 'src/engine/metadata-modules/remote-server/types/user-mapping-options';
export type DeepPartial<T> = { export type DeepPartial<T> = {
@ -49,7 +51,10 @@ export const buildUpdateRemoteServerRawQuery = (
} }
if (options.length < 1) { if (options.length < 1) {
throw new BadRequestException('No fields to update'); throw new RemoteServerException(
'No fields to update',
RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT,
);
} }
const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join( const rawQuery = `UPDATE metadata."remoteServer" SET ${options.join(

View File

@ -0,0 +1,30 @@
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
import {
UserInputError,
ForbiddenError,
ConflictError,
InternalServerError,
NotFoundError,
} from 'src/engine/utils/graphql-errors.util';
export const remoteServerGraphqlApiExceptionHandler = (error: any) => {
if (error instanceof RemoteServerException) {
switch (error.code) {
case RemoteServerExceptionCode.REMOTE_SERVER_NOT_FOUND:
throw new NotFoundError(error.message);
case RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT:
throw new UserInputError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_MUTATION_NOT_ALLOWED:
throw new ForbiddenError(error.message);
case RemoteServerExceptionCode.REMOTE_SERVER_ALREADY_EXISTS:
throw new ConflictError(error.message);
default:
throw new InternalServerError(error.message);
}
}
throw error;
};

View File

@ -1,7 +1,10 @@
import { BadRequestException } from '@nestjs/common';
import { isDefined } from 'class-validator'; import { isDefined } from 'class-validator';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/; const INPUT_REGEX = /^([A-Za-z0-9\-_.@]+)$/;
export const validateObjectAgainstInjections = (input: object) => { export const validateObjectAgainstInjections = (input: object) => {
@ -21,6 +24,9 @@ export const validateObjectAgainstInjections = (input: object) => {
export const validateStringAgainstInjections = (input: string) => { export const validateStringAgainstInjections = (input: string) => {
if (!INPUT_REGEX.test(input)) { if (!INPUT_REGEX.test(input)) {
throw new BadRequestException('Invalid remote server input'); throw new RemoteServerException(
'Invalid remote server input',
RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT,
);
} }
}; };

View File

@ -1,5 +1,3 @@
import { BadRequestException } from '@nestjs/common';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { import {
@ -7,6 +5,10 @@ import {
FeatureFlagKeys, FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity'; } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity'; import { RemoteServerType } from 'src/engine/metadata-modules/remote-server/remote-server.entity';
import {
RemoteServerException,
RemoteServerExceptionCode,
} from 'src/engine/metadata-modules/remote-server/remote-server.exception';
export const validateRemoteServerType = async ( export const validateRemoteServerType = async (
remoteServerType: RemoteServerType, remoteServerType: RemoteServerType,
@ -24,7 +26,10 @@ export const validateRemoteServerType = async (
const featureFlagEnabled = featureFlag && featureFlag.value; const featureFlagEnabled = featureFlag && featureFlag.value;
if (!featureFlagEnabled) { if (!featureFlagEnabled) {
throw new BadRequestException(`Type ${remoteServerType} is not supported.`); throw new RemoteServerException(
`Type ${remoteServerType} is not supported.`,
RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT,
);
} }
}; };
@ -35,8 +40,9 @@ const getFeatureFlagKey = (remoteServerType: RemoteServerType) => {
case RemoteServerType.STRIPE_FDW: case RemoteServerType.STRIPE_FDW:
return FeatureFlagKeys.IsStripeIntegrationEnabled; return FeatureFlagKeys.IsStripeIntegrationEnabled;
default: default:
throw new BadRequestException( throw new RemoteServerException(
`Type ${remoteServerType} is not supported.`, `Type ${remoteServerType} is not supported.`,
RemoteServerExceptionCode.INVALID_REMOTE_SERVER_INPUT,
); );
} }
}; };

View File

@ -1,5 +1,7 @@
import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException'; import {
import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils'; validateMetadataName,
InvalidStringException,
} from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
describe('validateMetadataName', () => { describe('validateMetadataName', () => {
it('does not throw if string is valid', () => { it('does not throw if string is valid', () => {

View File

@ -1,5 +1,3 @@
import { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException';
const VALID_STRING_PATTERN = /^[a-z][a-zA-Z0-9]*$/; const VALID_STRING_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
export const validateMetadataName = (string: string) => { export const validateMetadataName = (string: string) => {
@ -7,3 +5,11 @@ export const validateMetadataName = (string: string) => {
throw new InvalidStringException(string); throw new InvalidStringException(string);
} }
}; };
export class InvalidStringException extends Error {
constructor(string: string) {
const message = `String "${string}" is not valid`;
super(message);
}
}

View File

@ -13,6 +13,10 @@ import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadat
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
export type BasicFieldMetadataType = export type BasicFieldMetadataType =
| FieldMetadataType.UUID | FieldMetadataType.UUID
@ -66,8 +70,9 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
this.logger.error( this.logger.error(
`Column name not found for current or altered field metadata, can be due to a missing or an invalid target column map. Current column name: ${currentColumnName}, Altered column name: ${alteredColumnName}.`, `Column name not found for current or altered field metadata, can be due to a missing or an invalid target column map. Current column name: ${currentColumnName}, Altered column name: ${alteredColumnName}.`,
); );
throw new Error( throw new WorkspaceMigrationException(
`Column name not found for current or altered field metadata`, `Column name not found for current or altered field metadata`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
); );
} }

View File

@ -12,6 +12,10 @@ import {
WorkspaceMigrationColumnAlter, WorkspaceMigrationColumnAlter,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
export class ColumnActionAbstractFactory< export class ColumnActionAbstractFactory<
T extends FieldMetadataType | 'default', T extends FieldMetadataType | 'default',
@ -32,7 +36,10 @@ export class ColumnActionAbstractFactory<
return this.handleCreateAction(alteredFieldMetadata, options); return this.handleCreateAction(alteredFieldMetadata, options);
case WorkspaceMigrationColumnActionType.ALTER: { case WorkspaceMigrationColumnActionType.ALTER: {
if (!currentFieldMetadata) { if (!currentFieldMetadata) {
throw new Error('current field metadata is required for alter'); throw new WorkspaceMigrationException(
'current field metadata is required for alter',
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
);
} }
return this.handleAlterAction( return this.handleAlterAction(
@ -43,8 +50,10 @@ export class ColumnActionAbstractFactory<
} }
default: { default: {
this.logger.error(`Invalid action: ${action}`); this.logger.error(`Invalid action: ${action}`);
throw new WorkspaceMigrationException(
throw new Error('[AbstractFactory]: invalid action'); '[AbstractFactory]: invalid action',
WorkspaceMigrationExceptionCode.INVALID_ACTION,
);
} }
} }
} }
@ -53,7 +62,10 @@ export class ColumnActionAbstractFactory<
_fieldMetadata: FieldMetadataInterface<T>, _fieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions, _options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnCreate[] { ): WorkspaceMigrationColumnCreate[] {
throw new Error('handleCreateAction method not implemented.'); throw new WorkspaceMigrationException(
'handleCreateAction method not implemented.',
WorkspaceMigrationExceptionCode.INVALID_ACTION,
);
} }
protected handleAlterAction( protected handleAlterAction(
@ -61,6 +73,9 @@ export class ColumnActionAbstractFactory<
_alteredFieldMetadata: FieldMetadataInterface<T>, _alteredFieldMetadata: FieldMetadataInterface<T>,
_options?: WorkspaceColumnActionOptions, _options?: WorkspaceColumnActionOptions,
): WorkspaceMigrationColumnAlter[] { ): WorkspaceMigrationColumnAlter[] {
throw new Error('handleAlterAction method not implemented.'); throw new WorkspaceMigrationException(
'handleAlterAction method not implemented.',
WorkspaceMigrationExceptionCode.INVALID_ACTION,
);
} }
} }

View File

@ -13,6 +13,10 @@ import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/works
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types'; import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
export type CompositeFieldMetadataType = export type CompositeFieldMetadataType =
| FieldMetadataType.ADDRESS | FieldMetadataType.ADDRESS
@ -34,8 +38,9 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
this.logger.error( this.logger.error(
`Composite type not found for field metadata type: ${fieldMetadata.type}`, `Composite type not found for field metadata type: ${fieldMetadata.type}`,
); );
throw new Error( throw new WorkspaceMigrationException(
`Composite type not found for field metadata type: ${fieldMetadata.type}`, `Composite type not found for field metadata type: ${fieldMetadata.type}`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
); );
} }
@ -74,8 +79,9 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
this.logger.error( this.logger.error(
`Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`, `Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`,
); );
throw new Error( throw new WorkspaceMigrationException(
`Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`, `Composite type not found for field metadata type: ${currentFieldMetadata.type} or ${alteredFieldMetadata.type}`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
); );
} }
@ -91,8 +97,9 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
this.logger.error( this.logger.error(
`Current property not found for altered property: ${alteredProperty.name}`, `Current property not found for altered property: ${alteredProperty.name}`,
); );
throw new Error( throw new WorkspaceMigrationException(
`Current property not found for altered property: ${alteredProperty.name}`, `Current property not found for altered property: ${alteredProperty.name}`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
); );
} }

View File

@ -13,6 +13,10 @@ import { serializeDefaultValue } from 'src/engine/metadata-modules/field-metadat
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
export type EnumFieldMetadataType = export type EnumFieldMetadataType =
| FieldMetadataType.RATING | FieldMetadataType.RATING
@ -82,8 +86,9 @@ export class EnumColumnActionFactory extends ColumnActionAbstractFactory<EnumFie
this.logger.error( this.logger.error(
`Column name not found for current or altered field metadata, can be due to a missing or an invalid target column map. Current column name: ${currentColumnName}, Altered column name: ${alteredColumnName}.`, `Column name not found for current or altered field metadata, can be due to a missing or an invalid target column map. Current column name: ${currentColumnName}, Altered column name: ${alteredColumnName}.`,
); );
throw new Error( throw new WorkspaceMigrationException(
`Column name not found for current or altered field metadata`, `Column name not found for current or altered field metadata`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
); );
} }

View File

@ -1,4 +1,8 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>( export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
fieldMetadataType: Type, fieldMetadataType: Type,
@ -34,6 +38,9 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
case FieldMetadataType.RAW_JSON: case FieldMetadataType.RAW_JSON:
return 'jsonb'; return 'jsonb';
default: default:
throw new Error(`Cannot convert ${fieldMetadataType} to column type.`); throw new WorkspaceMigrationException(
`Cannot convert ${fieldMetadataType} to column type.`,
WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA,
);
} }
}; };

View File

@ -0,0 +1,14 @@
import { CustomException } from 'src/utils/custom-exception';
export class WorkspaceMigrationException extends CustomException {
code: WorkspaceMigrationExceptionCode;
constructor(message: string, code: WorkspaceMigrationExceptionCode) {
super(message, code);
}
}
export enum WorkspaceMigrationExceptionCode {
NO_FACTORY_FOUND = 'NO_FACTORY_FOUND',
INVALID_ACTION = 'INVALID_ACTION',
INVALID_FIELD_METADATA = 'INVALID_FIELD_METADATA',
}

View File

@ -12,6 +12,10 @@ import {
import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory';
import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory';
import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory';
import {
WorkspaceMigrationException,
WorkspaceMigrationExceptionCode,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception';
@Injectable() @Injectable()
export class WorkspaceMigrationFactory { export class WorkspaceMigrationFactory {
@ -131,7 +135,10 @@ export class WorkspaceMigrationFactory {
undefinedOrAlteredFieldMetadata, undefinedOrAlteredFieldMetadata,
); );
throw new Error(`No field metadata provided for action ${action}`); throw new WorkspaceMigrationException(
`No field metadata provided for action ${action}`,
WorkspaceMigrationExceptionCode.INVALID_ACTION,
);
} }
const columnActions = this.createColumnAction( const columnActions = this.createColumnAction(
@ -161,7 +168,10 @@ export class WorkspaceMigrationFactory {
}, },
); );
throw new Error(`No factory found for type ${alteredFieldMetadata.type}`); throw new WorkspaceMigrationException(
`No factory found for type ${alteredFieldMetadata.type}`,
WorkspaceMigrationExceptionCode.NO_FACTORY_FOUND,
);
} }
return factory.create( return factory.create(

View File

@ -183,3 +183,11 @@ export class TimeoutError extends BaseGraphQLError {
Object.defineProperty(this, 'name', { value: 'TimeoutError' }); Object.defineProperty(this, 'name', { value: 'TimeoutError' });
} }
} }
export class InternalServerError extends BaseGraphQLError {
constructor(message: string) {
super(message, ErrorCode.INTERNAL_SERVER_ERROR);
Object.defineProperty(this, 'name', { value: 'InternalServerError' });
}
}

View File

@ -0,0 +1,8 @@
export class CustomException extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}