mirror of
https://github.com/twentyhq/twenty.git
synced 2024-08-18 02:10:25 +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",
|
||||
"@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",
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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> {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
captureException(exception: unknown): void;
|
||||
captureMessage(message: string): void;
|
||||
captureExceptions(
|
||||
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 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({
|
||||
|
@ -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()) {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
61
yarn.lock
61
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user