Skip to content

Commit 523aa71

Browse files
committed
add Installer.run() API for simplified Lovable integration
1 parent 4b6d099 commit 523aa71

File tree

7 files changed

+761
-149
lines changed

7 files changed

+761
-149
lines changed

pkgs/edge-worker/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export { FlowWorkerLifecycle } from './flow/FlowWorkerLifecycle.js';
99
// Export ControlPlane for HTTP-based flow compilation
1010
export { ControlPlane } from './control-plane/index.js';
1111

12+
// Export Installer for no-CLI platforms (e.g., Lovable)
13+
export { Installer } from './installer/index.js';
14+
1215
// Export platform adapters
1316
export * from './platform/index.js';
1417

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createInstallerHandler } from './server.ts';
2+
3+
export const Installer = {
4+
run: (token: string) => {
5+
const handler = createInstallerHandler(token);
6+
Deno.serve({}, handler);
7+
},
8+
};
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import type { InstallerResult, StepResult } from './types.ts';
2+
3+
// Simplified fetch type for dependency injection (allows easier mocking)
4+
type FetchFn = (
5+
url: string,
6+
init?: RequestInit
7+
) => Promise<Response>;
8+
9+
// Dependency injection for testability
10+
export interface InstallerDeps {
11+
fetch: FetchFn;
12+
getEnv: (key: string) => string | undefined;
13+
}
14+
15+
const defaultDeps: InstallerDeps = {
16+
fetch: globalThis.fetch,
17+
getEnv: (key) => Deno.env.get(key),
18+
};
19+
20+
async function callControlPlane(
21+
endpoint: string,
22+
method: 'GET' | 'POST',
23+
supabaseUrl: string,
24+
serviceRoleKey: string,
25+
fetchFn: FetchFn
26+
): Promise<StepResult> {
27+
try {
28+
const response = await fetchFn(
29+
`${supabaseUrl}/functions/v1/pgflow${endpoint}`,
30+
{
31+
method,
32+
headers: {
33+
Authorization: `Bearer ${serviceRoleKey}`,
34+
'Content-Type': 'application/json',
35+
},
36+
}
37+
);
38+
39+
const data = await response.json();
40+
return { success: response.ok, status: response.status, data };
41+
} catch (error) {
42+
return {
43+
success: false,
44+
status: 0,
45+
error: error instanceof Error ? error.message : 'Unknown error',
46+
};
47+
}
48+
}
49+
50+
export function createInstallerHandler(
51+
expectedToken: string,
52+
deps: InstallerDeps = defaultDeps
53+
): (req: Request) => Promise<Response> {
54+
return async (req: Request) => {
55+
// Validate token from query params first (fail fast)
56+
const url = new URL(req.url);
57+
const token = url.searchParams.get('token');
58+
59+
if (token !== expectedToken) {
60+
return jsonResponse(
61+
{
62+
success: false,
63+
message:
64+
'Invalid or missing token. Use the exact URL from your Lovable prompt.',
65+
},
66+
401
67+
);
68+
}
69+
70+
// Read env vars inside handler (not at module level)
71+
const supabaseUrl = deps.getEnv('SUPABASE_URL');
72+
const serviceRoleKey = deps.getEnv('SUPABASE_SERVICE_ROLE_KEY');
73+
74+
if (!supabaseUrl || !serviceRoleKey) {
75+
return jsonResponse(
76+
{
77+
success: false,
78+
message: 'Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY',
79+
},
80+
500
81+
);
82+
}
83+
84+
console.log('pgflow installer starting...');
85+
86+
// Step 1: Configure vault secrets
87+
console.log('Configuring vault secrets...');
88+
const secrets = await callControlPlane(
89+
'/secrets/configure',
90+
'POST',
91+
supabaseUrl,
92+
serviceRoleKey,
93+
deps.fetch
94+
);
95+
96+
if (!secrets.success) {
97+
const result: InstallerResult = {
98+
success: false,
99+
secrets,
100+
migrations: {
101+
success: false,
102+
status: 0,
103+
error: 'Skipped - secrets failed',
104+
},
105+
message:
106+
'Failed to configure vault secrets. Check the pgflow Control Plane is deployed.',
107+
};
108+
return jsonResponse(result, 500);
109+
}
110+
111+
// Step 2: Run migrations
112+
console.log('Running migrations...');
113+
const migrations = await callControlPlane(
114+
'/migrations/up',
115+
'POST',
116+
supabaseUrl,
117+
serviceRoleKey,
118+
deps.fetch
119+
);
120+
121+
const result: InstallerResult = {
122+
success: secrets.success && migrations.success,
123+
secrets,
124+
migrations,
125+
message: migrations.success
126+
? 'pgflow installed successfully! Vault secrets configured and migrations applied.'
127+
: 'Secrets configured but migrations failed. Check the error details.',
128+
};
129+
130+
console.log('Installer complete:', result.message);
131+
return jsonResponse(result, result.success ? 200 : 500);
132+
};
133+
}
134+
135+
function jsonResponse(data: unknown, status: number): Response {
136+
return new Response(JSON.stringify(data, null, 2), {
137+
status,
138+
headers: { 'Content-Type': 'application/json' },
139+
});
140+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface StepResult {
2+
success: boolean;
3+
status: number;
4+
data?: unknown;
5+
error?: string;
6+
}
7+
8+
export interface InstallerResult {
9+
success: boolean;
10+
secrets: StepResult;
11+
migrations: StepResult;
12+
message: string;
13+
}

0 commit comments

Comments
 (0)