feat: wip server folder structure (#4573)
* feat: wip server folder structure * fix: merge * fix: wrong merge * fix: remove unused file * fix: comment * fix: lint * fix: merge * fix: remove console.log * fix: metadata graphql arguments broken
This commit is contained in:
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { YogaDriverServerContext } from '@graphql-yoga/nestjs';
|
||||
|
||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces/graphql-context.interface';
|
||||
|
||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateContextFactory {
|
||||
constructor(private readonly tokenService: TokenService) {}
|
||||
|
||||
async create(
|
||||
context: YogaDriverServerContext<'express'>,
|
||||
): Promise<GraphQLContext> {
|
||||
// Check if token is present in the request
|
||||
if (this.tokenService.isTokenPresent(context.req)) {
|
||||
const data = await this.tokenService.validateToken(context.req);
|
||||
|
||||
// Inject user and workspace into the context
|
||||
return { ...context, ...data };
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { CreateContextFactory } from './create-context.factory';
|
||||
|
||||
export const graphQLFactories = [CreateContextFactory];
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||
import { graphQLFactories } from 'src/engine/api/graphql/graphql-config/factories';
|
||||
|
||||
@Module({
|
||||
imports: [CoreEngineModule],
|
||||
providers: [...graphQLFactories],
|
||||
exports: [...graphQLFactories],
|
||||
})
|
||||
export class GraphQLConfigModule {}
|
||||
@ -0,0 +1,165 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
|
||||
import { GqlOptionsFactory } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
YogaDriverConfig,
|
||||
YogaDriverServerContext,
|
||||
} from '@graphql-yoga/nestjs';
|
||||
import { GraphQLSchema, GraphQLError } from 'graphql';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
||||
import { GraphQLSchemaWithContext, YogaInitialContext } from 'graphql-yoga';
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
|
||||
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
|
||||
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
|
||||
import { renderApolloPlayground } from 'src/engine/utils/render-apollo-playground.util';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { useExceptionHandler } from 'src/engine/integrations/exception-handler/hooks/use-exception-handler.hook';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
|
||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
||||
import { useSentryTracing } from 'src/engine/integrations/exception-handler/hooks/use-sentry-tracing';
|
||||
|
||||
import { CreateContextFactory } from './factories/create-context.factory';
|
||||
|
||||
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
|
||||
user?: User;
|
||||
workspace?: Workspace;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GraphQLConfigService
|
||||
implements GqlOptionsFactory<YogaDriverConfig<'express'>>
|
||||
{
|
||||
constructor(
|
||||
private readonly createContextFactory: CreateContextFactory,
|
||||
private readonly tokenService: TokenService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly moduleRef: ModuleRef,
|
||||
) {}
|
||||
|
||||
createGqlOptions(): YogaDriverConfig {
|
||||
const isDebugMode = this.environmentService.get('DEBUG_MODE');
|
||||
const plugins = [
|
||||
useThrottler({
|
||||
ttl: this.environmentService.get('API_RATE_LIMITING_TTL'),
|
||||
limit: this.environmentService.get('API_RATE_LIMITING_LIMIT'),
|
||||
identifyFn: (context) => {
|
||||
return context.user?.id ?? context.req.ip ?? 'anonymous';
|
||||
},
|
||||
}),
|
||||
useExceptionHandler({
|
||||
exceptionHandlerService: this.exceptionHandlerService,
|
||||
}),
|
||||
];
|
||||
|
||||
if (Sentry.isInitialized()) {
|
||||
plugins.push(useSentryTracing());
|
||||
}
|
||||
|
||||
const config: YogaDriverConfig = {
|
||||
context: (context) => this.createContextFactory.create(context),
|
||||
autoSchemaFile: true,
|
||||
include: [CoreEngineModule],
|
||||
conditionalSchema: async (context) => {
|
||||
let user: User | undefined;
|
||||
let workspace: Workspace | undefined;
|
||||
|
||||
try {
|
||||
if (!this.tokenService.isTokenPresent(context.req)) {
|
||||
return new GraphQLSchema({});
|
||||
}
|
||||
|
||||
const data = await this.tokenService.validateToken(context.req);
|
||||
|
||||
user = data.user;
|
||||
workspace = data.workspace;
|
||||
|
||||
return await this.createSchema(context, data);
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) {
|
||||
throw new GraphQLError('Unauthenticated', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof JsonWebTokenError) {
|
||||
//mockedUserJWT
|
||||
throw new GraphQLError('Unauthenticated', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof TokenExpiredError) {
|
||||
throw new GraphQLError('Unauthenticated', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw handleExceptionAndConvertToGraphQLError(
|
||||
error,
|
||||
this.exceptionHandlerService,
|
||||
user
|
||||
? {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
workspaceId: workspace?.id,
|
||||
workspaceDisplayName: workspace?.displayName,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
},
|
||||
resolvers: { JSON: GraphQLJSON },
|
||||
plugins: plugins,
|
||||
};
|
||||
|
||||
if (isDebugMode) {
|
||||
config.renderGraphiQL = () => {
|
||||
return renderApolloPlayground();
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async createSchema(
|
||||
context: YogaDriverServerContext<'express'> & YogaInitialContext,
|
||||
data: JwtData,
|
||||
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
|
||||
// Create a new contextId for each request
|
||||
const contextId = ContextIdFactory.create();
|
||||
|
||||
// Register the request in the contextId
|
||||
this.moduleRef.registerRequestByContextId(context.req, contextId);
|
||||
|
||||
// Resolve the WorkspaceSchemaFactory for the contextId
|
||||
const workspaceFactory = await this.moduleRef.resolve(
|
||||
WorkspaceSchemaFactory,
|
||||
contextId,
|
||||
{
|
||||
strict: false,
|
||||
},
|
||||
);
|
||||
|
||||
return await workspaceFactory.createGraphQLSchema(
|
||||
data.workspace.id,
|
||||
data.user?.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
import { getGraphQLRateLimiter } from 'graphql-rate-limit';
|
||||
import { Plugin } from '@envelop/core';
|
||||
import { useOnResolve } from '@envelop/on-resolve';
|
||||
|
||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
|
||||
|
||||
export class UnauthenticatedError extends Error {}
|
||||
|
||||
export type IdentifyFn<ContextType = ThrottlerContext> = (
|
||||
context: ContextType,
|
||||
) => string;
|
||||
|
||||
export type ThrottlerPluginOptions = {
|
||||
identifyFn: IdentifyFn;
|
||||
ttl?: number;
|
||||
limit?: number;
|
||||
transformError?: (message: string) => Error;
|
||||
onThrottlerError?: (event: {
|
||||
error: string;
|
||||
identifier: string;
|
||||
context: unknown;
|
||||
info: GraphQLResolveInfo;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
interface ThrottlerContext extends GraphQLContext {
|
||||
rateLimiterFn: ReturnType<typeof getGraphQLRateLimiter>;
|
||||
}
|
||||
|
||||
export const useThrottler = (
|
||||
options: ThrottlerPluginOptions,
|
||||
): Plugin<ThrottlerContext> => {
|
||||
const rateLimiterFn = getGraphQLRateLimiter({
|
||||
identifyContext: options.identifyFn,
|
||||
});
|
||||
|
||||
return {
|
||||
onPluginInit({ addPlugin }) {
|
||||
addPlugin(
|
||||
useOnResolve(async ({ args, root, context, info }) => {
|
||||
if (options.limit && options.ttl) {
|
||||
const id = options.identifyFn(context);
|
||||
|
||||
const errorMessage = await context.rateLimiterFn(
|
||||
{ parent: root, args, context, info },
|
||||
{
|
||||
max: options?.limit,
|
||||
window: `${options?.ttl}s`,
|
||||
message: interpolate('Too much request.', {
|
||||
id,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (errorMessage) {
|
||||
if (options.onThrottlerError) {
|
||||
options.onThrottlerError({
|
||||
error: errorMessage,
|
||||
identifier: id,
|
||||
context,
|
||||
info,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.transformError) {
|
||||
throw options.transformError(errorMessage);
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
async onContextBuilding({ extendContext }) {
|
||||
extendContext({
|
||||
rateLimiterFn,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function interpolate(message: string, args: { [key: string]: string }) {
|
||||
return message.replace(/\{{([^)]*)\}}/g, (_, key) => args[key.trim()]);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { YogaDriverServerContext } from '@graphql-yoga/nestjs';
|
||||
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export interface GraphQLContext extends YogaDriverServerContext<'express'> {
|
||||
user?: User;
|
||||
workspace?: Workspace;
|
||||
}
|
||||
Reference in New Issue
Block a user