Skip to content

Commit 6ed062f

Browse files
committed
feat(edge-worker): add ensure-compiled endpoint to ControlPlane
1 parent 4cd34c0 commit 6ed062f

File tree

2 files changed

+485
-16
lines changed

2 files changed

+485
-16
lines changed

pkgs/edge-worker/src/control-plane/server.ts

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AnyFlow } from '@pgflow/dsl';
1+
import type { AnyFlow, FlowShape } from '@pgflow/dsl';
22
import { compileFlow } from '@pgflow/dsl';
33

44
/**
@@ -17,6 +17,40 @@ export interface ErrorResponse {
1717
message: string;
1818
}
1919

20+
/**
21+
* Response type for the /flows/:slug/ensure-compiled endpoint
22+
*/
23+
export interface EnsureCompiledResponse {
24+
status: 'compiled' | 'verified' | 'recompiled' | 'mismatch';
25+
differences: string[];
26+
}
27+
28+
/**
29+
* Request body for the /flows/:slug/ensure-compiled endpoint
30+
*/
31+
export interface EnsureCompiledRequest {
32+
shape: FlowShape;
33+
mode: 'development' | 'production';
34+
}
35+
36+
/**
37+
* SQL function interface for database operations
38+
* Compatible with the postgres library's tagged template interface
39+
*/
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
// deno-lint-ignore no-explicit-any
42+
export type SqlFunction = (strings: TemplateStringsArray, ...values: any[]) => Promise<any[]>;
43+
44+
/**
45+
* Options for configuring the ControlPlane handler
46+
*/
47+
export interface ControlPlaneOptions {
48+
/** SQL function for database operations (required for ensure-compiled endpoint) */
49+
sql?: SqlFunction;
50+
/** Secret key for authentication (required for ensure-compiled endpoint) */
51+
secretKey?: string;
52+
}
53+
2054
/**
2155
* Input type for flow registration - accepts array or object (for namespace imports)
2256
*/
@@ -54,13 +88,17 @@ function buildFlowRegistry(flows: AnyFlow[]): Map<string, AnyFlow> {
5488
/**
5589
* Creates a request handler for the ControlPlane HTTP API
5690
* @param flowsInput Array or object of flow definitions to register
91+
* @param options Optional configuration for database and authentication
5792
* @returns Request handler function
5893
*/
59-
export function createControlPlaneHandler(flowsInput: FlowInput) {
94+
export function createControlPlaneHandler(
95+
flowsInput: FlowInput,
96+
options?: ControlPlaneOptions
97+
) {
6098
const flows = normalizeFlowInput(flowsInput);
6199
const registry = buildFlowRegistry(flows);
62100

63-
return (req: Request): Response => {
101+
return (req: Request): Response | Promise<Response> => {
64102
const url = new URL(req.url);
65103

66104
// Supabase Edge Functions always include function name as first segment
@@ -75,6 +113,15 @@ export function createControlPlaneHandler(flowsInput: FlowInput) {
75113
return handleGetFlow(registry, slug);
76114
}
77115

116+
// Handle POST /flows/:slug/ensure-compiled
117+
const ensureCompiledMatch = pathname.match(
118+
/^\/flows\/([a-zA-Z0-9_]+)\/ensure-compiled$/
119+
);
120+
if (ensureCompiledMatch && req.method === 'POST') {
121+
const slug = ensureCompiledMatch[1];
122+
return handleEnsureCompiled(req, slug, options);
123+
}
124+
78125
// 404 for unknown routes
79126
return jsonResponse(
80127
{
@@ -146,3 +193,120 @@ function jsonResponse(data: unknown, status: number): Response {
146193
},
147194
});
148195
}
196+
197+
/**
198+
* Verifies authentication using apikey header
199+
*/
200+
function verifyAuth(request: Request, secretKey: string | undefined): boolean {
201+
if (!secretKey) return false;
202+
const apikey = request.headers.get('apikey');
203+
return apikey === secretKey;
204+
}
205+
206+
/**
207+
* Validates the ensure-compiled request body
208+
*/
209+
function validateEnsureCompiledBody(
210+
body: unknown
211+
): { valid: true; data: EnsureCompiledRequest } | { valid: false; error: string } {
212+
if (!body || typeof body !== 'object') {
213+
return { valid: false, error: 'Request body must be an object' };
214+
}
215+
216+
const { shape, mode } = body as Record<string, unknown>;
217+
218+
if (!shape || typeof shape !== 'object') {
219+
return { valid: false, error: 'Missing or invalid shape in request body' };
220+
}
221+
222+
if (mode !== 'development' && mode !== 'production') {
223+
return {
224+
valid: false,
225+
error: "Invalid mode: must be 'development' or 'production'",
226+
};
227+
}
228+
229+
return { valid: true, data: { shape: shape as FlowShape, mode } };
230+
}
231+
232+
/**
233+
* Handles POST /flows/:slug/ensure-compiled requests
234+
*/
235+
async function handleEnsureCompiled(
236+
request: Request,
237+
flowSlug: string,
238+
options?: ControlPlaneOptions
239+
): Promise<Response> {
240+
// Check if SQL is configured
241+
if (!options?.sql) {
242+
return jsonResponse(
243+
{
244+
error: 'Not Found',
245+
message: 'ensure-compiled endpoint requires SQL configuration',
246+
},
247+
404
248+
);
249+
}
250+
251+
// Verify authentication
252+
if (!verifyAuth(request, options.secretKey)) {
253+
return jsonResponse(
254+
{
255+
error: 'Unauthorized',
256+
message: 'Invalid or missing apikey header',
257+
},
258+
401
259+
);
260+
}
261+
262+
// Parse and validate request body
263+
let body: unknown;
264+
try {
265+
body = await request.json();
266+
} catch {
267+
return jsonResponse(
268+
{
269+
error: 'Bad Request',
270+
message: 'Invalid JSON in request body',
271+
},
272+
400
273+
);
274+
}
275+
276+
const validation = validateEnsureCompiledBody(body);
277+
if (!validation.valid) {
278+
return jsonResponse(
279+
{
280+
error: 'Bad Request',
281+
message: validation.error,
282+
},
283+
400
284+
);
285+
}
286+
287+
const { shape, mode } = validation.data;
288+
289+
// Call SQL function
290+
try {
291+
const [result] = await options.sql`
292+
SELECT pgflow.ensure_flow_compiled(
293+
${flowSlug},
294+
${JSON.stringify(shape)}::jsonb,
295+
${mode}
296+
) as result
297+
`;
298+
299+
const response = result.result as EnsureCompiledResponse;
300+
301+
return jsonResponse(response, response.status === 'mismatch' ? 409 : 200);
302+
} catch (error) {
303+
console.error('Error calling ensure_flow_compiled:', error);
304+
return jsonResponse(
305+
{
306+
error: 'Database Error',
307+
message: error instanceof Error ? error.message : 'Unknown error',
308+
},
309+
500
310+
);
311+
}
312+
}

0 commit comments

Comments
 (0)