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:
@ -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',
|
||||
}
|
||||
@ -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 { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
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)
|
||||
@Resolver(() => FieldMetadataDTO)
|
||||
@ -34,10 +35,14 @@ export class FieldMetadataResolver {
|
||||
@Args('input') input: CreateOneFieldMetadataInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
return this.fieldMetadataService.createOne({
|
||||
...input.field,
|
||||
workspaceId,
|
||||
});
|
||||
try {
|
||||
return this.fieldMetadataService.createOne({
|
||||
...input.field,
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => FieldMetadataDTO)
|
||||
@ -45,10 +50,14 @@ export class FieldMetadataResolver {
|
||||
@Args('input') input: UpdateOneFieldMetadataInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
return this.fieldMetadataService.updateOne(input.id, {
|
||||
...input.update,
|
||||
workspaceId,
|
||||
});
|
||||
try {
|
||||
return this.fieldMetadataService.updateOne(input.id, {
|
||||
...input.update,
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@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 })
|
||||
async relationDefinition(
|
||||
@Parent() fieldMetadata: FieldMetadataDTO,
|
||||
@Context() context: { loaders: IDataloaders },
|
||||
): Promise<RelationDefinitionDTO | null> {
|
||||
): Promise<RelationDefinitionDTO | null | undefined> {
|
||||
if (fieldMetadata.type !== FieldMetadataType.RELATION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relationMetadataItem =
|
||||
await context.loaders.relationMetadataLoader.load(fieldMetadata.id);
|
||||
try {
|
||||
const relationMetadataItem =
|
||||
await context.loaders.relationMetadataLoader.load(fieldMetadata.id);
|
||||
|
||||
const relationDefinition =
|
||||
await this.fieldMetadataService.getRelationDefinitionFromRelationMetadata(
|
||||
return this.fieldMetadataService.getRelationDefinitionFromRelationMetadata(
|
||||
fieldMetadata,
|
||||
relationMetadataItem,
|
||||
);
|
||||
|
||||
return relationDefinition;
|
||||
} catch (error) {
|
||||
fieldMetadataGraphqlApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
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 { 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 { InvalidStringException } from 'src/engine/metadata-modules/errors/InvalidStringException';
|
||||
import { validateMetadataName } from 'src/engine/metadata-modules/utils/validate-metadata-name.utils';
|
||||
import {
|
||||
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 {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
@ -94,7 +95,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
|
||||
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) {
|
||||
@ -107,7 +111,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
!fieldMetadataInput.options &&
|
||||
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) {
|
||||
throw new ConflictException('Field already exists');
|
||||
throw new FieldMetadataException(
|
||||
'Field already exists',
|
||||
FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS,
|
||||
);
|
||||
}
|
||||
|
||||
const createdFieldMetadata = await fieldMetadataRepository.save({
|
||||
@ -183,7 +193,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
const workspaceQueryRunner = workspaceDataSource?.createQueryRunner();
|
||||
|
||||
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();
|
||||
@ -263,7 +276,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
});
|
||||
|
||||
if (!existingFieldMetadata) {
|
||||
throw new NotFoundException('Field does not exist');
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
@ -277,7 +293,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
|
||||
if (!objectMetadata) {
|
||||
throw new NotFoundException('Object does not exist');
|
||||
throw new FieldMetadataException(
|
||||
'Object metadata does not exist',
|
||||
FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadata);
|
||||
@ -287,15 +306,19 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
existingFieldMetadata.id &&
|
||||
fieldMetadataInput.isActive === false
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
throw new FieldMetadataException(
|
||||
'Cannot deactivate label identifier field',
|
||||
FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED,
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldMetadataInput.options) {
|
||||
for (const option of fieldMetadataInput.options) {
|
||||
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
|
||||
: null,
|
||||
});
|
||||
const updatedFieldMetadata = await fieldMetadataRepository.findOneOrFail({
|
||||
|
||||
const updatedFieldMetadata = await fieldMetadataRepository.findOne({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!updatedFieldMetadata) {
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
fieldMetadataInput.name ||
|
||||
updatableFieldInput.options ||
|
||||
@ -392,7 +423,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
});
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new NotFoundException('Field does not exist');
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadata =
|
||||
@ -403,7 +437,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
});
|
||||
|
||||
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);
|
||||
@ -454,7 +491,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
});
|
||||
|
||||
if (!fieldMetadata) {
|
||||
throw new NotFoundException('Field does not exist');
|
||||
throw new FieldMetadataException(
|
||||
'Field does not exist',
|
||||
FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata;
|
||||
@ -517,9 +557,12 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
relationMetadata.relationType === RelationMetadataType.MANY_TO_MANY ||
|
||||
relationMetadata.relationType === RelationMetadataType.MANY_TO_ONE
|
||||
) {
|
||||
throw new Error(`
|
||||
throw new FieldMetadataException(
|
||||
`
|
||||
Relation type ${relationMetadata.relationType} not supported
|
||||
`);
|
||||
`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (isRelationFromSource) {
|
||||
@ -561,8 +604,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
validateMetadataName(fieldMetadataInput.name);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidStringException) {
|
||||
throw new BadRequestException(
|
||||
throw new FieldMetadataException(
|
||||
`Characters used in name "${fieldMetadataInput.name}" are not supported`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
|
||||
@ -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';
|
||||
|
||||
describe('serializeDefaultValue', () => {
|
||||
@ -15,8 +14,10 @@ describe('serializeDefaultValue', () => {
|
||||
expect(serializeDefaultValue('now')).toBe('now()');
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid dynamic default value type', () => {
|
||||
expect(() => serializeDefaultValue('invalid')).toThrow(BadRequestException);
|
||||
it('should throw FieldMetadataException for invalid dynamic default value type', () => {
|
||||
expect(() => serializeDefaultValue('invalid')).toThrow(
|
||||
FieldMetadataException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle string static default value', () => {
|
||||
|
||||
@ -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 { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import {
|
||||
FieldMetadataException,
|
||||
FieldMetadataExceptionCode,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
|
||||
type ComputeColumnNameOptions = { isForeignKey?: boolean };
|
||||
|
||||
@ -29,8 +33,9 @@ export function computeColumnName<T extends FieldMetadataType | 'default'>(
|
||||
}
|
||||
|
||||
if (isCompositeFieldMetadataType(fieldMetadataOrFieldName.type)) {
|
||||
throw new Error(
|
||||
`Cannot compute column name for composite field metadata type: ${fieldMetadataOrFieldName.type}`,
|
||||
throw new FieldMetadataException(
|
||||
`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)) {
|
||||
throw new Error(
|
||||
throw new FieldMetadataException(
|
||||
`Cannot compute composite column name for non-composite field metadata type: ${fieldMetadataOrFieldName.type}`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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 {
|
||||
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 { serializeFunctionDefaultValue } from 'src/engine/metadata-modules/field-metadata/utils/serialize-function-default-value.util';
|
||||
|
||||
@ -18,7 +20,10 @@ export const serializeDefaultValue = (
|
||||
serializeFunctionDefaultValue(defaultValue);
|
||||
|
||||
if (!serializedTypeDefaultValue) {
|
||||
throw new BadRequestException('Invalid default value');
|
||||
throw new FieldMetadataException(
|
||||
'Invalid default value',
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return serializedTypeDefaultValue;
|
||||
@ -51,5 +56,8 @@ export const serializeDefaultValue = (
|
||||
return `'${JSON.stringify(defaultValue)}'`;
|
||||
}
|
||||
|
||||
throw new BadRequestException(`Invalid default value "${defaultValue}"`);
|
||||
throw new FieldMetadataException(
|
||||
`Invalid default value "${defaultValue}"`,
|
||||
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,10 @@ import {
|
||||
FieldMetadataComplexOption,
|
||||
FieldMetadataDefaultOption,
|
||||
} 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';
|
||||
|
||||
@ -24,7 +28,10 @@ export const validateOptionsForType = (
|
||||
if (options === null) return true;
|
||||
|
||||
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)) {
|
||||
@ -39,7 +46,10 @@ export const validateOptionsForType = (
|
||||
|
||||
// Check if all options are unique
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user