Skip to content

Commit 4ee9472

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/portable-functions
2 parents 0de3592 + 1dc38f5 commit 4ee9472

12 files changed

Lines changed: 79 additions & 38 deletions

File tree

.github/workflows/test-k8s-deployment.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ jobs:
139139
kubectl wait --for=condition=complete job/constructive-db \
140140
-n constructive-functions --timeout=180s
141141
142+
- name: Restart job service after DB ready
143+
run: |
144+
kubectl rollout restart deploy/knative-job-service -n constructive-functions
145+
kubectl rollout status deploy/knative-job-service -n constructive-functions --timeout=120s
146+
142147
- name: Port-forward postgres and run e2e tests
143148
env:
144149
PGHOST: localhost

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ const handler: FunctionHandler = async (params, context) => {
6868
// client/meta: GraphQL clients (tenant-scoped, created per-request)
6969
// log: structured logger
7070
// env: process.env
71-
// job: { jobId, workerId, databaseId }
71+
// job: { jobId, workerId, databaseId, actorId }
7272
return { complete: true };
7373
};
7474

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
);

functions/send-email-link/handler.ts

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,50 @@ import { send as sendPostmaster } from '@constructive-io/postmaster';
66
import { send as sendSmtp } from 'simple-smtp-server';
77
import { parseEnvBoolean } from '@pgpmjs/env';
88

9+
// Use plural connections with the `where` filter (graphile-connection-filter)
10+
// rather than `condition:` or singular-by-PK. The constructive server's
11+
// ConstructivePreset:
12+
// 1. Extends NoUniqueLookupPreset — disables singular root-field lookups
13+
// by unique constraint, including primary key (`database(id:)`,
14+
// `user(id:)`).
15+
// 2. Disables `PgConditionArgumentPlugin` — removes the built-in
16+
// `condition:` argument on connections.
17+
// The supported pattern is `where: { field: { equalTo: var } }`.
918
const GetUser = gql`
1019
query GetUser($userId: UUID!) {
11-
user(id: $userId) {
12-
username
13-
displayName
14-
profilePicture
20+
users(where: { id: { equalTo: $userId } }, first: 1) {
21+
nodes {
22+
username
23+
displayName
24+
profilePicture
25+
}
1526
}
1627
}
1728
`;
1829

1930
const GetDatabaseInfo = gql`
2031
query GetDatabaseInfo($databaseId: UUID!) {
21-
database(id: $databaseId) {
22-
sites {
23-
nodes {
24-
domains {
25-
nodes {
26-
subdomain
27-
domain
32+
databases(where: { id: { equalTo: $databaseId } }, first: 1) {
33+
nodes {
34+
sites {
35+
nodes {
36+
domains {
37+
nodes {
38+
subdomain
39+
domain
40+
}
2841
}
29-
}
30-
logo
31-
title
32-
siteThemes {
33-
nodes {
34-
theme
42+
logo
43+
title
44+
siteThemes {
45+
nodes {
46+
theme
47+
}
3548
}
36-
}
37-
siteModules(condition: { name: "legal_terms_module" }) {
38-
nodes {
39-
data
49+
siteModules(where: { name: { equalTo: "legal_terms_module" } }) {
50+
nodes {
51+
data
52+
}
4053
}
4154
}
4255
}
@@ -111,7 +124,7 @@ const sendEmailLink = async (
111124
databaseId
112125
});
113126

114-
const site = databaseInfo?.database?.sites?.nodes?.[0];
127+
const site = databaseInfo?.databases?.nodes?.[0]?.sites?.nodes?.[0];
115128
if (!site) {
116129
throw new Error('Site not found for database');
117130
}
@@ -177,7 +190,7 @@ const sendEmailLink = async (
177190
const inviter = await client.request<any>(GetUser, {
178191
userId: params.sender_id
179192
});
180-
inviterName = inviter?.user?.displayName;
193+
inviterName = inviter?.users?.nodes?.[0]?.displayName;
181194

182195
if (inviterName) {
183196
subject = `${inviterName} invited you to ${nick}!`;

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 { createClients } from './graphql';
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/graphql.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,14 @@ export const createClients = (
6161
...(schemata && { schemata })
6262
});
6363

64+
// Meta client targets platform-level data (databases, sites, domains, themes
65+
// in metaschema_public). It must NOT use tenant API routing — sending
66+
// X-Api-Name causes the server to load every schema registered for that API
67+
// (both *_public and *_private variants), which collides on duplicate codec
68+
// names like identityProviders. Use X-Meta-Schema instead.
6469
const meta = createGraphQLClient(metaGraphqlUrl, env, {
6570
hostHeaderEnvVar: 'META_GRAPHQL_HOST_HEADER',
66-
databaseId,
67-
...(apiName && { apiName }),
68-
...(schemata && { schemata })
71+
useMetaSchema: true,
6972
});
7073

7174
return { client, meta };

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
},

0 commit comments

Comments
 (0)