1- import type { AnyFlow } from '@pgflow/dsl' ;
1+ import type { AnyFlow , FlowShape } from '@pgflow/dsl' ;
22import { 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+ / ^ \/ f l o w s \/ ( [ a - z A - Z 0 - 9 _ ] + ) \/ e n s u r e - c o m p i l e d $ /
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