Generate GQL schema based on applicationId (#17860)
## Add application-scoped GraphQL schema generation When an application token is used to authenticate, the `/graphql` schema is now dynamically filtered to only include entities belonging to that application (plus the Twenty Standard Application). This enables third-party applications and the SDK to introspect a schema that is relevant to their scope, rather than seeing the full workspace schema with all custom objects. ### Changes - **New `generateApplicationToken` mutation** on the `/metadata` endpoint, allowing callers to exchange an API key for an application-scoped JWT token - **Schema filtering by application** in `WorkspaceSchemaFactory` — when `request.application` is present (from an application token), flat entity maps are filtered by `[appId, standardAppId]` before schema generation - **Per-app caching** — both the Yoga in-memory cache and Redis cache now include the `appId` in their keys to avoid serving wrong schemas - **Consolidated `getSubFlatEntityMapsByApplicationIdsOrThrow`** — unified the single-ID and multi-ID filtering utilities into one - **Integration tests** covering token generation (admin + API key auth) and schema introspection filtering (standard app token excludes custom objects) Schema generated on seeds with applicationToken (see that pets is missing) <img width="782" height="994" alt="image" src="https://github.com/user-attachments/assets/82510031-0965-435d-bc26-77c9f5d74e1f" />
This commit is contained in:
+16
-4
@@ -91,9 +91,12 @@ export class WorkspaceCacheStorageService {
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
typeDefs: string,
|
||||
applicationId?: string,
|
||||
): Promise<void> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.set<string>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
typeDefs,
|
||||
TTL_ONE_WEEK,
|
||||
);
|
||||
@@ -102,9 +105,12 @@ export class WorkspaceCacheStorageService {
|
||||
getGraphQLTypeDefs(
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
applicationId?: string,
|
||||
): Promise<string | undefined> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLTypeDefs}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,9 +118,12 @@ export class WorkspaceCacheStorageService {
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
usedScalarNames: string[],
|
||||
applicationId?: string,
|
||||
): Promise<void> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.set<string[]>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
usedScalarNames,
|
||||
TTL_ONE_WEEK,
|
||||
);
|
||||
@@ -123,9 +132,12 @@ export class WorkspaceCacheStorageService {
|
||||
getGraphQLUsedScalarNames(
|
||||
workspaceId: string,
|
||||
metadataVersion: number,
|
||||
applicationId?: string,
|
||||
): Promise<string[] | undefined> {
|
||||
const applicationSuffix = applicationId ? `:${applicationId}` : '';
|
||||
|
||||
return this.cacheStorageService.get<string[]>(
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}`,
|
||||
`${METADATA_VERSIONED_WORKSPACE_CACHE_KEY.GraphQLUsedScalarNames}:${workspaceId}:${metadataVersion}${applicationSuffix}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user