Skip to content

Commit 1f71718

Browse files
authored
fix: consolidate contextProvider into headerProvider (#65)
1 parent 5b744d1 commit 1f71718

3 files changed

Lines changed: 65 additions & 30 deletions

File tree

packages/server/__tests__/openapi-loader.test.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,7 +1394,7 @@ paths:
13941394
});
13951395
});
13961396

1397-
describe('headerProvider and contextProvider', () => {
1397+
describe('headerProvider', () => {
13981398
it('should accept headerProvider option', async () => {
13991399
const spec = {
14001400
openapi: '3.0.0',
@@ -1416,10 +1416,48 @@ paths:
14161416
const apiGroup = await loadOpenAPI(path, { headerProvider });
14171417

14181418
expect(apiGroup.functions).toHaveLength(1);
1419-
// headerProvider is used at runtime, not at load time
14201419
});
14211420

1422-
it('should accept contextProvider option', async () => {
1421+
it('should pass requestContext directly to headerProvider', async () => {
1422+
const spec = {
1423+
openapi: '3.0.0',
1424+
info: { title: 'Test', version: '1.0.0' },
1425+
servers: [{ url: 'https://api.example.com' }],
1426+
paths: {
1427+
'/test': {
1428+
get: {
1429+
operationId: 'test',
1430+
responses: { '200': { description: 'OK' } },
1431+
},
1432+
},
1433+
},
1434+
};
1435+
1436+
const path = await writeSpec('header-provider-context.json', spec);
1437+
const headerProvider = vi.fn().mockResolvedValue({ Authorization: 'Bearer tok_123' });
1438+
1439+
const apiGroup = await loadOpenAPI(path, {
1440+
headerProvider,
1441+
baseURL: 'https://api.example.com',
1442+
});
1443+
1444+
const handler = apiGroup.functions![0].handler!;
1445+
const requestContext = { userId: 'user-1', headers: { 'x-tenant': 'acme' } };
1446+
1447+
try {
1448+
await handler({}, { requestContext, emit: () => {} });
1449+
} catch {
1450+
// fetch will fail, we only care about what headerProvider received
1451+
}
1452+
1453+
expect(headerProvider).toHaveBeenCalledTimes(1);
1454+
expect(headerProvider).toHaveBeenCalledWith(
1455+
{},
1456+
requestContext
1457+
);
1458+
});
1459+
1460+
it('should still accept deprecated contextProvider for backward compatibility', async () => {
14231461
const spec = {
14241462
openapi: '3.0.0',
14251463
info: { title: 'Test', version: '1.0.0' },

packages/server/src/graphql-loader.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,24 @@ import fs from 'node:fs/promises';
3131
* Dynamic header provider for GraphQL requests.
3232
*
3333
* @param params - The GraphQL query parameters
34-
* @param context - Optional context from contextProvider (e.g., auth tokens)
34+
* @param requestContext - Optional request context from the execution environment (e.g., user info, auth tokens)
3535
*/
3636
export type GraphQLAuthProvider = (
3737
params?: Record<string, any>,
38-
context?: Record<string, any>
38+
requestContext?: Record<string, any>
3939
) => Promise<Record<string, string>> | Record<string, string>;
4040

4141
/**
4242
* Resolve headers for GraphQL requests based on auth options
4343
* Priority: headerProvider > authProvider + auth > static headers
4444
* @param options - Load options including auth config
4545
* @param params - Optional request params passed to headerProvider for dynamic resolution
46-
* @param executionContext - Optional execution context passed from handler (contains requestContext)
46+
* @param requestContext - Optional request context from the execution environment (e.g., user info, auth tokens)
4747
*/
4848
async function resolveHeaders(
4949
options: LoadGraphQLOptions,
5050
params?: Record<string, any>,
51-
executionContext?: Record<string, any>
51+
requestContext?: Record<string, any>
5252
): Promise<Record<string, string>> {
5353
const headers: Record<string, string> = {};
5454

@@ -65,10 +65,11 @@ async function resolveHeaders(
6565
}
6666

6767
if (options.headerProvider) {
68-
const context = options.contextProvider
69-
? await options.contextProvider(executionContext)
70-
: undefined;
71-
const dynamicHeaders = await options.headerProvider(params, context);
68+
let ctx = requestContext;
69+
if (options.contextProvider) {
70+
ctx = await options.contextProvider(requestContext);
71+
}
72+
const dynamicHeaders = await options.headerProvider(params, ctx);
7273
Object.assign(headers, dynamicHeaders);
7374
}
7475

@@ -193,10 +194,11 @@ export interface LoadGraphQLOptions {
193194
headers?: Record<string, string>;
194195
/** Auth provider for dynamic credential resolution */
195196
authProvider?: AuthProvider;
196-
/** Dynamic header provider function - called before each request */
197+
/** Dynamic header provider function - called before each request with params and requestContext */
197198
headerProvider?: GraphQLAuthProvider;
198-
/** Context provider - called before each request to get current context (e.g., auth tokens).
199-
* Receives execution context which may contain requestContext from execute() call. */
199+
/**
200+
* @deprecated Use headerProvider instead, which now receives requestContext directly as its second parameter.
201+
*/
200202
contextProvider?: (
201203
executionContext?: Record<string, any>
202204
) => Record<string, any> | Promise<Record<string, any>>;
@@ -342,7 +344,7 @@ function convertFieldToFunction(
342344
description,
343345
inputSchema,
344346
outputSchema,
345-
handler: async (params: unknown, context?: { metadata?: Record<string, any> }) => {
347+
handler: async (params: unknown, context?: { metadata?: Record<string, any>; requestContext?: Record<string, unknown> }) => {
346348
const paramsObj = (params as Record<string, any>) || {};
347349
const customFields = paramsObj._fields;
348350

@@ -373,8 +375,7 @@ function convertFieldToFunction(
373375
context.metadata.graphql_variables = variables;
374376
}
375377

376-
// Pass execution context (including requestContext) to resolveHeaders
377-
const headers = await resolveHeaders(options, paramsObj, context);
378+
const headers = await resolveHeaders(options, paramsObj, context?.requestContext);
378379

379380
const fetchFn = options.fetcher || fetch;
380381
const response = await fetchFn(url, {

packages/server/src/openapi-loader.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -166,21 +166,18 @@ export interface LoadOpenAPIOptions {
166166

167167
/**
168168
* Dynamic header provider for per-request authentication (e.g., per-user OAuth).
169-
* Similar to GraphQL's headerProvider. Called before each API request.
169+
* Called before each API request with the request parameters and the execution's requestContext.
170170
* @param params - The request parameters
171-
* @param context - Optional context from contextProvider
171+
* @param requestContext - Optional request context from the execution environment (e.g., user info, auth tokens)
172172
* @returns Headers to add to the request
173173
*/
174174
headerProvider?: (
175175
params: Record<string, unknown> | undefined,
176-
context?: Record<string, unknown>
176+
requestContext?: Record<string, unknown>
177177
) => Promise<Record<string, string>> | Record<string, string>;
178178

179179
/**
180-
* Context provider to extract context from execution environment.
181-
* Similar to GraphQL's contextProvider. Called once per request.
182-
* @param executionContext - The execution context from ATP
183-
* @returns Context object passed to headerProvider
180+
* @deprecated Use headerProvider instead, which now receives requestContext directly as its second parameter.
184181
*/
185182
contextProvider?: (
186183
executionContext?: Record<string, unknown>
@@ -515,13 +512,12 @@ function convertOperation(
515512
'Content-Type': 'application/json',
516513
};
517514

518-
let context: Record<string, unknown> | undefined;
519-
if (options.contextProvider) {
520-
context = await options.contextProvider(handlerContext?.requestContext);
521-
}
522-
523515
if (options.headerProvider) {
524-
const dynamicHeaders = await options.headerProvider(input, context);
516+
let requestContext = handlerContext?.requestContext;
517+
if (options.contextProvider) {
518+
requestContext = await options.contextProvider(requestContext);
519+
}
520+
const dynamicHeaders = await options.headerProvider(input, requestContext);
525521
Object.assign(headers, dynamicHeaders);
526522
log.debug('Added headers from headerProvider', { keys: Object.keys(dynamicHeaders) });
527523
}

0 commit comments

Comments
 (0)