9bc63a01c9
## 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" />
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
|
|
import { generateApplicationToken } from 'test/integration/metadata/suites/application/utils/generate-application-token.util';
|
|
|
|
describe('generateApplicationToken', () => {
|
|
let applicationId: string;
|
|
|
|
beforeAll(async () => {
|
|
const { data } = await findManyApplications({
|
|
expectToFail: false,
|
|
});
|
|
|
|
const application = data.findManyApplications[0];
|
|
|
|
expect(application).toBeDefined();
|
|
|
|
applicationId = application.id;
|
|
});
|
|
|
|
it('should generate an application token with admin access token', async () => {
|
|
const { data } = await generateApplicationToken({
|
|
applicationId,
|
|
expectToFail: false,
|
|
});
|
|
|
|
expect(data.generateApplicationToken).toBeDefined();
|
|
expect(data.generateApplicationToken.token).toBeDefined();
|
|
expect(typeof data.generateApplicationToken.token).toBe('string');
|
|
expect(data.generateApplicationToken.token.length).toBeGreaterThan(0);
|
|
expect(data.generateApplicationToken.expiresAt).toBeDefined();
|
|
});
|
|
|
|
it('should generate an application token with API key access token', async () => {
|
|
const { data } = await generateApplicationToken({
|
|
applicationId,
|
|
expectToFail: false,
|
|
token: API_KEY_ACCESS_TOKEN,
|
|
});
|
|
|
|
expect(data.generateApplicationToken).toBeDefined();
|
|
expect(data.generateApplicationToken.token).toBeDefined();
|
|
expect(typeof data.generateApplicationToken.token).toBe('string');
|
|
expect(data.generateApplicationToken.token.length).toBeGreaterThan(0);
|
|
expect(data.generateApplicationToken.expiresAt).toBeDefined();
|
|
});
|
|
|
|
it('should fail with a non-existent application id', async () => {
|
|
const { errors } = await generateApplicationToken({
|
|
applicationId: '00000000-0000-0000-0000-000000000000',
|
|
expectToFail: true,
|
|
});
|
|
|
|
expect(errors).toBeDefined();
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
});
|
|
});
|