From 52f4c34cd6dcd26c8d8bf9de8fd3a61b7ebdbdf2 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 25 Apr 2024 14:01:32 +0200 Subject: [PATCH] Cache yoga conditional schema (#5170) In this PR I'm introducing a new patch on @graphql-yoga/nestjs package. This patch overrides a previous patch that was made to compute the conditionnal schema on each request, Here we use a cache map to compute only once per schema workspace cache version. This allows us to have sub 100ms query time. --- packages/twenty-server/package.json | 2 +- ...ql-yoga-nestjs-npm-2.1.0-cb509e6047.patch} | 249 ++++++++++++++---- yarn.lock | 8 +- 3 files changed, 210 insertions(+), 49 deletions(-) rename packages/twenty-server/patches/{@graphql-yoga+nestjs+2.1.0.patch => @graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch} (65%) diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 1a0d513bbf..03d320869d 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -14,7 +14,7 @@ "database:migrate:prod": "npx -y typeorm migration:run -d dist/src/database/typeorm/metadata/metadata.datasource && npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource" }, "dependencies": { - "@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch", + "@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch", "@nestjs/cache-manager": "^2.2.1", "@nestjs/devtools-integration": "^0.1.6", "@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch", diff --git a/packages/twenty-server/patches/@graphql-yoga+nestjs+2.1.0.patch b/packages/twenty-server/patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch similarity index 65% rename from packages/twenty-server/patches/@graphql-yoga+nestjs+2.1.0.patch rename to packages/twenty-server/patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch index f5999714d4..b545bd0742 100644 --- a/packages/twenty-server/patches/@graphql-yoga+nestjs+2.1.0.patch +++ b/packages/twenty-server/patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch @@ -1,16 +1,23 @@ diff --git a/dist/cjs/index.js b/dist/cjs/index.js -index 1684394..8a92c3c 100644 +index 16843949d8589a299d8195b0a349ac4dac0bacbf..21e7fe2bbcba36b04a274be9d2219fd38790b508 100644 --- a/dist/cjs/index.js +++ b/dist/cjs/index.js -@@ -5,6 +5,7 @@ const tslib_1 = require("tslib"); +@@ -3,10 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true }); + exports.YogaDriver = exports.AbstractYogaDriver = void 0; + const tslib_1 = require("tslib"); const graphql_1 = require("graphql"); ++const schema_1 = require("@graphql-tools/schema"); const graphql_yoga_1 = require("graphql-yoga"); const common_1 = require("@nestjs/common"); -+const schema_1 = require("@graphql-tools/schema"); const graphql_2 = require("@nestjs/graphql"); class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { ++ ++ schemaCache = new Map(); ++ async start(options) { -@@ -27,7 +28,7 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { + const platformName = this.httpAdapterHost.httpAdapter.getType(); + options = { +@@ -27,7 +31,7 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { async stop() { // noop } @@ -19,29 +26,47 @@ index 1684394..8a92c3c 100644 const app = this.httpAdapterHost.httpAdapter.getInstance(); preStartHook?.(app); // nest's logger doesnt have the info method -@@ -42,6 +43,21 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { +@@ -42,6 +46,39 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { } const yoga = (0, graphql_yoga_1.createYoga)({ ...options, + schema: async (request) => { ++ const workspaceId = request.req.workspace.id ++ const workspaceCacheVersion = request.req.cacheVersion ++ const url = request.req.baseUrl ++ ++ const cacheKey = `${workspaceId}-${workspaceCacheVersion}-${url}` ++ ++ if(this.schemaCache.has(cacheKey)) { ++ return this.schemaCache.get(cacheKey) ++ } ++ + const schemas = []; ++ + if (options.schema) { + schemas.push(options.schema); + } ++ + if (conditionalSchema) { + const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; + if (conditionalSchemaResult) { + schemas.push(conditionalSchemaResult); + } + } -+ return (0, schema_1.mergeSchemas)({ ++ ++ ++ const mergedSchemas = (0, schema_1.mergeSchemas)({ + schemas, + }); ++ ++ this.schemaCache.set(cacheKey, mergedSchemas) ++ ++ return mergedSchemas; + }, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use nest logger -@@ -54,11 +70,26 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { +@@ -54,11 +91,44 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver { this.yoga = yoga; app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res })); } @@ -52,36 +77,59 @@ index 1684394..8a92c3c 100644 const yoga = (0, graphql_yoga_1.createYoga)({ ...options, + schema: async (request) => { ++ const workspaceId = request.req.workspace.id ++ const workspaceCacheVersion = request.req.cacheVersion ++ const url = request.req.baseUrl ++ ++ const cacheKey = `${workspaceId}-${workspaceCacheVersion}-${url}` ++ ++ if(this.schemaCache.has(cacheKey)) { ++ return this.schemaCache.get(cacheKey) ++ } ++ + const schemas = []; ++ + if (options.schema) { + schemas.push(options.schema); + } ++ + if (conditionalSchema) { + const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; + if (conditionalSchemaResult) { + schemas.push(conditionalSchemaResult); + } + } -+ return (0, schema_1.mergeSchemas)({ ++ ++ ++ const mergedSchemas = (0, schema_1.mergeSchemas)({ + schemas, + }); ++ ++ this.schemaCache.set(cacheKey, mergedSchemas) ++ ++ return mergedSchemas; + }, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use fastify logger diff --git a/dist/esm/index.js b/dist/esm/index.js -index 7068c51..8ba5d2a 100644 +index 7068c519320b379917c46763cd280b1cdd3e48f0..418e1030373fc1e0fb85a932ac8da9b39f580570 100644 --- a/dist/esm/index.js +++ b/dist/esm/index.js -@@ -2,6 +2,7 @@ import { __decorate } from "tslib"; +@@ -2,8 +2,12 @@ import { __decorate } from "tslib"; import { printSchema } from 'graphql'; import { createYoga, filter, pipe } from 'graphql-yoga'; import { Injectable, Logger } from '@nestjs/common'; +import { mergeSchemas } from '@graphql-tools/schema'; import { AbstractGraphQLDriver, GqlSubscriptionService, } from '@nestjs/graphql'; export class AbstractYogaDriver extends AbstractGraphQLDriver { ++ ++ schemaCache = new Map(); ++ async start(options) { -@@ -24,7 +25,7 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver { + const platformName = this.httpAdapterHost.httpAdapter.getType(); + options = { +@@ -24,7 +28,7 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver { async stop() { // noop } @@ -90,29 +138,47 @@ index 7068c51..8ba5d2a 100644 const app = this.httpAdapterHost.httpAdapter.getInstance(); preStartHook?.(app); // nest's logger doesnt have the info method -@@ -39,6 +40,21 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver { +@@ -39,6 +43,39 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver { } const yoga = createYoga({ ...options, + schema: async (request) => { ++ const workspaceId = request.req.workspace.id ++ const workspaceCacheVersion = request.req.cacheVersion ++ const url = request.req.baseUrl ++ ++ const cacheKey = `${workspaceId}-${workspaceCacheVersion}-${url}` ++ ++ if (this.schemaCache.has(cacheKey)) { ++ return this.schemaCache.get(cacheKey) ++ } ++ + const schemas = []; ++ + if (options.schema) { -+ schemas.push(options.schema); ++ schemas.push(options.schema); + } ++ + if (conditionalSchema) { -+ const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; -+ if (conditionalSchemaResult) { -+ schemas.push(conditionalSchemaResult); -+ } ++ const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; ++ ++ if (conditionalSchemaResult) { ++ schemas.push(conditionalSchemaResult); + } -+ return mergeSchemas({ -+ schemas, ++ } ++ ++ const mergedSchemas = mergeSchemas({ ++ schemas, + }); ++ ++ this.schemaCache.set(cacheKey, mergedSchemas) ++ ++ return mergedSchemas; + }, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use nest logger -@@ -51,11 +67,26 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver { +@@ -51,11 +88,44 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver { this.yoga = yoga; app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res })); } @@ -123,25 +189,43 @@ index 7068c51..8ba5d2a 100644 const yoga = createYoga({ ...options, + schema: async (request) => { ++ const workspaceId = request.req.workspace.id ++ const workspaceCacheVersion = request.req.cacheVersion ++ const url = request.req.baseUrl ++ ++ const cacheKey = `${workspaceId}-${workspaceCacheVersion}-${url}` ++ ++ if (this.schemaCache.has(cacheKey)) { ++ return this.schemaCache.get(cacheKey) ++ } ++ + const schemas = []; ++ + if (options.schema) { -+ schemas.push(options.schema); ++ schemas.push(options.schema); + } ++ + if (conditionalSchema) { -+ const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; -+ if (conditionalSchemaResult) { -+ schemas.push(conditionalSchemaResult); -+ } ++ const conditionalSchemaResult = typeof conditionalSchema === 'function' ? await conditionalSchema(request) : await conditionalSchema; ++ ++ if (conditionalSchemaResult) { ++ schemas.push(conditionalSchemaResult); + } -+ return mergeSchemas({ -+ schemas, ++ } ++ ++ const mergedSchemas = mergeSchemas({ ++ schemas, + }); ++ ++ this.schemaCache.set(cacheKey, mergedSchemas) ++ ++ return mergedSchemas; + }, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use fastify logger diff --git a/dist/typings/index.d.cts b/dist/typings/index.d.cts -index 2c6a965..fd86dac 100644 +index 2c6a9656193392680121487c7147db459d6b69ab..2f2b59f0e311f0526a7cfdad97372229301aabd7 100644 --- a/dist/typings/index.d.cts +++ b/dist/typings/index.d.cts @@ -1,7 +1,8 @@ @@ -154,18 +238,19 @@ index 2c6a965..fd86dac 100644 export type YogaDriverPlatform = 'express' | 'fastify'; export type YogaDriverServerContext = Platform extends 'fastify' ? { req: FastifyRequest; -@@ -10,7 +11,9 @@ export type YogaDriverServerContext = Platf +@@ -10,7 +11,10 @@ export type YogaDriverServerContext = Platf req: ExpressRequest; res: ExpressResponse; }; -export type YogaDriverServerOptions = Omit, never>, 'context' | 'schema'>; ++ +export type YogaDriverServerOptions = Omit, never>, 'context' | 'schema'> & { + conditionalSchema?: YogaSchemaDefinition> | undefined; +}; export type YogaDriverServerInstance = YogaServerInstance, never>; export type YogaDriverConfig = GqlModuleOptions & YogaDriverServerOptions & { /** -@@ -26,10 +29,10 @@ export declare abstract class AbstractYogaDriver; start(options: YogaDriverConfig): Promise; stop(): Promise; @@ -179,7 +264,7 @@ index 2c6a965..fd86dac 100644 }): void; subscriptionWithFilter(instanceRef: unknown, filterFn: (payload: TPayload, variables: TVariables, context: TContext) => boolean | Promise, createSubscribeContext: Function): (args_0: TPayload, args_1: TVariables, args_2: TContext) => Promise>; diff --git a/dist/typings/index.d.ts b/dist/typings/index.d.ts -index 2c6a965..fd86dac 100644 +index 2c6a9656193392680121487c7147db459d6b69ab..fd86daccf3e5a93ff44b568c9793c16d761f4f53 100644 --- a/dist/typings/index.d.ts +++ b/dist/typings/index.d.ts @@ -1,7 +1,8 @@ @@ -217,7 +302,7 @@ index 2c6a965..fd86dac 100644 }): void; subscriptionWithFilter(instanceRef: unknown, filterFn: (payload: TPayload, variables: TVariables, context: TContext) => boolean | Promise, createSubscribeContext: Function): (args_0: TPayload, args_1: TVariables, args_2: TContext) => Promise>; diff --git a/src/index.ts b/src/index.ts -index ce142f6..cda4117 100644 +index ce142f61ede52499485b19d8af057f4cb828d0f7..5888d31cae1b7aca57ed0819209812ac941edabb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ @@ -225,7 +310,7 @@ index ce142f6..cda4117 100644 import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; -import { printSchema } from 'graphql'; -import { createYoga, filter, pipe, YogaServerInstance, YogaServerOptions } from 'graphql-yoga'; -+import { GraphQLSchema, printSchema } from 'graphql'; ++import { printSchema, GraphQLSchema } from 'graphql'; +import { createYoga, filter, pipe, YogaServerInstance, YogaServerOptions, GraphQLSchemaWithContext, PromiseOrValue, YogaInitialContext } from 'graphql-yoga'; import type { ExecutionParams } from 'subscriptions-transport-ws'; import { Injectable, Logger } from '@nestjs/common'; @@ -233,20 +318,37 @@ index ce142f6..cda4117 100644 import { AbstractGraphQLDriver, GqlModuleOptions, -@@ -11,6 +12,12 @@ import { +@@ -11,23 +12,31 @@ import { SubscriptionConfig, } from '@nestjs/graphql'; +export type YogaSchemaDefinition = + | PromiseOrValue> + | (( -+ context: TContext & YogaInitialContext, -+ ) => PromiseOrValue>); ++ context: TContext & YogaInitialContext, ++ ) => PromiseOrValue>); + export type YogaDriverPlatform = 'express' | 'fastify'; export type YogaDriverServerContext = -@@ -27,7 +34,9 @@ export type YogaDriverServerContext = + Platform extends 'fastify' +- ? { +- req: FastifyRequest; +- reply: FastifyReply; +- } +- : { +- req: ExpressRequest; +- res: ExpressResponse; +- }; ++ ? { ++ req: FastifyRequest; ++ reply: FastifyReply; ++ } ++ : { ++ req: ExpressRequest; ++ res: ExpressResponse; ++ }; + export type YogaDriverServerOptions = Omit< YogaServerOptions, never>, 'context' | 'schema' @@ -257,20 +359,39 @@ index ce142f6..cda4117 100644 export type YogaDriverServerInstance = YogaServerInstance< YogaDriverServerContext, -@@ -78,7 +87,7 @@ export abstract class AbstractYogaDriver< +@@ -53,6 +62,8 @@ export type YogaDriverSubscriptionConfig = { + export abstract class AbstractYogaDriver< + Platform extends YogaDriverPlatform, + > extends AbstractGraphQLDriver> { ++ schemaCache = new Map(); ++ + protected yoga!: YogaDriverServerInstance; + + public async start(options: YogaDriverConfig) { +@@ -78,7 +89,7 @@ export abstract class AbstractYogaDriver< } protected registerExpress( - options: YogaDriverConfig<'express'>, -+ { conditionalSchema, ...options}: YogaDriverConfig<'express'>, ++ { conditionalSchema, ...options }: YogaDriverConfig<'express'>, { preStartHook }: { preStartHook?: (app: Express) => void } = {}, ) { const app: Express = this.httpAdapterHost.httpAdapter.getInstance(); -@@ -98,6 +107,25 @@ export abstract class AbstractYogaDriver< +@@ -98,6 +109,39 @@ export abstract class AbstractYogaDriver< const yoga = createYoga>({ ...options, + schema: async request => { ++ const workspaceId = request.req.workspace.id ++ const workspaceCacheVersion = request.req.cacheVersion ++ const url = request.req.baseUrl ++ ++ const cacheKey = `${workspaceId}-${workspaceCacheVersion}-${url}` ++ ++ if (this.schemaCache.has(cacheKey)) { ++ return this.schemaCache.get(cacheKey) ++ } ++ + const schemas: GraphQLSchema[] = []; + + if (options.schema) { @@ -285,14 +406,29 @@ index ce142f6..cda4117 100644 + } + } + -+ return mergeSchemas({ ++ const mergedSchemas = mergeSchemas({ + schemas, + }); ++ ++ this.schemaCache.set(cacheKey, mergedSchemas) ++ ++ return mergedSchemas; + }, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use nest logger -@@ -115,7 +143,7 @@ export abstract class AbstractYogaDriver< +@@ -105,8 +149,8 @@ export abstract class AbstractYogaDriver< + options.logging == null + ? false + : options.logging +- ? new LoggerWithInfo('YogaDriver') +- : options.logging, ++ ? new LoggerWithInfo('YogaDriver') ++ : options.logging, + }); + + this.yoga = yoga as YogaDriverServerInstance; +@@ -115,7 +159,7 @@ export abstract class AbstractYogaDriver< } protected registerFastify( @@ -301,11 +437,21 @@ index ce142f6..cda4117 100644 { preStartHook }: { preStartHook?: (app: FastifyInstance) => void } = {}, ) { const app: FastifyInstance = this.httpAdapterHost.httpAdapter.getInstance(); -@@ -124,6 +152,25 @@ export abstract class AbstractYogaDriver< +@@ -124,6 +168,39 @@ export abstract class AbstractYogaDriver< const yoga = createYoga>({ ...options, + schema: async request => { ++ const workspaceId = request.req.workspace.id ++ const workspaceCacheVersion = request.req.cacheVersion ++ const url = request.req.baseUrl ++ ++ const cacheKey = `${workspaceId}-${workspaceCacheVersion}-${url}` ++ ++ if (this.schemaCache.has(cacheKey)) { ++ return this.schemaCache.get(cacheKey) ++ } ++ + const schemas: GraphQLSchema[] = []; + + if (options.schema) { @@ -320,10 +466,25 @@ index ce142f6..cda4117 100644 + } + } + -+ return mergeSchemas({ ++ const mergedSchemas = mergeSchemas({ + schemas, + }); ++ ++ this.schemaCache.set(cacheKey, mergedSchemas) ++ ++ return mergedSchemas; + }, graphqlEndpoint: options.path, // disable logging by default // however, if `true` use fastify logger +@@ -191,8 +268,8 @@ export class YogaDriver< + const config: SubscriptionConfig = + options.subscriptions === true + ? { +- 'graphql-ws': true, +- } ++ 'graphql-ws': true, ++ } + : options.subscriptions; + + if (config['graphql-ws']) { diff --git a/yarn.lock b/yarn.lock index 8ba667ee18..42f094a799 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6828,16 +6828,16 @@ __metadata: languageName: node linkType: hard -"@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch::locator=twenty-server%40workspace%3Apackages%2Ftwenty-server": +"@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch::locator=twenty-server%40workspace%3Apackages%2Ftwenty-server": version: 2.1.0 - resolution: "@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@npm%3A2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch::version=2.1.0&hash=25fc63&locator=twenty-server%40workspace%3Apackages%2Ftwenty-server" + resolution: "@graphql-yoga/nestjs@patch:@graphql-yoga/nestjs@npm%3A2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch::version=2.1.0&hash=6db821&locator=twenty-server%40workspace%3Apackages%2Ftwenty-server" peerDependencies: "@nestjs/common": ^10.0.0 "@nestjs/core": ^10.0.0 "@nestjs/graphql": ^12.0.0 graphql: ^15.0.0 || ^16.0.0 graphql-yoga: ^4.0.4 - checksum: 333501a04f79ef158cd92e240abecf1056ea12e8f63345758d6f79bbd88d923846fac44940765ae48a1b05d108107e3ba1539eea1092802c5bdf74bf166ea16a + checksum: 512ed39d8a0b9e238b31b0a9fc0a4ee2c8980f7f38f218635beede535bcec9fbf11731ff2f986c0d551a003094c6935748cee721d3a46339b3a6e0467a699e1d languageName: node linkType: hard @@ -46272,7 +46272,7 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-server@workspace:packages/twenty-server" dependencies: - "@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch" + "@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch" "@nestjs/cache-manager": "npm:^2.2.1" "@nestjs/cli": "npm:10.3.0" "@nestjs/devtools-integration": "npm:^0.1.6"