fix: ensure unique GraphQL schema caching per API key (#16411)

## Description

This PR fixes an issue where the GraphQL schema was being incorrectly
cached and shared across different API keys within the same workspace.
This resulted in the `createdBy` field (Actor) from the first API key's
request being erroneously attributed to subsequent requests made by
different API keys.

## Changes

- Updated the `@graphql-yoga/nestjs` patch to include the request's
`Authorization` header in the schema cache key generation logic.
- This ensures that every unique authentication token (and thus every
unique API key) generates a distinct cache entry, preventing schema
context collisions.

Closes #15093
This commit is contained in:
Abdul Rahman
2025-12-15 22:53:33 +05:30
committed by GitHub
parent 0ecb60f50b
commit 289e8bf1d4
2 changed files with 38 additions and 32 deletions
@@ -11,7 +11,7 @@ index 1684394..32602b3 100644
const common_1 = require("@nestjs/common");
const graphql_2 = require("@nestjs/graphql");
class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver {
+
+
+ schemaCache = new Map();
+
async start(options) {
@@ -26,7 +26,7 @@ index 1684394..32602b3 100644
const app = this.httpAdapterHost.httpAdapter.getInstance();
preStartHook?.(app);
// nest's logger doesnt have the info method
@@ -42,6 +46,46 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver {
@@ -42,6 +46,47 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver {
}
const yoga = (0, graphql_yoga_1.createYoga)({
...options,
@@ -34,14 +34,15 @@ index 1684394..32602b3 100644
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
+ const workspaceUserId = request.req.user?.id ?? 'anonymous'
+ const apiKeyId = request.req.apiKey?.id ?? 'no-api-key'
+ const url = request.req.baseUrl
+
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${url}-${workspaceCacheVersion}`
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${apiKeyId}-${url}-${workspaceCacheVersion}`
+
+ if(this.schemaCache.has(cacheKey)) {
+ return this.schemaCache.get(cacheKey)
+ }
+
+
+ const schemas = [];
+
+ if (options.schema) {
@@ -67,13 +68,13 @@ index 1684394..32602b3 100644
+ }
+
+ this.schemaCache.set(cacheKey, mergedSchemas)
+
+
+ return mergedSchemas;
+ },
graphqlEndpoint: options.path,
// disable logging by default
// however, if `true` use nest logger
@@ -54,11 +98,51 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver {
@@ -54,11 +98,52 @@ class AbstractYogaDriver extends graphql_2.AbstractGraphQLDriver {
this.yoga = yoga;
app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res }));
}
@@ -87,14 +88,15 @@ index 1684394..32602b3 100644
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
+ const workspaceUserId = request.req.user?.id ?? 'anonymous'
+ const apiKeyId = request.req.apiKey?.id ?? 'no-api-key'
+ const url = request.req.baseUrl
+
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${url}-${workspaceCacheVersion}`
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${apiKeyId}-${url}-${workspaceCacheVersion}`
+
+ if(this.schemaCache.has(cacheKey)) {
+ return this.schemaCache.get(cacheKey)
+ }
+
+
+ const schemas = [];
+
+ if (options.schema) {
@@ -120,7 +122,7 @@ index 1684394..32602b3 100644
+ }
+
+ this.schemaCache.set(cacheKey, mergedSchemas)
+
+
+ return mergedSchemas;
+ },
graphqlEndpoint: options.path,
@@ -141,9 +143,9 @@ index 7068c51..b8cbf9e 100644
+import { createYoga, filter, pipe } from 'graphql-yoga';
+import { __decorate } from "tslib";
export class AbstractYogaDriver extends AbstractGraphQLDriver {
+
+
+ schemaCache = new Map();
+
+
async start(options) {
const platformName = this.httpAdapterHost.httpAdapter.getType();
options = {
@@ -156,7 +158,7 @@ index 7068c51..b8cbf9e 100644
const app = this.httpAdapterHost.httpAdapter.getInstance();
preStartHook?.(app);
// nest's logger doesnt have the info method
@@ -39,6 +43,46 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
@@ -39,6 +43,47 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
}
const yoga = createYoga({
...options,
@@ -164,9 +166,10 @@ index 7068c51..b8cbf9e 100644
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
+ const workspaceUserId = request.req.user?.id ?? 'anonymous'
+ const apiKeyId = request.req.apiKey?.id ?? 'no-api-key'
+ const url = request.req.baseUrl
+
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${url}-${workspaceCacheVersion}`
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${apiKeyId}-${url}-${workspaceCacheVersion}`
+
+ if (this.schemaCache.has(cacheKey)) {
+ return this.schemaCache.get(cacheKey)
@@ -203,7 +206,7 @@ index 7068c51..b8cbf9e 100644
graphqlEndpoint: options.path,
// disable logging by default
// however, if `true` use nest logger
@@ -51,11 +95,51 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
@@ -51,11 +95,52 @@ export class AbstractYogaDriver extends AbstractGraphQLDriver {
this.yoga = yoga;
app.use(yoga.graphqlEndpoint, (req, res) => yoga(req, res, { req, res }));
}
@@ -217,9 +220,10 @@ index 7068c51..b8cbf9e 100644
+ const workspaceId = request.req.workspace?.id ?? 'anonymous'
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion ?? '0'
+ const workspaceUserId = request.req.user?.id ?? 'anonymous'
+ const apiKeyId = request.req.apiKey?.id ?? 'no-api-key'
+ const url = request.req.baseUrl
+
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${url}-${workspaceCacheVersion}`
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${apiKeyId}-${url}-${workspaceCacheVersion}`
+
+ if (this.schemaCache.has(cacheKey)) {
+ return this.schemaCache.get(cacheKey)
@@ -353,7 +357,7 @@ index ce142f6..10e17d2 100644
@@ -11,23 +12,31 @@ import {
SubscriptionConfig,
} from '@nestjs/graphql';
+export type YogaSchemaDefinition<TContext> =
+ | PromiseOrValue<GraphQLSchemaWithContext<TContext>>
+ | ((
@@ -361,7 +365,7 @@ index ce142f6..10e17d2 100644
+ ) => PromiseOrValue<GraphQLSchemaWithContext<TContext>>);
+
export type YogaDriverPlatform = 'express' | 'fastify';
export type YogaDriverServerContext<Platform extends YogaDriverPlatform> =
Platform extends 'fastify'
- ? {
@@ -380,7 +384,7 @@ index ce142f6..10e17d2 100644
+ req: ExpressRequest;
+ res: ExpressResponse;
+ };
export type YogaDriverServerOptions<Platform extends YogaDriverPlatform> = Omit<
YogaServerOptions<YogaDriverServerContext<Platform>, never>,
'context' | 'schema'
@@ -388,7 +392,7 @@ index ce142f6..10e17d2 100644
+> & {
+ conditionalSchema?: YogaSchemaDefinition<YogaDriverServerContext<Platform>> | undefined;
+};
export type YogaDriverServerInstance<Platform extends YogaDriverPlatform> = YogaServerInstance<
YogaDriverServerContext<Platform>,
@@ -53,6 +62,8 @@ export type YogaDriverSubscriptionConfig = {
@@ -398,28 +402,29 @@ index ce142f6..10e17d2 100644
+ schemaCache = new Map();
+
protected yoga!: YogaDriverServerInstance<Platform>;
public async start(options: YogaDriverConfig<Platform>) {
@@ -78,7 +89,7 @@ export abstract class AbstractYogaDriver<
}
protected registerExpress(
- options: YogaDriverConfig<'express'>,
+ { conditionalSchema, ...options }: YogaDriverConfig<'express'>,
{ preStartHook }: { preStartHook?: (app: Express) => void } = {},
) {
const app: Express = this.httpAdapterHost.httpAdapter.getInstance();
@@ -98,6 +109,40 @@ export abstract class AbstractYogaDriver<
@@ -98,6 +109,41 @@ export abstract class AbstractYogaDriver<
const yoga = createYoga<YogaDriverServerContext<'express'>>({
...options,
+ schema: async request => {
+ const workspaceId = request.req.workspace.id
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion
+ const workspaceUserId = request.req.user?.id ?? 'anonymous'
+ const apiKeyId = request.req.apiKey?.id ?? 'no-api-key'
+ const url = request.req.baseUrl
+
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${url}-${workspaceCacheVersion}`
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${apiKeyId}-${url}-${workspaceCacheVersion}`
+
+ if (this.schemaCache.has(cacheKey)) {
+ return this.schemaCache.get(cacheKey)
@@ -459,28 +464,29 @@ index ce142f6..10e17d2 100644
+ ? new LoggerWithInfo('YogaDriver')
+ : options.logging,
});
this.yoga = yoga as YogaDriverServerInstance<Platform>;
@@ -115,7 +160,7 @@ export abstract class AbstractYogaDriver<
}
protected registerFastify(
- options: YogaDriverConfig<'fastify'>,
+ { conditionalSchema, ...options }: YogaDriverConfig<'fastify'>,
{ preStartHook }: { preStartHook?: (app: FastifyInstance) => void } = {},
) {
const app: FastifyInstance = this.httpAdapterHost.httpAdapter.getInstance();
@@ -124,6 +169,40 @@ export abstract class AbstractYogaDriver<
@@ -124,6 +169,41 @@ export abstract class AbstractYogaDriver<
const yoga = createYoga<YogaDriverServerContext<'fastify'>>({
...options,
+ schema: async request => {
+ const workspaceId = request.req.workspace.id
+ const workspaceCacheVersion = request.req.workspaceMetadataVersion
+ const workspaceUserId = request.req.user?.id ?? 'anonymous'
+ const apiKeyId = request.req.apiKey?.id ?? 'no-api-key'
+ const url = request.req.baseUrl
+
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${url}-${workspaceCacheVersion}`
+ const cacheKey = `${workspaceId}-${workspaceUserId}-${apiKeyId}-${url}-${workspaceCacheVersion}`
+
+ if (this.schemaCache.has(cacheKey)) {
+ return this.schemaCache.get(cacheKey)
@@ -520,5 +526,5 @@ index ce142f6..10e17d2 100644
+ 'graphql-ws': true,
+ }
: options.subscriptions;
if (config['graphql-ws']) {
+2 -2
View File
@@ -7626,14 +7626,14 @@ __metadata:
"@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":
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=8f5028&locator=twenty-server%40workspace%3Apackages%2Ftwenty-server"
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=971f26&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: 10c0/503620747ab1e747ef61b21a794060f613dc86313f7452e86fcbef9ca722be262d83be0dc57e9add15172e0b6cfb6999fe5785419529e7c81663da5283b97169
checksum: 10c0/68ebaf195c93a6d31e22f91e5474cfb51675eac44a6c1a2ef7c856539e704dd9e7f8ea7cfd930a0ad37894edbcab6502b642f202d9f31f8f38b36c7d4d5add43
languageName: node
linkType: hard