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:
Jérémy M
2023-09-28 16:27:34 +02:00
committed by GitHub
parent 485bc64b4f
commit 629bdbbf50
35 changed files with 1860 additions and 124 deletions

View File

@ -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;
}
}