send pg graphql exception to sentry + fix missing nullable for relations (#3101)

* Send pg_graphql errors to sentry

* Send pg_graphql errors to sentry

* fix

* fix

* fix

* fix relation nullable
This commit is contained in:
Weiko 2023-12-21 16:07:25 +01:00 committed by GitHub
parent e9bc13b5fa
commit 0d00e3d62d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 97 deletions

View File

@ -49,7 +49,7 @@ export const httpExceptionHandler = (
); );
} else { } else {
error = new BaseGraphQLError( error = new BaseGraphQLError(
exception.message, 'Internal Server Error',
exception.getStatus().toString(), exception.getStatus().toString(),
); );
} }

View File

@ -1,21 +1,37 @@
import { ModuleRef } from "@nestjs/core"; import { ModuleRef } from '@nestjs/core';
import { QueueJobOptions } from "src/integrations/message-queue/drivers/interfaces/job-options.interface";
import { MessageQueueDriver } from "src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface"; import { QueueJobOptions } from 'src/integrations/message-queue/drivers/interfaces/job-options.interface';
import { MessageQueueJob, MessageQueueJobData } from "src/integrations/message-queue/interfaces/message-queue-job.interface"; import { MessageQueueDriver } from 'src/integrations/message-queue/drivers/interfaces/message-queue-driver.interface';
import { MessageQueue } from "src/integrations/message-queue/message-queue.constants"; import {
import { MessageQueueModule } from "src/integrations/message-queue/message-queue.module"; MessageQueueJob,
import { getJobClassName } from "src/integrations/message-queue/utils/get-job-class-name.util"; MessageQueueJobData,
import { QueueWorkerModule } from "src/queue-worker.module"; } from 'src/integrations/message-queue/interfaces/message-queue-job.interface';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { getJobClassName } from 'src/integrations/message-queue/utils/get-job-class-name.util';
export class SyncDriver implements MessageQueueDriver { export class SyncDriver implements MessageQueueDriver {
constructor(private readonly jobsModuleRef: ModuleRef) {} constructor(private readonly jobsModuleRef: ModuleRef) {}
async add<T extends MessageQueueJobData>(_queueName: MessageQueue, jobName: string, data: T, _options?: QueueJobOptions | undefined): Promise<void> {
async add<T extends MessageQueueJobData>(
_queueName: MessageQueue,
jobName: string,
data: T,
_options?: QueueJobOptions | undefined,
): Promise<void> {
const jobClassName = getJobClassName(jobName); const jobClassName = getJobClassName(jobName);
const job: MessageQueueJob<MessageQueueJobData> = this.jobsModuleRef.get(jobClassName, { strict: true }); const job: MessageQueueJob<MessageQueueJobData> = this.jobsModuleRef.get(
jobClassName,
{ strict: true },
);
return await job.handle(data); return await job.handle(data);
} }
work<T>(queueName: MessageQueue, handler: ({ data, id }: { data: T; id: string; }) => void | Promise<void>) {
work<T>(
queueName: MessageQueue,
handler: ({ data, id }: { data: T; id: string }) => void | Promise<void>,
) {
return; return;
} }
} }

View File

@ -133,6 +133,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
isCustom: true, isCustom: true,
targetColumnMap: {}, targetColumnMap: {},
isActive: true, isActive: true,
isNullable: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
objectMetadataId: relationMetadataInput.fromObjectMetadataId, objectMetadataId: relationMetadataInput.fromObjectMetadataId,
workspaceId: relationMetadataInput.workspaceId, workspaceId: relationMetadataInput.workspaceId,
@ -150,6 +151,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
: relationMetadataInput.toName, : relationMetadataInput.toName,
}, },
isActive: true, isActive: true,
isNullable: true,
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
objectMetadataId: relationMetadataInput.toObjectMetadataId, objectMetadataId: relationMetadataInput.toObjectMetadataId,
workspaceId: relationMetadataInput.workspaceId, workspaceId: relationMetadataInput.workspaceId,
@ -167,6 +169,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
value: foreignKeyColumnName, value: foreignKeyColumnName,
}, },
isActive: true, isActive: true,
isNullable: true,
// Should not be visible on the front side // Should not be visible on the front side
isSystem: true, isSystem: true,
type: FieldMetadataType.UUID, type: FieldMetadataType.UUID,
@ -202,6 +205,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat
action: WorkspaceMigrationColumnActionType.CREATE, action: WorkspaceMigrationColumnActionType.CREATE,
columnName: foreignKeyColumnName, columnName: foreignKeyColumnName,
columnType: 'uuid', columnType: 'uuid',
isNullable: true,
}, },
], ],
}, },

View File

@ -1,4 +1,9 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { IConnection } from 'src/utils/pagination/interfaces/connection.interface'; import { IConnection } from 'src/utils/pagination/interfaces/connection.interface';
import { import {
@ -18,8 +23,10 @@ import {
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory'; import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory';
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface'; import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface';
import { import {
@ -34,6 +41,7 @@ export class WorkspaceQueryRunnerService {
constructor( constructor(
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory, private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly exceptionHandlerService: ExceptionHandlerService,
) {} ) {}
async findMany< async findMany<
@ -44,14 +52,23 @@ export class WorkspaceQueryRunnerService {
args: FindManyResolverArgs<Filter, OrderBy>, args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<Record> | undefined> { ): Promise<IConnection<Record> | undefined> {
const { workspaceId, targetTableName } = options; try {
const query = await this.workspaceQueryBuilderFactory.findMany( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.findMany(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const result = await this.execute(query, workspaceId);
return this.parseResult<IConnection<Record>>(result, targetTableName, ''); return this.parseResult<IConnection<Record>>(result, targetTableName, '');
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async findOne< async findOne<
@ -61,40 +78,58 @@ export class WorkspaceQueryRunnerService {
args: FindOneResolverArgs<Filter>, args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
if (!args.filter || Object.keys(args.filter).length === 0) { try {
throw new BadRequestException('Missing filter argument'); if (!args.filter || Object.keys(args.filter).length === 0) {
} throw new BadRequestException('Missing filter argument');
const { workspaceId, targetTableName } = options; }
const query = await this.workspaceQueryBuilderFactory.findOne( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.findOne(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const parsedResult = this.parseResult<IConnection<Record>>( const result = await this.execute(query, workspaceId);
result, const parsedResult = this.parseResult<IConnection<Record>>(
targetTableName, result,
'', targetTableName,
); '',
);
return parsedResult?.edges?.[0]?.node; return parsedResult?.edges?.[0]?.node;
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async createMany<Record extends IRecord = IRecord>( async createMany<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Record>, args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options; try {
const query = await this.workspaceQueryBuilderFactory.createMany( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.createMany(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( return this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'insertInto', 'insertInto',
)?.records; )?.records;
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async createOne<Record extends IRecord = IRecord>( async createOne<Record extends IRecord = IRecord>(
@ -110,54 +145,81 @@ export class WorkspaceQueryRunnerService {
args: UpdateOneResolverArgs<Record>, args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
const { workspaceId, targetTableName } = options; try {
const query = await this.workspaceQueryBuilderFactory.updateOne( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.updateOne(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( return this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'update', 'update',
)?.records?.[0]; )?.records?.[0];
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async deleteOne<Record extends IRecord = IRecord>( async deleteOne<Record extends IRecord = IRecord>(
args: DeleteOneResolverArgs, args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
const { workspaceId, targetTableName } = options; try {
const query = await this.workspaceQueryBuilderFactory.deleteOne( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.deleteOne(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( return this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'deleteFrom', 'deleteFrom',
)?.records?.[0]; )?.records?.[0];
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async updateMany<Record extends IRecord = IRecord>( async updateMany<Record extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Record>, args: UpdateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options; try {
const query = await this.workspaceQueryBuilderFactory.updateMany( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.updateMany(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( return this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'update', 'update',
)?.records; )?.records;
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async deleteMany< async deleteMany<
@ -167,18 +229,27 @@ export class WorkspaceQueryRunnerService {
args: DeleteManyResolverArgs<Filter>, args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> { ): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options; try {
const query = await this.workspaceQueryBuilderFactory.deleteMany( const { workspaceId, targetTableName } = options;
args, const query = await this.workspaceQueryBuilderFactory.deleteMany(
options, args,
); options,
const result = await this.execute(query, workspaceId); );
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( return this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'deleteFrom', 'deleteFrom',
)?.records; )?.records;
} catch (exception) {
const error = globalExceptionHandler(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
} }
async execute( async execute(
@ -214,12 +285,12 @@ export class WorkspaceQueryRunnerService {
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey]; const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
const errors = graphqlResult?.[0]?.resolve?.errors; const errors = graphqlResult?.[0]?.resolve?.errors;
if (Array.isArray(errors) && errors.length > 0) {
console.error(`GraphQL errors on ${command}${targetTableName}`, errors);
}
if (!result) { if (!result) {
throw new BadRequestException('Malformed result from GraphQL query'); throw new InternalServerErrorException(
`GraphQL errors on ${command}${targetTableName}: ${JSON.stringify(
errors,
)}`,
);
} }
return parseResult(result); return parseResult(result);