feat: dynamic graphQL schema generation based on user workspace (#1725)
* wip: refacto and start creating custom resolver * feat: findMany & findUnique of a custom entity * feat: wip pagination * feat: initial metadata migration * feat: universal findAll with pagination * fix: clean small stuff in pagination * fix: test * fix: miss file * feat: rename custom into universal * feat: create metadata schema in default database * Multi-tenant db schemas POC fix tests and use query builders remove synchronize restore updatedAt remove unnecessary import use queryRunner fix camelcase add migrations for standard objects Multi-tenant db schemas POC fix tests and use query builders remove synchronize restore updatedAt remove unnecessary import use queryRunner fix camelcase add migrations for standard objects poc: conditional schema at runtime wip: try to create resolver in Nest.JS context fix * feat: wip add pg_graphql * feat: setup pg_graphql during database init * wip: dynamic resolver * poc: dynamic resolver and query using pg_graphql * feat: pg_graphql use ARG in Dockerfile * feat: clean findMany & findOne dynamic resolver * feat: get correct schema based on access token * fix: remove old file * fix: tests * fix: better comment * fix: e2e test not working, error format change due to yoga * remove typeorm entity generation + fix jwt + fix search_path + remove anon * fix conflict --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: corentin <corentin@twenty.com>
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GraphQLModule } from '@nestjs/graphql';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
|
||||
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { GraphQLError, GraphQLSchema } from 'graphql';
|
||||
import { ExtractJwt } from 'passport-jwt';
|
||||
import { TokenExpiredError, verify } from 'jsonwebtoken';
|
||||
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@ -15,24 +17,77 @@ import { PrismaModule } from './database/prisma.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { AbilityModule } from './ability/ability.module';
|
||||
import { TenantModule } from './tenant/tenant.module';
|
||||
import { SchemaGenerationService } from './tenant/schema-generation/schema-generation.service';
|
||||
import { EnvironmentService } from './integrations/environment/environment.service';
|
||||
import {
|
||||
JwtAuthStrategy,
|
||||
JwtPayload,
|
||||
} from './core/auth/strategies/jwt.auth.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
GraphQLModule.forRoot<ApolloDriverConfig>({
|
||||
playground: false,
|
||||
GraphQLModule.forRoot<YogaDriverConfig>({
|
||||
context: ({ req }) => ({ req }),
|
||||
driver: ApolloDriver,
|
||||
driver: YogaDriver,
|
||||
autoSchemaFile: true,
|
||||
resolvers: { JSON: GraphQLJSON },
|
||||
plugins: [ApolloServerPluginLandingPageLocalDefault()],
|
||||
formatError: (error: GraphQLError) => {
|
||||
error.extensions.stacktrace = undefined;
|
||||
return error;
|
||||
conditionalSchema: async (request) => {
|
||||
try {
|
||||
// Get the SchemaGenerationService from the AppModule
|
||||
const service = AppModule.moduleRef.get(SchemaGenerationService, {
|
||||
strict: false,
|
||||
});
|
||||
|
||||
// Get the JwtAuthStrategy from the AppModule
|
||||
const jwtStrategy = AppModule.moduleRef.get(JwtAuthStrategy, {
|
||||
strict: false,
|
||||
});
|
||||
|
||||
// Get the EnvironmentService from the AppModule
|
||||
const environmentService = AppModule.moduleRef.get(
|
||||
EnvironmentService,
|
||||
{
|
||||
strict: false,
|
||||
},
|
||||
);
|
||||
|
||||
// Extract JWT from the request
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request.req);
|
||||
|
||||
// If there is no token, return an empty schema
|
||||
if (!token) {
|
||||
return new GraphQLSchema({});
|
||||
}
|
||||
|
||||
// Verify and decode JWT
|
||||
const decoded = verify(
|
||||
token,
|
||||
environmentService.getAccessTokenSecret(),
|
||||
);
|
||||
|
||||
// Validate JWT
|
||||
const { workspace } = await jwtStrategy.validate(
|
||||
decoded as JwtPayload,
|
||||
);
|
||||
|
||||
const conditionalSchema = await service.generateSchema(workspace.id);
|
||||
|
||||
return conditionalSchema;
|
||||
} catch (error) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
throw new GraphQLError('Unauthenticated', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
csrfPrevention: false,
|
||||
resolvers: { JSON: GraphQLJSON },
|
||||
plugins: [],
|
||||
}),
|
||||
PrismaModule,
|
||||
HealthModule,
|
||||
@ -43,4 +98,10 @@ import { TenantModule } from './tenant/tenant.module';
|
||||
],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
static moduleRef: ModuleRef;
|
||||
|
||||
constructor(private moduleRef: ModuleRef) {
|
||||
AppModule.moduleRef = this.moduleRef;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user