mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 23:37:21 +03:00
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:
parent
7adb5cc00d
commit
cdc51add7d
@ -57,8 +57,9 @@
|
|||||||
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
|
"@ptc-org/nestjs-query-typeorm": "4.2.1-alpha.2",
|
||||||
"@react-email/components": "0.0.12",
|
"@react-email/components": "0.0.12",
|
||||||
"@react-email/render": "0.0.10",
|
"@react-email/render": "0.0.10",
|
||||||
"@sentry/node": "^7.66.0",
|
"@sentry/node": "^7.98.0",
|
||||||
"@sentry/profiling-node": "^1.3.4",
|
"@sentry/profiling-node": "^1.3.4",
|
||||||
|
"@sentry/tracing": "^7.98.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bullmq": "^4.14.0",
|
"bullmq": "^4.14.0",
|
||||||
|
@ -2,8 +2,34 @@ import { CommandFactory } from 'nest-commander';
|
|||||||
|
|
||||||
import { CommandModule } from './command.module';
|
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() {
|
async function bootstrap() {
|
||||||
// TODO: inject our own logger service to handle the output (Sentry, etc.)
|
const errorHandler = (err: Error) => {
|
||||||
await CommandFactory.run(CommandModule, ['warn', 'error', 'log']);
|
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();
|
bootstrap();
|
||||||
|
@ -40,7 +40,7 @@ export class ApiRestQueryBuilderFactory {
|
|||||||
objectMetadataItems: ObjectMetadataEntity[];
|
objectMetadataItems: ObjectMetadataEntity[];
|
||||||
objectMetadataItem: ObjectMetadataEntity;
|
objectMetadataItem: ObjectMetadataEntity;
|
||||||
}> {
|
}> {
|
||||||
const workspace = await this.tokenService.validateToken(request);
|
const { workspace } = await this.tokenService.validateToken(request);
|
||||||
|
|
||||||
const objectMetadataItems =
|
const objectMetadataItems =
|
||||||
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
||||||
|
@ -192,7 +192,10 @@ export class TokenService {
|
|||||||
return !!token;
|
return !!token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateToken(request: Request): Promise<Workspace> {
|
async validateToken(request: Request): Promise<{
|
||||||
|
user?: User;
|
||||||
|
workspace: Workspace;
|
||||||
|
}> {
|
||||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@ -203,11 +206,11 @@ export class TokenService {
|
|||||||
this.environmentService.getAccessTokenSecret(),
|
this.environmentService.getAccessTokenSecret(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { workspace } = await this.jwtStrategy.validate(
|
const { user, workspace } = await this.jwtStrategy.validate(
|
||||||
decoded as JwtPayload,
|
decoded as JwtPayload,
|
||||||
);
|
);
|
||||||
|
|
||||||
return workspace;
|
return { user, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyLoginToken(loginToken: string): Promise<string> {
|
async verifyLoginToken(loginToken: string): Promise<string> {
|
||||||
|
@ -30,7 +30,7 @@ export class OpenApiService {
|
|||||||
let objectMetadataItems;
|
let objectMetadataItems;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspace = await this.tokenService.validateToken(request);
|
const { workspace } = await this.tokenService.validateToken(request);
|
||||||
|
|
||||||
objectMetadataItems =
|
objectMetadataItems =
|
||||||
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
await this.objectMetadataService.findManyWithinWorkspace(workspace.id);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { HttpException } from '@nestjs/common';
|
import { HttpException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
BaseGraphQLError,
|
BaseGraphQLError,
|
||||||
@ -21,20 +23,31 @@ const graphQLPredefinedExceptions = {
|
|||||||
export const handleExceptionAndConvertToGraphQLError = (
|
export const handleExceptionAndConvertToGraphQLError = (
|
||||||
exception: Error,
|
exception: Error,
|
||||||
exceptionHandlerService: ExceptionHandlerService,
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
user?: ExceptionHandlerUser,
|
||||||
): BaseGraphQLError => {
|
): BaseGraphQLError => {
|
||||||
handleException(exception, exceptionHandlerService);
|
handleException(exception, exceptionHandlerService, user);
|
||||||
|
|
||||||
return convertExceptionToGraphQLError(exception);
|
return convertExceptionToGraphQLError(exception);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const filterException = (exception: Error): boolean => {
|
||||||
|
if (exception instanceof HttpException && exception.getStatus() < 500) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const handleException = (
|
export const handleException = (
|
||||||
exception: Error,
|
exception: Error,
|
||||||
exceptionHandlerService: ExceptionHandlerService,
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
user?: ExceptionHandlerUser,
|
||||||
): void => {
|
): void => {
|
||||||
if (exception instanceof HttpException && exception.getStatus() < 500) {
|
if (filterException(exception)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
exceptionHandlerService.captureException(exception);
|
|
||||||
|
exceptionHandlerService.captureExceptions([exception], { user });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const convertExceptionToGraphQLError = (
|
export const convertExceptionToGraphQLError = (
|
||||||
@ -62,8 +75,11 @@ export const convertHttpExceptionToGraphql = (exception: HttpException) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
error.stack = exception.stack;
|
// Only show the stack trace in development mode
|
||||||
error.extensions['response'] = exception.getResponse();
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
error.stack = exception.stack;
|
||||||
|
error.extensions['response'] = exception.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
};
|
};
|
||||||
|
@ -9,11 +9,7 @@ import {
|
|||||||
import { GraphQLSchema, GraphQLError } from 'graphql';
|
import { GraphQLSchema, GraphQLError } from 'graphql';
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
||||||
import {
|
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
|
||||||
GraphQLSchemaWithContext,
|
|
||||||
YogaInitialContext,
|
|
||||||
maskError,
|
|
||||||
} from 'graphql-yoga';
|
|
||||||
|
|
||||||
import { TokenService } from 'src/core/auth/services/token.service';
|
import { TokenService } from 'src/core/auth/services/token.service';
|
||||||
import { CoreModule } from 'src/core/core.module';
|
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 { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util';
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
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()
|
@Injectable()
|
||||||
export class GraphQLConfigService
|
export class GraphQLConfigService
|
||||||
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
|
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
|
||||||
@ -36,33 +35,24 @@ export class GraphQLConfigService
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
createGqlOptions(): YogaDriverConfig {
|
createGqlOptions(): YogaDriverConfig {
|
||||||
const exceptionHandlerService = this.exceptionHandlerService;
|
|
||||||
const isDebugMode = this.environmentService.isDebugMode();
|
const isDebugMode = this.environmentService.isDebugMode();
|
||||||
const config: YogaDriverConfig = {
|
const config: YogaDriverConfig = {
|
||||||
context: ({ req }) => ({ req }),
|
context: ({ req }) => ({ req }),
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
include: [CoreModule],
|
include: [CoreModule],
|
||||||
maskedErrors: {
|
|
||||||
maskError(error: GraphQLError, message, isDev) {
|
|
||||||
if (error.originalError) {
|
|
||||||
return handleExceptionAndConvertToGraphQLError(
|
|
||||||
error.originalError,
|
|
||||||
exceptionHandlerService,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return maskError(error, message, isDev);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conditionalSchema: async (context) => {
|
conditionalSchema: async (context) => {
|
||||||
|
let user: User | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.tokenService.isTokenPresent(context.req)) {
|
if (!this.tokenService.isTokenPresent(context.req)) {
|
||||||
return new GraphQLSchema({});
|
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) {
|
} catch (error) {
|
||||||
if (error instanceof UnauthorizedException) {
|
if (error instanceof UnauthorizedException) {
|
||||||
throw new GraphQLError('Unauthenticated', {
|
throw new GraphQLError('Unauthenticated', {
|
||||||
@ -92,11 +82,22 @@ export class GraphQLConfigService
|
|||||||
throw handleExceptionAndConvertToGraphQLError(
|
throw handleExceptionAndConvertToGraphQLError(
|
||||||
error,
|
error,
|
||||||
this.exceptionHandlerService,
|
this.exceptionHandlerService,
|
||||||
|
user
|
||||||
|
? {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolvers: { JSON: GraphQLJSON },
|
resolvers: { JSON: GraphQLJSON },
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
useExceptionHandler({
|
||||||
|
exceptionHandlerService: this.exceptionHandlerService,
|
||||||
|
tokenService: this.tokenService,
|
||||||
|
}),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isDebugMode) {
|
if (isDebugMode) {
|
||||||
|
@ -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';
|
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
|
||||||
|
|
||||||
export class ExceptionHandlerConsoleDriver
|
export class ExceptionHandlerConsoleDriver
|
||||||
implements ExceptionHandlerDriverInterface
|
implements ExceptionHandlerDriverInterface
|
||||||
{
|
{
|
||||||
captureException(exception: unknown) {
|
captureExceptions(
|
||||||
|
exceptions: ReadonlyArray<any>,
|
||||||
|
options?: ExceptionHandlerOptions,
|
||||||
|
) {
|
||||||
console.group('Exception Captured');
|
console.group('Exception Captured');
|
||||||
console.error(exception);
|
console.info(options);
|
||||||
|
console.error(exceptions);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
captureMessage(message: string): void {
|
captureMessage(message: string, user?: ExceptionHandlerUser): void {
|
||||||
console.group('Message Captured');
|
console.group('Message Captured');
|
||||||
|
console.info(user);
|
||||||
console.info(message);
|
console.info(message);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { ProfilingIntegration } from '@sentry/profiling-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 {
|
import {
|
||||||
ExceptionHandlerDriverInterface,
|
ExceptionHandlerDriverInterface,
|
||||||
ExceptionHandlerSentryDriverFactoryOptions,
|
ExceptionHandlerSentryDriverFactoryOptions,
|
||||||
@ -30,11 +33,69 @@ export class ExceptionHandlerSentryDriver
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
captureException(exception: Error) {
|
captureExceptions(
|
||||||
Sentry.captureException(exception);
|
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) {
|
captureMessage(message: string, user?: ExceptionHandlerUser) {
|
||||||
Sentry.captureMessage(message);
|
Sentry.captureMessage(message, (scope) => {
|
||||||
|
if (user) {
|
||||||
|
scope.setUser({
|
||||||
|
id: user.id,
|
||||||
|
ip_address: user.ipAddress,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return scope;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
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()
|
@Injectable()
|
||||||
export class ExceptionHandlerService {
|
export class ExceptionHandlerService {
|
||||||
@ -11,7 +12,10 @@ export class ExceptionHandlerService {
|
|||||||
private driver: ExceptionHandlerDriverInterface,
|
private driver: ExceptionHandlerDriverInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
captureException(exception: unknown) {
|
captureExceptions(
|
||||||
this.driver.captureException(exception);
|
exceptions: ReadonlyArray<any>,
|
||||||
|
options?: ExceptionHandlerOptions,
|
||||||
|
): string[] {
|
||||||
|
return this.driver.captureExceptions(exceptions, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -1,4 +1,10 @@
|
|||||||
|
import { ExceptionHandlerOptions } from './exception-handler-options.interface';
|
||||||
|
import { ExceptionHandlerUser } from './exception-handler-user.interface';
|
||||||
|
|
||||||
export interface ExceptionHandlerDriverInterface {
|
export interface ExceptionHandlerDriverInterface {
|
||||||
captureException(exception: unknown): void;
|
captureExceptions(
|
||||||
captureMessage(message: string): void;
|
exceptions: ReadonlyArray<any>,
|
||||||
|
options?: ExceptionHandlerOptions,
|
||||||
|
): string[];
|
||||||
|
captureMessage(message: string, user?: ExceptionHandlerUser): void;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export interface ExceptionHandlerUser {
|
||||||
|
id?: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
email?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
@ -5,6 +5,7 @@ import * as bodyParser from 'body-parser';
|
|||||||
import { graphqlUploadExpress } from 'graphql-upload';
|
import { graphqlUploadExpress } from 'graphql-upload';
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import { useContainer } from 'class-validator';
|
import { useContainer } from 'class-validator';
|
||||||
|
import '@sentry/tracing';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
@ -15,14 +16,16 @@ import { EnvironmentService } from './integrations/environment/environment.servi
|
|||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
const app = await NestFactory.create(AppModule, {
|
const app = await NestFactory.create(AppModule, {
|
||||||
cors: true,
|
cors: true,
|
||||||
logger: process.env.DEBUG_MODE
|
bufferLogs: true,
|
||||||
? ['error', 'warn', 'log', 'verbose', 'debug']
|
|
||||||
: ['error', 'warn', 'log'],
|
|
||||||
});
|
});
|
||||||
|
const logger = app.get(LoggerService);
|
||||||
|
|
||||||
// Apply class-validator container so that we can use injection in validators
|
// Apply class-validator container so that we can use injection in validators
|
||||||
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
||||||
|
|
||||||
|
// Use our logger
|
||||||
|
app.useLogger(logger);
|
||||||
|
|
||||||
// Apply validation pipes globally
|
// Apply validation pipes globally
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { YogaDriverConfig } from '@graphql-yoga/nestjs';
|
import { YogaDriverConfig } from '@graphql-yoga/nestjs';
|
||||||
import { GraphQLError } from 'graphql';
|
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
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 { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.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 { MetadataModule } from 'src/metadata/metadata.module';
|
||||||
import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util';
|
import { renderApolloPlayground } from 'src/workspace/utils/render-apollo-playground.util';
|
||||||
|
|
||||||
export const metadataModuleFactory = async (
|
export const metadataModuleFactory = async (
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
exceptionHandlerService: ExceptionHandlerService,
|
exceptionHandlerService: ExceptionHandlerService,
|
||||||
|
tokenService: TokenService,
|
||||||
): Promise<YogaDriverConfig> => {
|
): Promise<YogaDriverConfig> => {
|
||||||
const config: YogaDriverConfig = {
|
const config: YogaDriverConfig = {
|
||||||
context: ({ req }) => ({ req }),
|
context: ({ req }) => ({ req }),
|
||||||
@ -21,20 +21,13 @@ export const metadataModuleFactory = async (
|
|||||||
return renderApolloPlayground({ path: 'metadata' });
|
return renderApolloPlayground({ path: 'metadata' });
|
||||||
},
|
},
|
||||||
resolvers: { JSON: GraphQLJSON },
|
resolvers: { JSON: GraphQLJSON },
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
useExceptionHandler({
|
||||||
|
exceptionHandlerService,
|
||||||
|
tokenService,
|
||||||
|
}),
|
||||||
|
],
|
||||||
path: '/metadata',
|
path: '/metadata',
|
||||||
maskedErrors: {
|
|
||||||
maskError(error: GraphQLError, message, isDev) {
|
|
||||||
if (error.originalError) {
|
|
||||||
return handleExceptionAndConvertToGraphQLError(
|
|
||||||
error.originalError,
|
|
||||||
exceptionHandlerService,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return maskError(error, message, isDev);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (environmentService.isDebugMode()) {
|
if (environmentService.isDebugMode()) {
|
||||||
|
@ -6,8 +6,10 @@ import { YogaDriverConfig, YogaDriver } from '@graphql-yoga/nestjs';
|
|||||||
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
||||||
import { metadataModuleFactory } from 'src/metadata/metadata.module-factory';
|
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 { 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 { DataSourceModule } from './data-source/data-source.module';
|
||||||
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
|
import { FieldMetadataModule } from './field-metadata/field-metadata.module';
|
||||||
@ -18,7 +20,8 @@ import { RelationMetadataModule } from './relation-metadata/relation-metadata.mo
|
|||||||
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
GraphQLModule.forRootAsync<YogaDriverConfig>({
|
||||||
driver: YogaDriver,
|
driver: YogaDriver,
|
||||||
useFactory: metadataModuleFactory,
|
useFactory: metadataModuleFactory,
|
||||||
inject: [EnvironmentService, ExceptionHandlerService],
|
imports: [AuthModule],
|
||||||
|
inject: [EnvironmentService, ExceptionHandlerService, TokenService],
|
||||||
}),
|
}),
|
||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
FieldMetadataModule,
|
FieldMetadataModule,
|
||||||
|
@ -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 { getJobClassName } from 'src/integrations/message-queue/utils/get-job-class-name.util';
|
||||||
import { QueueWorkerModule } from 'src/queue-worker.module';
|
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() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.createApplicationContext(QueueWorkerModule);
|
let exceptionHandlerService: ExceptionHandlerService | undefined;
|
||||||
|
let loggerService: LoggerService | undefined;
|
||||||
|
|
||||||
for (const queueName of Object.values(MessageQueue)) {
|
try {
|
||||||
const messageQueueService: MessageQueueService = app.get(queueName);
|
const app = await NestFactory.createApplicationContext(QueueWorkerModule, {
|
||||||
|
bufferLogs: true,
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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();
|
bootstrap();
|
||||||
|
@ -35,7 +35,6 @@ import {
|
|||||||
} from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job';
|
} from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job';
|
||||||
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
|
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
|
||||||
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
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 { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util';
|
||||||
|
|
||||||
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface';
|
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface';
|
||||||
@ -64,37 +63,28 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<IConnection<Record> | undefined> {
|
): Promise<IConnection<Record> | undefined> {
|
||||||
try {
|
const { workspaceId, objectMetadataItem } = options;
|
||||||
const { workspaceId, objectMetadataItem } = options;
|
const start = performance.now();
|
||||||
const start = performance.now();
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||||
args,
|
args,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, workspaceId);
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`query time: ${end - start} ms on query ${
|
`query time: ${end - start} ms on query ${
|
||||||
options.objectMetadataItem.nameSingular
|
options.objectMetadataItem.nameSingular
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.parseResult<IConnection<Record>>(
|
return this.parseResult<IConnection<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
} catch (exception) {
|
|
||||||
const error = handleExceptionAndConvertToGraphQLError(
|
|
||||||
exception,
|
|
||||||
this.exceptionHandlerService,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne<
|
async findOne<
|
||||||
@ -104,181 +94,138 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: FindOneResolverArgs<Filter>,
|
args: FindOneResolverArgs<Filter>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record | undefined> {
|
): Promise<Record | undefined> {
|
||||||
try {
|
if (!args.filter || Object.keys(args.filter).length === 0) {
|
||||||
if (!args.filter || Object.keys(args.filter).length === 0) {
|
throw new BadRequestException('Missing filter argument');
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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>(
|
async createMany<Record extends IRecord = IRecord>(
|
||||||
args: CreateManyResolverArgs<Record>,
|
args: CreateManyResolverArgs<Record>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record[] | undefined> {
|
): Promise<Record[] | undefined> {
|
||||||
try {
|
const { workspaceId, objectMetadataItem } = options;
|
||||||
const { workspaceId, objectMetadataItem } = options;
|
const query = await this.workspaceQueryBuilderFactory.createMany(
|
||||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
args,
|
||||||
args,
|
options,
|
||||||
options,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, workspaceId);
|
||||||
|
|
||||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'insertInto',
|
'insertInto',
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
await this.triggerWebhooks<Record>(
|
||||||
parsedResults,
|
parsedResults,
|
||||||
CallWebhookJobsJobOperation.create,
|
CallWebhookJobsJobOperation.create,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedResults;
|
return parsedResults;
|
||||||
} catch (exception) {
|
|
||||||
const error = handleExceptionAndConvertToGraphQLError(
|
|
||||||
exception,
|
|
||||||
this.exceptionHandlerService,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOne<Record extends IRecord = IRecord>(
|
async createOne<Record extends IRecord = IRecord>(
|
||||||
args: CreateOneResolverArgs<Record>,
|
args: CreateOneResolverArgs<Record>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record | undefined> {
|
): 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>(
|
async updateOne<Record extends IRecord = IRecord>(
|
||||||
args: UpdateOneResolverArgs<Record>,
|
args: UpdateOneResolverArgs<Record>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record | undefined> {
|
): Promise<Record | undefined> {
|
||||||
try {
|
const { workspaceId, objectMetadataItem } = options;
|
||||||
const { workspaceId, objectMetadataItem } = options;
|
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
args,
|
||||||
args,
|
options,
|
||||||
options,
|
);
|
||||||
);
|
const result = await this.execute(query, workspaceId);
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'update',
|
'update',
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
await this.triggerWebhooks<Record>(
|
||||||
parsedResults,
|
parsedResults,
|
||||||
CallWebhookJobsJobOperation.update,
|
CallWebhookJobsJobOperation.update,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedResults?.[0];
|
return parsedResults?.[0];
|
||||||
} catch (exception) {
|
|
||||||
const error = handleExceptionAndConvertToGraphQLError(
|
|
||||||
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> {
|
||||||
try {
|
const { workspaceId, objectMetadataItem } = options;
|
||||||
const { workspaceId, objectMetadataItem } = options;
|
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
args,
|
||||||
args,
|
options,
|
||||||
options,
|
);
|
||||||
);
|
const result = await this.execute(query, workspaceId);
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'deleteFrom',
|
'deleteFrom',
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
await this.triggerWebhooks<Record>(
|
||||||
parsedResults,
|
parsedResults,
|
||||||
CallWebhookJobsJobOperation.delete,
|
CallWebhookJobsJobOperation.delete,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedResults?.[0];
|
return parsedResults?.[0];
|
||||||
} catch (exception) {
|
|
||||||
const error = handleExceptionAndConvertToGraphQLError(
|
|
||||||
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> {
|
||||||
try {
|
const { workspaceId, objectMetadataItem } = options;
|
||||||
const { workspaceId, objectMetadataItem } = options;
|
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
args,
|
||||||
args,
|
options,
|
||||||
options,
|
);
|
||||||
);
|
const result = await this.execute(query, workspaceId);
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'update',
|
'update',
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
await this.triggerWebhooks<Record>(
|
||||||
parsedResults,
|
parsedResults,
|
||||||
CallWebhookJobsJobOperation.update,
|
CallWebhookJobsJobOperation.update,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedResults;
|
return parsedResults;
|
||||||
} catch (exception) {
|
|
||||||
const error = handleExceptionAndConvertToGraphQLError(
|
|
||||||
exception,
|
|
||||||
this.exceptionHandlerService,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteMany<
|
async deleteMany<
|
||||||
@ -288,35 +235,26 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: DeleteManyResolverArgs<Filter>,
|
args: DeleteManyResolverArgs<Filter>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record[] | undefined> {
|
): Promise<Record[] | undefined> {
|
||||||
try {
|
const { workspaceId, objectMetadataItem } = options;
|
||||||
const { workspaceId, objectMetadataItem } = options;
|
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
args,
|
||||||
args,
|
options,
|
||||||
options,
|
);
|
||||||
);
|
const result = await this.execute(query, workspaceId);
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'deleteFrom',
|
'deleteFrom',
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
await this.triggerWebhooks<Record>(
|
await this.triggerWebhooks<Record>(
|
||||||
parsedResults,
|
parsedResults,
|
||||||
CallWebhookJobsJobOperation.delete,
|
CallWebhookJobsJobOperation.delete,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedResults;
|
return parsedResults;
|
||||||
} catch (exception) {
|
|
||||||
const error = handleExceptionAndConvertToGraphQLError(
|
|
||||||
exception,
|
|
||||||
this.exceptionHandlerService,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
|
61
yarn.lock
61
yarn.lock
@ -11177,6 +11177,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sentry/browser@npm:6.19.7":
|
||||||
version: 6.19.7
|
version: 6.19.7
|
||||||
resolution: "@sentry/browser@npm:6.19.7"
|
resolution: "@sentry/browser@npm:6.19.7"
|
||||||
@ -11236,6 +11247,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sentry/hub@npm:6.19.7":
|
||||||
version: 6.19.7
|
version: 6.19.7
|
||||||
resolution: "@sentry/hub@npm:6.19.7"
|
resolution: "@sentry/hub@npm:6.19.7"
|
||||||
@ -11271,6 +11292,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sentry/profiling-node@npm:^1.3.4":
|
||||||
version: 1.3.4
|
version: 1.3.4
|
||||||
resolution: "@sentry/profiling-node@npm:1.3.4"
|
resolution: "@sentry/profiling-node@npm:1.3.4"
|
||||||
@ -11337,6 +11370,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sentry/types@npm:6.19.7":
|
||||||
version: 6.19.7
|
version: 6.19.7
|
||||||
resolution: "@sentry/types@npm:6.19.7"
|
resolution: "@sentry/types@npm:6.19.7"
|
||||||
@ -11358,6 +11400,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sentry/utils@npm:6.19.7":
|
||||||
version: 6.19.7
|
version: 6.19.7
|
||||||
resolution: "@sentry/utils@npm:6.19.7"
|
resolution: "@sentry/utils@npm:6.19.7"
|
||||||
@ -11386,6 +11435,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@sideway/address@npm:^4.1.3":
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
resolution: "@sideway/address@npm: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"
|
"@ptc-org/nestjs-query-typeorm": "npm:4.2.1-alpha.2"
|
||||||
"@react-email/components": "npm:0.0.12"
|
"@react-email/components": "npm:0.0.12"
|
||||||
"@react-email/render": "npm:0.0.10"
|
"@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/profiling-node": "npm:^1.3.4"
|
||||||
|
"@sentry/tracing": "npm:^7.98.0"
|
||||||
"@types/lodash.isempty": "npm:^4.4.7"
|
"@types/lodash.isempty": "npm:^4.4.7"
|
||||||
"@types/lodash.isobject": "npm:^3.0.7"
|
"@types/lodash.isobject": "npm:^3.0.7"
|
||||||
"@types/lodash.omit": "npm:^4.5.9"
|
"@types/lodash.omit": "npm:^4.5.9"
|
||||||
|
Loading…
Reference in New Issue
Block a user