feat: add user to sentry (#3467)

* feat: wip add user to sentry

* feat: wip interceptor

* feat: wip add user to sentry

* feat: add user into sentry errors

* fix: hide stack trace in production

* fix: properly log commands and handle exceptions

* fix: filter command exceptions

* feat: handle jobs errors
This commit is contained in:
Jérémy M 2024-02-01 16:14:08 +01:00 committed by GitHub
parent 7adb5cc00d
commit cdc51add7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 581 additions and 264 deletions

View File

@ -57,8 +57,9 @@
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
"@react-email/components": "0.0.12",
"@react-email/render": "0.0.10",
"@sentry/node": "^7.66.0",
"@sentry/node": "^7.98.0",
"@sentry/profiling-node": "^1.3.4",
"@sentry/tracing": "^7.98.0",
"axios": "^1.6.2",
"bcrypt": "^5.1.1",
"bullmq": "^4.14.0",

View File

@ -2,8 +2,34 @@ import { CommandFactory } from 'nest-commander';
import { CommandModule } from './command.module';
import { LoggerService } from './integrations/logger/logger.service';
import { ExceptionHandlerService } from './integrations/exception-handler/exception-handler.service';
import { filterException } from './filters/utils/global-exception-handler.util';
async function bootstrap() {
// TODO: inject our own logger service to handle the output (Sentry, etc.)
await CommandFactory.run(CommandModule, ['warn', 'error', 'log']);
const errorHandler = (err: Error) => {
loggerService.error(err?.message, err?.name);
if (filterException(err)) {
return;
}
exceptionHandlerService.captureExceptions([err]);
};
const app = await CommandFactory.createWithoutRunning(CommandModule, {
bufferLogs: true,
errorHandler,
serviceErrorHandler: errorHandler,
});
const loggerService = app.get(LoggerService);
const exceptionHandlerService = app.get(ExceptionHandlerService);
// Inject our logger
app.useLogger(loggerService);
await CommandFactory.runApplication(app);
app.close();
}
bootstrap();

View File

@ -40,7 +40,7 @@ export class ApiRestQueryBuilderFactory {
objectMetadataItems: ObjectMetadataEntity[];
objectMetadataItem: ObjectMetadataEntity;
}> {
const workspace = await this.tokenService.validateToken(request);
const { workspace } = await this.tokenService.validateToken(request);
const objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);

View File

@ -192,7 +192,10 @@ export class TokenService {
return !!token;
}
async validateToken(request: Request): Promise<Workspace> {
async validateToken(request: Request): Promise<{
user?: User;
workspace: Workspace;
}> {
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
if (!token) {
@ -203,11 +206,11 @@ export class TokenService {
this.environmentService.getAccessTokenSecret(),
);
const { workspace } = await this.jwtStrategy.validate(
const { user, workspace } = await this.jwtStrategy.validate(
decoded as JwtPayload,
);
return workspace;
return { user, workspace };
}
async verifyLoginToken(loginToken: string): Promise<string> {

View File

@ -30,7 +30,7 @@ export class OpenApiService {
let objectMetadataItems;
try {
const workspace = await this.tokenService.validateToken(request);
const { workspace } = await this.tokenService.validateToken(request);
objectMetadataItems =
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);

View File

@ -1,5 +1,7 @@
import { HttpException } from '@nestjs/common';
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
import {
AuthenticationError,
BaseGraphQLError,
@ -21,20 +23,31 @@ const graphQLPredefinedExceptions = {
export const handleExceptionAndConvertToGraphQLError = (
exception: Error,
exceptionHandlerService: ExceptionHandlerService,
user?: ExceptionHandlerUser,
): BaseGraphQLError => {
handleException(exception, exceptionHandlerService);
handleException(exception, exceptionHandlerService, user);
return convertExceptionToGraphQLError(exception);
};
export const filterException = (exception: Error): boolean => {
if (exception instanceof HttpException && exception.getStatus() < 500) {
return true;
}
return false;
};
export const handleException = (
exception: Error,
exceptionHandlerService: ExceptionHandlerService,
user?: ExceptionHandlerUser,
): void => {
if (exception instanceof HttpException && exception.getStatus() < 500) {
if (filterException(exception)) {
return;
}
exceptionHandlerService.captureException(exception);
exceptionHandlerService.captureExceptions([exception], { user });
};
export const convertExceptionToGraphQLError = (
@ -62,8 +75,11 @@ export const convertHttpExceptionToGraphql = (exception: HttpException) => {
);
}
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
// Only show the stack trace in development mode
if (process.env.NODE_ENV === 'development') {
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
}
return error;
};

View File

@ -9,11 +9,7 @@ import {
import { GraphQLSchema, GraphQLError } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import {
GraphQLSchemaWithContext,
YogaInitialContext,
maskError,
} from 'graphql-yoga';
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
import { TokenService } from 'src/core/auth/services/token.service';
import { CoreModule } from 'src/core/core.module';
@ -24,6 +20,9 @@ import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/globa
import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { useExceptionHandler } from './integrations/exception-handler/hooks/use-exception-handler.hook';
import { User } from './core/user/user.entity';
@Injectable()
export class GraphQLConfigService
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
@ -36,33 +35,24 @@ export class GraphQLConfigService
) {}
createGqlOptions(): YogaDriverConfig {
const exceptionHandlerService = this.exceptionHandlerService;
const isDebugMode = this.environmentService.isDebugMode();
const config: YogaDriverConfig = {
context: ({ req }) => ({ req }),
autoSchemaFile: true,
include: [CoreModule],
maskedErrors: {
maskError(error: GraphQLError, message, isDev) {
if (error.originalError) {
return handleExceptionAndConvertToGraphQLError(
error.originalError,
exceptionHandlerService,
);
}
return maskError(error, message, isDev);
},
},
conditionalSchema: async (context) => {
let user: User | undefined;
try {
if (!this.tokenService.isTokenPresent(context.req)) {
return new GraphQLSchema({});
}
const workspace = await this.tokenService.validateToken(context.req);
const data = await this.tokenService.validateToken(context.req);
return await this.createSchema(context, workspace);
user = data.user;
return await this.createSchema(context, data.workspace);
} catch (error) {
if (error instanceof UnauthorizedException) {
throw new GraphQLError('Unauthenticated', {
@ -92,11 +82,22 @@ export class GraphQLConfigService
throw handleExceptionAndConvertToGraphQLError(
error,
this.exceptionHandlerService,
user
? {
id: user.id,
email: user.email,
}
: undefined,
);
}
},
resolvers: { JSON: GraphQLJSON },
plugins: [],
plugins: [
useExceptionHandler({
exceptionHandlerService: this.exceptionHandlerService,
tokenService: this.tokenService,
}),
],
};
if (isDebugMode) {

View File

@ -1,16 +1,26 @@
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface';
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
export class ExceptionHandlerConsoleDriver
implements ExceptionHandlerDriverInterface
{
captureException(exception: unknown) {
captureExceptions(
exceptions: ReadonlyArray<any>,
options?: ExceptionHandlerOptions,
) {
console.group('Exception Captured');
console.error(exception);
console.info(options);
console.error(exceptions);
console.groupEnd();
return [];
}
captureMessage(message: string): void {
captureMessage(message: string, user?: ExceptionHandlerUser): void {
console.group('Message Captured');
console.info(user);
console.info(message);
console.groupEnd();
}

View File

@ -1,6 +1,9 @@
import * as Sentry from '@sentry/node';
import { ProfilingIntegration } from '@sentry/profiling-node';
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface';
import {
ExceptionHandlerDriverInterface,
ExceptionHandlerSentryDriverFactoryOptions,
@ -30,11 +33,69 @@ export class ExceptionHandlerSentryDriver
});
}
captureException(exception: Error) {
Sentry.captureException(exception);
captureExceptions(
exceptions: ReadonlyArray<any>,
options?: ExceptionHandlerOptions,
) {
const eventIds: string[] = [];
Sentry.withScope((scope) => {
if (options?.operation) {
scope.setTag('operation', options.operation.name);
scope.setTag('operationName', options.operation.name);
}
if (options?.document) {
scope.setExtra('document', options.document);
}
for (const exception of exceptions) {
const errorPath = (exception.path ?? [])
.map((v: string | number) => (typeof v === 'number' ? '$index' : v))
.join(' > ');
if (errorPath) {
scope.addBreadcrumb({
category: 'execution-path',
message: errorPath,
level: 'debug',
});
}
const eventId = Sentry.captureException(exception, {
fingerprint: [
'graphql',
errorPath,
options?.operation?.name,
options?.operation?.type,
],
contexts: {
GraphQL: {
operationName: options?.operation?.name,
operationType: options?.operation?.type,
},
},
});
eventIds.push(eventId);
}
});
return eventIds;
}
captureMessage(message: string) {
Sentry.captureMessage(message);
captureMessage(message: string, user?: ExceptionHandlerUser) {
Sentry.captureMessage(message, (scope) => {
if (user) {
scope.setUser({
id: user.id,
ip_address: user.ipAddress,
email: user.email,
username: user.username,
});
}
return scope;
});
}
}

View File

@ -1,8 +1,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface';
import { EXCEPTION_HANDLER_DRIVER } from './exception-handler.constants';
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
import { EXCEPTION_HANDLER_DRIVER } from 'src/integrations/exception-handler/exception-handler.constants';
@Injectable()
export class ExceptionHandlerService {
@ -11,7 +12,10 @@ export class ExceptionHandlerService {
private driver: ExceptionHandlerDriverInterface,
) {}
captureException(exception: unknown) {
this.driver.captureException(exception);
captureExceptions(
exceptions: ReadonlyArray<any>,
options?: ExceptionHandlerOptions,
): string[] {
return this.driver.captureExceptions(exceptions, options);
}
}

View File

@ -0,0 +1,150 @@
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
import * as express from 'express';
import {
getDocumentString,
handleStreamOrSingleExecutionResult,
OnExecuteDoneHookResultOnNextHook,
Plugin,
} from '@envelop/core';
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { TokenService } from 'src/core/auth/services/token.service';
import {
convertExceptionToGraphQLError,
filterException,
} from 'src/filters/utils/global-exception-handler.util';
export type ExceptionHandlerPluginOptions = {
/**
* The driver to use to handle exceptions.
*/
exceptionHandlerService: ExceptionHandlerService;
/**
* The token service to use to get the token from the request.
*/
tokenService: TokenService;
/**
* The key of the event id in the error's extension. `null` to disable.
* @default exceptionEventId
*/
eventIdKey?: string | null;
};
export const useExceptionHandler = <
PluginContext extends Record<string, any> = object,
>(
options: ExceptionHandlerPluginOptions,
): Plugin<PluginContext> => {
const exceptionHandlerService = options.exceptionHandlerService;
const tokenService = options.tokenService;
const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId';
function addEventId(
err: GraphQLError,
eventId: string | undefined | null,
): GraphQLError {
if (eventIdKey !== null && eventId) {
err.extensions[eventIdKey] = eventId;
}
return err;
}
return {
async onExecute({ args }) {
const rootOperation = args.document.definitions.find(
(o) => o.kind === Kind.OPERATION_DEFINITION,
) as OperationDefinitionNode;
const operationType = rootOperation.operation;
const document = getDocumentString(args.document, print);
const request = args.contextValue.req as express.Request;
const opName =
args.operationName ||
rootOperation.name?.value ||
'Anonymous Operation';
let user: ExceptionHandlerUser | undefined;
if (tokenService.isTokenPresent(request)) {
try {
const data = await tokenService.validateToken(request);
user = {
id: data.user?.id,
email: data.user?.email,
};
} catch {}
}
return {
onExecuteDone(payload) {
const handleResult: OnExecuteDoneHookResultOnNextHook<object> = ({
result,
setResult,
}) => {
if (result.errors && result.errors.length > 0) {
const exceptions = result.errors.reduce<{
filtered: any[];
unfiltered: any[];
}>(
(acc, err) => {
// Filter out exceptions that we don't want to be captured by exception handler
if (filterException(err)) {
acc.filtered.push(err);
} else {
acc.unfiltered.push(err);
}
return acc;
},
{
filtered: [],
unfiltered: [],
},
);
if (exceptions.unfiltered.length > 0) {
const eventIds = exceptionHandlerService.captureExceptions(
exceptions.unfiltered,
{
operation: {
name: opName,
type: operationType,
},
document,
user,
},
);
exceptions.unfiltered.map((err, i) =>
addEventId(err, eventIds?.[i]),
);
}
const concatenatedErrors = [
...exceptions.filtered,
...exceptions.unfiltered,
];
const errors = concatenatedErrors.map((err) => {
// Properly convert errors to GraphQLErrors
const graphQLError = convertExceptionToGraphQLError(
err.originalError,
);
return graphQLError;
});
setResult({
...result,
errors,
});
}
};
return handleStreamOrSingleExecutionResult(payload, handleResult);
},
};
},
};
};

View File

@ -1,4 +1,10 @@
import { ExceptionHandlerOptions } from './exception-handler-options.interface';
import { ExceptionHandlerUser } from './exception-handler-user.interface';
export interface ExceptionHandlerDriverInterface {
captureException(exception: unknown): void;
captureMessage(message: string): void;
captureExceptions(
exceptions: ReadonlyArray<any>,
options?: ExceptionHandlerOptions,
): string[];
captureMessage(message: string, user?: ExceptionHandlerUser): void;
}

View File

@ -0,0 +1,12 @@
import { OperationTypeNode } from 'graphql';
import { ExceptionHandlerUser } from './exception-handler-user.interface';
export interface ExceptionHandlerOptions {
operation?: {
type: OperationTypeNode;
name: string;
};
document?: string;
user?: ExceptionHandlerUser;
}

View File

@ -0,0 +1,6 @@
export interface ExceptionHandlerUser {
id?: string;
ipAddress?: string;
email?: string;
username?: string;
}

View File

@ -5,6 +5,7 @@ import * as bodyParser from 'body-parser';
import { graphqlUploadExpress } from 'graphql-upload';
import bytes from 'bytes';
import { useContainer } from 'class-validator';
import '@sentry/tracing';
import { AppModule } from './app.module';
@ -15,14 +16,16 @@ import { EnvironmentService } from './integrations/environment/environment.servi
const bootstrap = async () => {
const app = await NestFactory.create(AppModule, {
cors: true,
logger: process.env.DEBUG_MODE
? ['error', 'warn', 'log', 'verbose', 'debug']
: ['error', 'warn', 'log'],
bufferLogs: true,
});
const logger = app.get(LoggerService);
// Apply class-validator container so that we can use injection in validators
useContainer(app.select(AppModule), { fallbackOnErrors: true });
// Use our logger
app.useLogger(logger);
// Apply validation pipes globally
app.useGlobalPipes(
new ValidationPipe({

View File

@ -1,17 +1,17 @@
import { YogaDriverConfig } from '@graphql-yoga/nestjs';
import { GraphQLError } from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { maskError } from 'graphql-yoga';
import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/global-exception-handler.util';
import { TokenService } from 'src/core/auth/services/token.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { useExceptionHandler } from 'src/integrations/exception-handler/hooks/use-exception-handler.hook';
import { MetadataModule } from 'src/metadata/metadata.module';
import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util';
export const metadataModuleFactory = async (
environmentService: EnvironmentService,
exceptionHandlerService: ExceptionHandlerService,
tokenService: TokenService,
): Promise<YogaDriverConfig> => {
const config: YogaDriverConfig = {
context: ({ req }) => ({ req }),
@ -21,20 +21,13 @@ export const metadataModuleFactory = async (
return renderApolloPlayground({ path: 'metadata' });
},
resolvers: { JSON: GraphQLJSON },
plugins: [],
plugins: [
useExceptionHandler({
exceptionHandlerService,
tokenService,
}),
],
path: '/metadata',
maskedErrors: {
maskError(error: GraphQLError, message, isDev) {
if (error.originalError) {
return handleExceptionAndConvertToGraphQLError(
error.originalError,
exceptionHandlerService,
);
}
return maskError(error, message, isDev);
},
},
};
if (environmentService.isDebugMode()) {

View File

@ -6,8 +6,10 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
import { metadataModuleFactory } from 'src/metadata/metadata.module-factory';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { TokenService } from 'src/core/auth/services/token.service';
import { AuthModule } from 'src/core/auth/auth.module';
import { DataSourceModule } from './data-source/data-source.module';
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
@ -18,7 +20,8 @@ import { RelationMetadataModule } from './relation-metadata/relation-metadata.mo
GraphQLModule.forRootAsync<YogaDriverConfig>({
driver: YogaDriver,
useFactory: metadataModuleFactory,
inject: [EnvironmentService, ExceptionHandlerService],
imports: [AuthModule],
inject: [EnvironmentService, ExceptionHandlerService, TokenService],
}),
DataSourceModule,
FieldMetadataModule,

View File

@ -11,20 +11,45 @@ import { MessageQueueService } from 'src/integrations/message-queue/services/mes
import { getJobClassName } from 'src/integrations/message-queue/utils/get-job-class-name.util';
import { QueueWorkerModule } from 'src/queue-worker.module';
import { LoggerService } from './integrations/logger/logger.service';
import { ExceptionHandlerService } from './integrations/exception-handler/exception-handler.service';
import { filterException } from './filters/utils/global-exception-handler.util';
async function bootstrap() {
const app = await NestFactory.createApplicationContext(QueueWorkerModule);
let exceptionHandlerService: ExceptionHandlerService | undefined;
let loggerService: LoggerService | undefined;
for (const queueName of Object.values(MessageQueue)) {
const messageQueueService: MessageQueueService = app.get(queueName);
await messageQueueService.work(async (jobData: MessageQueueJobData) => {
const jobClassName = getJobClassName(jobData.name);
const job: MessageQueueJob<MessageQueueJobData> = app
.select(JobsModule)
.get(jobClassName, { strict: true });
await job.handle(jobData.data);
try {
const app = await NestFactory.createApplicationContext(QueueWorkerModule, {
bufferLogs: true,
});
loggerService = app.get(LoggerService);
exceptionHandlerService = app.get(ExceptionHandlerService);
// Inject our logger
app.useLogger(loggerService!);
for (const queueName of Object.values(MessageQueue)) {
const messageQueueService: MessageQueueService = app.get(queueName);
await messageQueueService.work(async (jobData: MessageQueueJobData) => {
const jobClassName = getJobClassName(jobData.name);
const job: MessageQueueJob<MessageQueueJobData> = app
.select(JobsModule)
.get(jobClassName, { strict: true });
await job.handle(jobData.data);
});
}
} catch (err) {
loggerService?.error(err?.message, err?.name);
if (!filterException(err)) {
exceptionHandlerService?.captureExceptions([err]);
}
throw err;
}
}
bootstrap();

View File

@ -35,7 +35,6 @@ import {
} from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job';
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { handleExceptionAndConvertToGraphQLError } from 'src/filters/utils/global-exception-handler.util';
import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface';
@ -64,37 +63,28 @@ export class WorkspaceQueryRunnerService {
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<Record> | undefined> {
try {
const { workspaceId, objectMetadataItem } = options;
const start = performance.now();
const { workspaceId, objectMetadataItem } = options;
const start = performance.now();
const query = await this.workspaceQueryBuilderFactory.findMany(
args,
options,
);
const query = await this.workspaceQueryBuilderFactory.findMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
const end = performance.now();
const result = await this.execute(query, workspaceId);
const end = performance.now();
console.log(
`query time: ${end - start} ms on query ${
options.objectMetadataItem.nameSingular
}`,
);
console.log(
`query time: ${end - start} ms on query ${
options.objectMetadataItem.nameSingular
}`,
);
return this.parseResult<IConnection<Record>>(
result,
objectMetadataItem,
'',
);
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
return this.parseResult<IConnection<Record>>(
result,
objectMetadataItem,
'',
);
}
async findOne<
@ -104,181 +94,138 @@ export class WorkspaceQueryRunnerService {
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
try {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument');
}
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.findOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResult = this.parseResult<IConnection<Record>>(
result,
objectMetadataItem,
'',
);
return parsedResult?.edges?.[0]?.node;
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument');
}
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.findOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResult = this.parseResult<IConnection<Record>>(
result,
objectMetadataItem,
'',
);
return parsedResult?.edges?.[0]?.node;
}
async createMany<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
try {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.createMany(
args,
options,
);
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.createMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'insertInto',
)?.records;
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'insertInto',
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.create,
options,
);
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.create,
options,
);
return parsedResults;
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
return parsedResults;
}
async createOne<Record extends IRecord = IRecord>(
args: CreateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const results = await this.createMany({ data: [args.data] }, options);
await this.createMany({ data: [args.data] }, options);
return results?.[0];
throw new BadRequestException('Not implemented');
// return results?.[0];
}
async updateOne<Record extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
try {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.updateOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.updateOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
)?.records;
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.update,
options,
);
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.update,
options,
);
return parsedResults?.[0];
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
return parsedResults?.[0];
}
async deleteOne<Record extends IRecord = IRecord>(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
try {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteOne(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
)?.records;
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
return parsedResults?.[0];
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
return parsedResults?.[0];
}
async updateMany<Record extends IRecord = IRecord>(
args: UpdateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
try {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.updateMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.updateMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
)?.records;
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'update',
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.update,
options,
);
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.update,
options,
);
return parsedResults;
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
return parsedResults;
}
async deleteMany<
@ -288,35 +235,26 @@ export class WorkspaceQueryRunnerService {
args: DeleteManyResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
try {
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
const { workspaceId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.deleteMany(
args,
options,
);
const result = await this.execute(query, workspaceId);
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
)?.records;
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result,
objectMetadataItem,
'deleteFrom',
)?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
return parsedResults;
} catch (exception) {
const error = handleExceptionAndConvertToGraphQLError(
exception,
this.exceptionHandlerService,
);
return Promise.reject(error);
}
return parsedResults;
}
async execute(

View File

@ -11177,6 +11177,17 @@ __metadata:
languageName: node
linkType: hard
"@sentry-internal/tracing@npm:7.98.0":
version: 7.98.0
resolution: "@sentry-internal/tracing@npm:7.98.0"
dependencies:
"@sentry/core": "npm:7.98.0"
"@sentry/types": "npm:7.98.0"
"@sentry/utils": "npm:7.98.0"
checksum: f2547315376e3bf6dc2e06c823e67d3816260f9ee84b45e8bd1db4b73b9388b48afaf69d3bfec5772cf94b898550494a91301bd3b48e12eb8f54da8529b81ca0
languageName: node
linkType: hard
"@sentry/browser@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/browser@npm:6.19.7"
@ -11236,6 +11247,16 @@ __metadata:
languageName: node
linkType: hard
"@sentry/core@npm:7.98.0":
version: 7.98.0
resolution: "@sentry/core@npm:7.98.0"
dependencies:
"@sentry/types": "npm:7.98.0"
"@sentry/utils": "npm:7.98.0"
checksum: e5098bd47a1d05ca888134cc7d0d4ec1aabc51f319deba9f5d83c41c62927347d4b08539cc51fd3e3f6e242b62c8869ea89c468f80610194422c97d2015cdb0a
languageName: node
linkType: hard
"@sentry/hub@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/hub@npm:6.19.7"
@ -11271,6 +11292,18 @@ __metadata:
languageName: node
linkType: hard
"@sentry/node@npm:^7.98.0":
version: 7.98.0
resolution: "@sentry/node@npm:7.98.0"
dependencies:
"@sentry-internal/tracing": "npm:7.98.0"
"@sentry/core": "npm:7.98.0"
"@sentry/types": "npm:7.98.0"
"@sentry/utils": "npm:7.98.0"
checksum: 4de5dddd60937b0b72781d3f46cf3f075456377a8eb807425c18d4ea53a98c13b055d6b64f17ebcd7cb7d881111c31d855ad964591e4fcc804e6d729ad2503b3
languageName: node
linkType: hard
"@sentry/profiling-node@npm:^1.3.4":
version: 1.3.4
resolution: "@sentry/profiling-node@npm:1.3.4"
@ -11337,6 +11370,15 @@ __metadata:
languageName: node
linkType: hard
"@sentry/tracing@npm:^7.98.0":
version: 7.98.0
resolution: "@sentry/tracing@npm:7.98.0"
dependencies:
"@sentry-internal/tracing": "npm:7.98.0"
checksum: 678d7b023138bb3fa7bb95231ac9671e97f2b2fe1b03719dda021a0545d5345fbeb633d3ca4f7fba49ccf6e0534c3a6d1d62e03f32875ae13d083270257fe4fa
languageName: node
linkType: hard
"@sentry/types@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/types@npm:6.19.7"
@ -11358,6 +11400,13 @@ __metadata:
languageName: node
linkType: hard
"@sentry/types@npm:7.98.0":
version: 7.98.0
resolution: "@sentry/types@npm:7.98.0"
checksum: eafb7ff6a645a40f18cd35e6db08daae7aa86331c208f5b99e774cf2345eb4cb1c58a580d98dde0deb7b51c658fb1621e7f1d702772cb49bf8bfb82054228e9b
languageName: node
linkType: hard
"@sentry/utils@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/utils@npm:6.19.7"
@ -11386,6 +11435,15 @@ __metadata:
languageName: node
linkType: hard
"@sentry/utils@npm:7.98.0":
version: 7.98.0
resolution: "@sentry/utils@npm:7.98.0"
dependencies:
"@sentry/types": "npm:7.98.0"
checksum: c42148f32377dde69dd4bbe63ab7ac89a7e594f40ecd8bb3748675533f7d27112535cca4aa8103298ddb9a884d961fd50eced1040acbaa832141fcc66225d3e7
languageName: node
linkType: hard
"@sideway/address@npm:^4.1.3":
version: 4.1.4
resolution: "@sideway/address@npm:4.1.4"
@ -43158,8 +43216,9 @@ __metadata:
"@ptc-org/nestjs-query-typeorm": "npm:4.2.1-alpha.2"
"@react-email/components": "npm:0.0.12"
"@react-email/render": "npm:0.0.10"
"@sentry/node": "npm:^7.66.0"
"@sentry/node": "npm:^7.98.0"
"@sentry/profiling-node": "npm:^1.3.4"
"@sentry/tracing": "npm:^7.98.0"
"@types/lodash.isempty": "npm:^4.4.7"
"@types/lodash.isobject": "npm:^3.0.7"
"@types/lodash.omit": "npm:^4.5.9"