Skip to content

Commit be1c2a5

Browse files
committed
feat: thread actor_id through job worker → fn-app → fn-runtime
- Add actor_id to JobRow interface (worker reads from top-level column) - Send X-Actor-Id header from worker to cloud functions - Read and forward X-Actor-Id in fn-app (including callbacks) - Expose actorId in FunctionContext.job for function handlers - Update E2E test Job interface and addJob to use new 2-arg signature - Update CLAUDE.md SQL example for new add_job signature
1 parent faafc85 commit be1c2a5

8 files changed

Lines changed: 33 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,11 @@ Useful queries:
157157
-- Check pending jobs
158158
SELECT id, task_identifier, attempts, locked_by, last_error FROM app_jobs.jobs ORDER BY id;
159159

160-
-- Check database record (needed for job insertion)
161-
SELECT id, name FROM metaschema_public.database;
160+
-- Set JWT claims for job context (database_id is read internally by add_job)
161+
SELECT set_config('jwt.claims.database_id', (SELECT id::text FROM metaschema_public.database LIMIT 1), true);
162162

163163
-- Manually insert a test job
164164
SELECT * FROM app_jobs.add_job(
165-
(SELECT id FROM metaschema_public.database LIMIT 1),
166165
'simple-email'::text,
167166
'{"to":"test@example.com","subject":"test","html":"<p>hello</p>"}'::json
168167
);

job/worker/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface JobRow {
1111
task_identifier: string;
1212
payload?: unknown;
1313
database_id?: string;
14+
actor_id?: string;
1415
}
1516

1617
const log = new Logger('jobs:worker');
@@ -117,6 +118,7 @@ export default class Worker {
117118
await req(task_identifier, {
118119
body: payload,
119120
databaseId: job.database_id,
121+
actorId: job.actor_id,
120122
workerId: this.workerId,
121123
jobId: job.id
122124
});

job/worker/src/req.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import http from 'node:http';
22
import https from 'node:https';
33
import { URL } from 'node:url';
4+
45
import {
56
getCallbackBaseUrl,
67
getJobGatewayConfig,
@@ -30,14 +31,15 @@ const getFunctionUrl = (fn: string): string => {
3031

3132
interface RequestOptions {
3233
body: unknown;
33-
databaseId: string;
34+
databaseId?: string;
35+
actorId?: string;
3436
workerId: string;
3537
jobId: string | number;
3638
}
3739

3840
const request = (
3941
fn: string,
40-
{ body, databaseId, workerId, jobId }: RequestOptions
42+
{ body, databaseId, actorId, workerId, jobId }: RequestOptions
4143
) => {
4244
const url = getFunctionUrl(fn);
4345
log.info(`dispatching job`, {
@@ -73,7 +75,8 @@ const request = (
7375
// these are used by job-worker/job-fn
7476
'X-Worker-Id': workerId,
7577
'X-Job-Id': String(jobId),
76-
'X-Database-Id': databaseId,
78+
...(databaseId ? { 'X-Database-Id': databaseId } : {}),
79+
...(actorId ? { 'X-Actor-Id': actorId } : {}),
7780

7881
// async HTTP completion callback
7982
'X-Callback-Url': completeUrl

packages/fn-app/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ type JobContext = {
1313
workerId: string | undefined;
1414
jobId: string | undefined;
1515
databaseId: string | undefined;
16+
actorId: string | undefined;
1617
};
1718

1819
function getHeaders(req: any) {
1920
return {
2021
'x-worker-id': req.get('X-Worker-Id'),
2122
'x-job-id': req.get('X-Job-Id'),
2223
'x-database-id': req.get('X-Database-Id'),
24+
'x-actor-id': req.get('X-Actor-Id'),
2325
'x-callback-url': req.get('X-Callback-Url')
2426
};
2527
}
@@ -99,6 +101,10 @@ const sendJobCallback = async (
99101
headers['X-Database-Id'] = databaseId;
100102
}
101103

104+
if (ctx.actorId) {
105+
headers['X-Actor-Id'] = ctx.actorId;
106+
}
107+
102108
const body: Record<string, unknown> = {
103109
status
104110
};
@@ -168,6 +174,7 @@ const createJobApp = () => {
168174
'Content-Type': 'application/json',
169175
'X-Worker-Id': req.get('X-Worker-Id'),
170176
'X-Database-Id': req.get('X-Database-Id'),
177+
'X-Actor-Id': req.get('X-Actor-Id'),
171178
'X-Job-Id': req.get('X-Job-Id')
172179
});
173180
next();
@@ -179,7 +186,8 @@ const createJobApp = () => {
179186
callbackUrl: req.get('X-Callback-Url'),
180187
workerId: req.get('X-Worker-Id'),
181188
jobId: req.get('X-Job-Id'),
182-
databaseId: req.get('X-Database-Id')
189+
databaseId: req.get('X-Database-Id'),
190+
actorId: req.get('X-Actor-Id')
183191
};
184192

185193
// Store on res.locals so the error middleware can also mark callbacks as sent.

packages/fn-runtime/src/context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { FunctionContext } from './types';
44

55
type RequestHeaders = {
66
databaseId?: string;
7+
actorId?: string;
78
workerId?: string;
89
jobId?: string;
910
};
@@ -15,7 +16,7 @@ export const buildContext = (
1516
const env = process.env as Record<string, string | undefined>;
1617
const log = createLogger(options.name || 'fn-runtime');
1718

18-
const { databaseId, workerId, jobId } = headers;
19+
const { databaseId, actorId, workerId, jobId } = headers;
1920

2021
// Create GraphQL clients if databaseId is available and GRAPHQL_URL is set
2122
let client: FunctionContext['client'];
@@ -45,7 +46,7 @@ export const buildContext = (
4546
}
4647

4748
return {
48-
job: { jobId, workerId, databaseId },
49+
job: { jobId, workerId, databaseId, actorId },
4950
client,
5051
meta,
5152
log,

packages/fn-runtime/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const createFunctionServer = (
1313
const context = buildContext(
1414
{
1515
databaseId: req.get('X-Database-Id') || req.get('x-database-id') || process.env.DEFAULT_DATABASE_ID,
16+
actorId: req.get('X-Actor-Id') || req.get('x-actor-id'),
1617
workerId: req.get('X-Worker-Id') || req.get('x-worker-id'),
1718
jobId: req.get('X-Job-Id') || req.get('x-job-id')
1819
},

packages/fn-runtime/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type FunctionContext = {
1010
jobId?: string;
1111
workerId?: string;
1212
databaseId?: string;
13+
actorId?: string;
1314
};
1415
client: GraphQLClient;
1516
meta: GraphQLClient;

tests/e2e/utils/jobs.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import { TestClient } from './db';
22

33
export interface Job {
44
id: string;
5-
database_id: string;
5+
database_id: string | null;
6+
actor_id: string | null;
67
queue_name: string | null;
78
task_identifier: string;
89
payload: Record<string, any>;
910
priority: number;
1011
run_at: Date;
1112
attempts: number;
1213
max_attempts: number;
14+
is_available: boolean;
1315
key: string | null;
1416
last_error: string | null;
1517
locked_at: Date | null;
@@ -22,9 +24,13 @@ export async function addJob(
2224
taskIdentifier: string,
2325
payload: Record<string, any>
2426
): Promise<Job> {
27+
await pg.none(
28+
`SELECT set_config('jwt.claims.database_id', $1, true)`,
29+
[databaseId]
30+
);
2531
const job = await pg.oneOrNone<Job>(
26-
`SELECT * FROM app_jobs.add_job($1::uuid, $2::text, $3::json)`,
27-
[databaseId, taskIdentifier, JSON.stringify(payload)]
32+
`SELECT * FROM app_jobs.add_job($1::text, $2::json)`,
33+
[taskIdentifier, JSON.stringify(payload)]
2834
);
2935
if (!job) {
3036
throw new Error(`Failed to add job: ${taskIdentifier}`);

0 commit comments

Comments
 (0)