Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions graphql/env/__tests__/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ describe('getEnvOptions', () => {
});
});

it('parses the API entity type environment variable', () => {
const result = getGraphQLEnvVars({
API_ENTITY_TYPE: 'platform'
});

expect(result.api?.entityType).toBe('platform');
});

it('accepts custom SMS provider names', () => {
const result = getGraphQLEnvVars({
SMS_PROVIDER: 'custom-sms-gateway'
Expand Down
4 changes: 3 additions & 1 deletion graphql/env/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial
API_META_SCHEMAS,
API_ANON_ROLE,
API_ROLE_NAME,
API_ENTITY_TYPE,

EMBEDDER_PROVIDER,
EMBEDDER_MODEL,
Expand Down Expand Up @@ -65,7 +66,8 @@ export const getGraphQLEnvVars = (env: NodeJS.ProcessEnv = process.env): Partial
...(API_EXPOSED_SCHEMAS && { exposedSchemas: API_EXPOSED_SCHEMAS.split(',').map(s => s.trim()) }),
...(API_META_SCHEMAS && { metaSchemas: API_META_SCHEMAS.split(',').map(s => s.trim()) }),
...(API_ANON_ROLE && { anonRole: API_ANON_ROLE }),
...(API_ROLE_NAME && { roleName: API_ROLE_NAME })
...(API_ROLE_NAME && { roleName: API_ROLE_NAME }),
...(API_ENTITY_TYPE && { entityType: API_ENTITY_TYPE })
},
...((EMBEDDER_PROVIDER || CHAT_PROVIDER) && {
llm: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Request } from 'express';

describe('graphile context entity attribution', () => {
const buildContext = (
req: Partial<Request>,
entityType?: string
): Record<string, string> => {
const context: Record<string, string> = {};

if (entityType && req.databaseId) {
context['jwt.claims.entity_id'] = req.databaseId;
context['jwt.claims.entity_type'] = entityType;
}

if (req.databaseId) {
context['jwt.claims.database_id'] = req.databaseId;
}

return context;
};
Comment on lines +4 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Test duplicates logic instead of exercising middleware

The new test graphile-entity-attribution.test.ts re-implements the claim-building logic in a local buildContext helper (graphile-entity-attribution.test.ts:4-20) that mirrors graphile.ts:218-221 and asserts only against that copy, never importing or invoking the real buildPreset context builder. Because the test and production code are separate copies, a regression in the middleware's attribution logic (wrong predicate, dropped claim, mis-ordered override) would leave these tests green, so the suite provides no protection for the behavior it claims to verify.

📋 Prompt for AI Agents

In graphql/server/src/middleware/graphile.ts, extract the context-building block (currently the if (req) { ... } that sets jwt.claims.entity_id/entity_type/database_id/api_id etc., around lines 217-243) into an exported pure helper such as export function buildJwtClaimContext(req, apiEntityType): Record<string,string>, and call it from the grafast context callback. Then rewrite graphql/server/src/middleware/__tests__/graphile-entity-attribution.test.ts to import and assert against that exported helper (ideally plus an integration test that drives a request through the middleware) instead of re-implementing the same logic in a local buildContext, so the test exercises the real production code path and catches regressions in the attribution logic.


it('sets the complete entity pair when configured and the request has a database', () => {
expect(buildContext({ databaseId: 'db-1' }, 'platform')).toEqual({
'jwt.claims.database_id': 'db-1',
'jwt.claims.entity_id': 'db-1',
'jwt.claims.entity_type': 'platform'
});
});

it('sets no entity claims when the API entity type is not configured', () => {
expect(buildContext({ databaseId: 'db-1' })).toEqual({
'jwt.claims.database_id': 'db-1'
});
});

it('sets no entity claims when the request has no database', () => {
expect(buildContext({}, 'platform')).toEqual({});
});
});
18 changes: 16 additions & 2 deletions graphql/server/src/middleware/graphile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ const buildPreset = (
roleName: string,
databaseSettings?: DatabaseSettings,
apiId?: string,
compute?: ComputeConfig
compute?: ComputeConfig,
apiEntityType?: string
): GraphileConfig.Preset => {
return {
extends: [createConstructivePreset(databaseSettings)],
Expand Down Expand Up @@ -214,6 +215,10 @@ const buildPreset = (
const context: Record<string, string> = {};

if (req) {
if (apiEntityType && req.databaseId) {
context['jwt.claims.entity_id'] = req.databaseId;
context['jwt.claims.entity_type'] = apiEntityType;
}
if (req.databaseId) {
context['jwt.claims.database_id'] = req.databaseId;
}
Expand Down Expand Up @@ -403,7 +408,16 @@ export const graphile = (opts: ConstructiveOptions): RequestHandler => {

// Create promise and store in in-flight map BEFORE try block
const compute = api.apiId ? await req.constructive?.useModule('compute') : undefined;
const preset = buildPreset(pool, schema || [], anonRole, roleName, api.databaseSettings, api.apiId, compute);
const preset = buildPreset(
pool,
schema || [],
anonRole,
roleName,
api.databaseSettings,
api.apiId,
compute,
opts.api?.entityType
);
const creationPromise = observeGraphileBuild(
{
cacheKey: key,
Expand Down
2 changes: 2 additions & 0 deletions graphql/types/src/graphile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface GraphileFeatureOptions {
export interface ApiOptions {
/** Database schemas to expose through the API */
exposedSchemas?: string[];
/** Entity type attributed to requests received through this API */
entityType?: string;
/** Anonymous role name for unauthenticated requests */
anonRole?: string;
/** Default role name for authenticated requests */
Expand Down
Loading