Skip to content

Commit f5358e2

Browse files
committed
update docs for auto-compilation flow and remove flows.ts
1 parent 3278b94 commit f5358e2

File tree

13 files changed

+232
-102
lines changed

13 files changed

+232
-102
lines changed

pkgs/cli/__tests__/commands/compile/index.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('fetchFlowSQL', () => {
7272
status: 404,
7373
json: async () => ({
7474
error: 'Flow Not Found',
75-
message: "Flow 'unknown_flow' not found. Did you add it to flows.ts?",
75+
message: "Flow 'unknown_flow' not found. Did you add it to supabase/functions/pgflow/index.ts?",
7676
}),
7777
};
7878

@@ -83,7 +83,7 @@ describe('fetchFlowSQL', () => {
8383
).rejects.toThrow("Flow 'unknown_flow' not found");
8484
await expect(
8585
fetchFlowSQL('unknown_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
86-
).rejects.toThrow('Add your flow to supabase/functions/pgflow/flows.ts');
86+
).rejects.toThrow('Add your flow to supabase/functions/pgflow/index.ts');
8787
});
8888

8989
it('should handle ECONNREFUSED with startup instructions', async () => {
@@ -167,7 +167,7 @@ describe('fetchFlowSQL', () => {
167167
).rejects.toThrow("Flow 'unknown_flow' not found");
168168
await expect(
169169
fetchFlowSQL('unknown_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
170-
).rejects.toThrow('Did you add it to flows.ts');
170+
).rejects.toThrow('Did you add it to supabase/functions/pgflow/index.ts');
171171
});
172172

173173
it('should construct correct URL with flow slug', async () => {

pkgs/cli/__tests__/commands/install/create-edge-function.test.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('createEdgeFunction', () => {
2222
fs.rmSync(tempDir, { recursive: true, force: true });
2323
});
2424

25-
it('should create all three files when none exist', async () => {
25+
it('should create both files when none exist', async () => {
2626
const result = await createEdgeFunction({
2727
supabasePath,
2828
autoConfirm: true,
@@ -36,23 +36,16 @@ describe('createEdgeFunction', () => {
3636

3737
// Verify all files exist
3838
const indexPath = path.join(pgflowFunctionDir, 'index.ts');
39-
const flowsPath = path.join(pgflowFunctionDir, 'flows.ts');
4039
const denoJsonPath = path.join(pgflowFunctionDir, 'deno.json');
4140

4241
expect(fs.existsSync(indexPath)).toBe(true);
43-
expect(fs.existsSync(flowsPath)).toBe(true);
4442
expect(fs.existsSync(denoJsonPath)).toBe(true);
4543

46-
// Verify index.ts content
44+
// Verify index.ts content (inline flow registration, no flows.ts)
4745
const indexContent = fs.readFileSync(indexPath, 'utf8');
4846
expect(indexContent).toContain("import { ControlPlane } from '@pgflow/edge-worker'");
49-
expect(indexContent).toContain("import { flows } from './flows.ts'");
50-
expect(indexContent).toContain('ControlPlane.serve(flows)');
51-
52-
// Verify flows.ts content
53-
const flowsContent = fs.readFileSync(flowsPath, 'utf8');
54-
expect(flowsContent).toContain('export const flows = [');
55-
expect(flowsContent).toContain('// Import your flows here');
47+
expect(indexContent).toContain('ControlPlane.serve([');
48+
expect(indexContent).toContain('// Import your flows here');
5649

5750
// Verify deno.json content
5851
const denoJsonContent = fs.readFileSync(denoJsonPath, 'utf8');
@@ -66,11 +59,9 @@ describe('createEdgeFunction', () => {
6659
fs.mkdirSync(pgflowFunctionDir, { recursive: true });
6760

6861
const indexPath = path.join(pgflowFunctionDir, 'index.ts');
69-
const flowsPath = path.join(pgflowFunctionDir, 'flows.ts');
7062
const denoJsonPath = path.join(pgflowFunctionDir, 'deno.json');
7163

7264
fs.writeFileSync(indexPath, '// existing content');
73-
fs.writeFileSync(flowsPath, '// existing content');
7465
fs.writeFileSync(denoJsonPath, '// existing content');
7566

7667
const result = await createEdgeFunction({
@@ -83,7 +74,6 @@ describe('createEdgeFunction', () => {
8374

8475
// Verify files still exist with original content
8576
expect(fs.readFileSync(indexPath, 'utf8')).toBe('// existing content');
86-
expect(fs.readFileSync(flowsPath, 'utf8')).toBe('// existing content');
8777
expect(fs.readFileSync(denoJsonPath, 'utf8')).toBe('// existing content');
8878
});
8979

@@ -92,7 +82,6 @@ describe('createEdgeFunction', () => {
9282
fs.mkdirSync(pgflowFunctionDir, { recursive: true });
9383

9484
const indexPath = path.join(pgflowFunctionDir, 'index.ts');
95-
const flowsPath = path.join(pgflowFunctionDir, 'flows.ts');
9685
const denoJsonPath = path.join(pgflowFunctionDir, 'deno.json');
9786

9887
// Only create index.ts
@@ -103,19 +92,15 @@ describe('createEdgeFunction', () => {
10392
autoConfirm: true,
10493
});
10594

106-
// Should return true because some files were created
95+
// Should return true because deno.json was created
10796
expect(result).toBe(true);
10897

10998
// Verify index.ts was not modified
11099
expect(fs.readFileSync(indexPath, 'utf8')).toBe('// existing content');
111100

112-
// Verify flows.ts and deno.json were created
113-
expect(fs.existsSync(flowsPath)).toBe(true);
101+
// Verify deno.json was created
114102
expect(fs.existsSync(denoJsonPath)).toBe(true);
115103

116-
const flowsContent = fs.readFileSync(flowsPath, 'utf8');
117-
expect(flowsContent).toContain('export const flows = [');
118-
119104
const denoJsonContent = fs.readFileSync(denoJsonPath, 'utf8');
120105
expect(denoJsonContent).toContain('"imports"');
121106
});
@@ -138,7 +123,6 @@ describe('createEdgeFunction', () => {
138123

139124
// Verify files exist
140125
expect(fs.existsSync(path.join(pgflowFunctionDir, 'index.ts'))).toBe(true);
141-
expect(fs.existsSync(path.join(pgflowFunctionDir, 'flows.ts'))).toBe(true);
142126
expect(fs.existsSync(path.join(pgflowFunctionDir, 'deno.json'))).toBe(true);
143127
});
144128

pkgs/cli/src/commands/compile/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export async function fetchFlowSQL(
3030
const errorData = await response.json();
3131
throw new Error(
3232
`Flow '${flowSlug}' not found.\n\n` +
33-
`${errorData.message || 'Did you add it to flows.ts?'}\n\n` +
33+
`${errorData.message || 'Did you add it to supabase/functions/pgflow/index.ts?'}\n\n` +
3434
`Fix:\n` +
35-
`1. Add your flow to supabase/functions/pgflow/flows.ts\n` +
35+
`1. Add your flow to supabase/functions/pgflow/index.ts\n` +
3636
`2. Restart edge functions: supabase functions serve`
3737
);
3838
}
@@ -45,11 +45,6 @@ export async function fetchFlowSQL(
4545
return await response.json();
4646
} catch (error) {
4747
if (error instanceof Error) {
48-
// Debug: show actual error and URL
49-
console.error(`[DEBUG] Fetch failed for URL: ${url}`);
50-
console.error(`[DEBUG] Error message: ${error.message}`);
51-
console.error(`[DEBUG] Error cause:`, (error as any).cause);
52-
5348
// Check for connection refused errors
5449
if (
5550
error.message.includes('ECONNREFUSED') ||

pkgs/cli/src/commands/install/create-edge-function.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,13 @@ import chalk from 'chalk';
55
import { getVersion } from '../../utils/get-version.js';
66

77
const INDEX_TS_TEMPLATE = `import { ControlPlane } from '@pgflow/edge-worker';
8-
import { flows } from './flows.ts';
9-
10-
ControlPlane.serve(flows);
11-
`;
12-
13-
const FLOWS_TS_TEMPLATE = `// Import your flows here
8+
// Import your flows here:
149
// import { MyFlow } from '../_flows/my_flow.ts';
1510
16-
// Export flows array for ControlPlane
17-
export const flows = [
11+
ControlPlane.serve([
12+
// Add your flows here:
1813
// MyFlow,
19-
];
14+
]);
2015
`;
2116

2217
const DENO_JSON_TEMPLATE = (version: string) => `{
@@ -44,7 +39,6 @@ export async function createEdgeFunction({
4439
const pgflowFunctionDir = path.join(functionsDir, 'pgflow');
4540

4641
const indexPath = path.join(pgflowFunctionDir, 'index.ts');
47-
const flowsPath = path.join(pgflowFunctionDir, 'flows.ts');
4842
const denoJsonPath = path.join(pgflowFunctionDir, 'deno.json');
4943

5044
// Check what needs to be created
@@ -54,10 +48,6 @@ export async function createEdgeFunction({
5448
filesToCreate.push({ path: indexPath, name: 'index.ts' });
5549
}
5650

57-
if (!fs.existsSync(flowsPath)) {
58-
filesToCreate.push({ path: flowsPath, name: 'flows.ts' });
59-
}
60-
6151
if (!fs.existsSync(denoJsonPath)) {
6252
filesToCreate.push({ path: denoJsonPath, name: 'deno.json' });
6353
}
@@ -69,7 +59,6 @@ export async function createEdgeFunction({
6959
const detailedMsg = [
7060
'Existing files:',
7161
` ${chalk.dim('•')} ${chalk.bold('supabase/functions/pgflow/index.ts')}`,
72-
` ${chalk.dim('•')} ${chalk.bold('supabase/functions/pgflow/flows.ts')}`,
7362
` ${chalk.dim('•')} ${chalk.bold('supabase/functions/pgflow/deno.json')}`,
7463
].join('\n');
7564

@@ -113,10 +102,6 @@ export async function createEdgeFunction({
113102
fs.writeFileSync(indexPath, INDEX_TS_TEMPLATE);
114103
}
115104

116-
if (filesToCreate.some((f) => f.path === flowsPath)) {
117-
fs.writeFileSync(flowsPath, FLOWS_TS_TEMPLATE);
118-
}
119-
120105
if (filesToCreate.some((f) => f.path === denoJsonPath)) {
121106
fs.writeFileSync(denoJsonPath, DENO_JSON_TEMPLATE(getVersion()));
122107
}

pkgs/cli/supabase/functions/pgflow/flows.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { ControlPlane } from '@pgflow/edge-worker';
2-
import { flows } from './flows.ts';
2+
import { TestFlowE2E } from '../_flows/test_flow_e2e.ts';
33

4-
ControlPlane.serve(flows);
4+
ControlPlane.serve([TestFlowE2E]);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* @example
88
* ```typescript
99
* import { ControlPlane } from '@pgflow/edge-worker';
10-
* import { flows } from './flows.ts';
10+
* import { MyFlow } from '../_flows/my_flow.ts';
1111
*
12-
* ControlPlane.serve(flows);
12+
* ControlPlane.serve([MyFlow]);
1313
* ```
1414
*/
1515

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function handleGetFlow(
9696
return jsonResponse(
9797
{
9898
error: 'Flow Not Found',
99-
message: `Flow '${slug}' not found. Did you add it to flows.ts?`,
99+
message: `Flow '${slug}' not found. Did you add it to supabase/functions/pgflow/index.ts?`,
100100
},
101101
404
102102
);

pkgs/website/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ export default defineConfig({
321321
link: '/concepts/three-layer-architecture/',
322322
},
323323
{ label: 'Data model', link: '/concepts/data-model/' },
324+
{
325+
label: 'Auto-compilation',
326+
link: '/concepts/auto-compilation/',
327+
},
324328
],
325329
},
326330
{
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
title: Auto-Compilation Architecture
3+
description: How pgflow compiles TypeScript flows to SQL via HTTP
4+
sidebar:
5+
order: 25
6+
---
7+
8+
import { Aside } from "@astrojs/starlight/components";
9+
10+
pgflow compiles TypeScript flow definitions to SQL migrations via an HTTP-based architecture. This design eliminates the need for a local Deno installation and ensures compilation uses the same runtime as production.
11+
12+
## How It Works
13+
14+
When you run `pgflow compile greet_user`, the following happens:
15+
16+
```
17+
┌─────────────┐ HTTP GET ┌─────────────────────┐
18+
│ pgflow CLI │ ─────────────────>│ ControlPlane Edge │
19+
│ │ │ Function │
20+
│ │ │ │
21+
│ │ SQL Array │ 1. Look up flow │
22+
│ │ <─────────────────│ 2. Call compileFlow│
23+
│ │ │ 3. Return SQL │
24+
│ │ └─────────────────────┘
25+
│ 4. Write │
26+
│ migration │
27+
└─────────────┘
28+
```
29+
30+
1. **CLI sends request** - The compile command sends an HTTP GET request to:
31+
`http://127.0.0.1:54321/functions/v1/pgflow/flows/{slug}`
32+
33+
2. **ControlPlane looks up flow** - The edge function has a registry of flows (from your `index.ts`). It finds the flow by slug.
34+
35+
3. **Compilation happens in Deno** - The ControlPlane calls `compileFlow()` from `@pgflow/dsl`, which extracts the flow structure and generates SQL.
36+
37+
4. **SQL returned to CLI** - The response contains an array of SQL statements.
38+
39+
5. **CLI writes migration** - The CLI joins the SQL and writes it to `supabase/migrations/{timestamp}_create_{slug}_flow.sql`.
40+
41+
## The ControlPlane Edge Function
42+
43+
The `pgflow` edge function is created during installation and serves as your project's ControlPlane:
44+
45+
```typescript title="supabase/functions/pgflow/index.ts"
46+
import { ControlPlane } from '@pgflow/edge-worker';
47+
import { GreetUser } from '../_flows/greet_user.ts';
48+
import { ProcessOrder } from '../_flows/process_order.ts';
49+
50+
ControlPlane.serve([
51+
GreetUser,
52+
ProcessOrder,
53+
]);
54+
```
55+
56+
The ControlPlane:
57+
- **Registers flows** by slug in an in-memory registry
58+
- **Exposes** the `/flows/:slug` endpoint for compilation
59+
- **Returns 404** if a flow slug is not found in the registry
60+
- **Returns 500** with error details if compilation fails
61+
62+
## Why HTTP-Based Compilation?
63+
64+
This architecture provides several benefits:
65+
66+
**No local Deno required** - Users don't need Deno installed on their machine. The Supabase Edge Functions runtime handles everything.
67+
68+
**Same runtime as production** - Flows are compiled using the exact same Deno environment they'll run in, eliminating "works on my machine" issues.
69+
70+
**Consistent dependency resolution** - The `deno.json` import map in your edge function ensures consistent package versions.
71+
72+
**Simpler CLI** - The CLI is a lightweight Node.js package that makes HTTP requests, rather than needing to bundle the entire compilation infrastructure.
73+
74+
## Adding New Flows
75+
76+
To make a flow available for compilation:
77+
78+
1. Create the flow definition in `_flows/`
79+
2. Import it in `supabase/functions/pgflow/index.ts`
80+
3. Add it to the `ControlPlane.serve([...])` array
81+
4. Restart edge functions: `npx supabase functions serve`
82+
83+
<Aside type="caution">
84+
After modifying `index.ts`, you must restart the edge functions server. The ControlPlane builds its registry at startup.
85+
</Aside>
86+
87+
## API Reference
88+
89+
### GET /flows/:slug
90+
91+
Compiles a flow to SQL.
92+
93+
**Request:**
94+
```
95+
GET /functions/v1/pgflow/flows/greet_user
96+
Authorization: Bearer <publishable_key>
97+
```
98+
99+
**Success Response (200):**
100+
```json
101+
{
102+
"flowSlug": "greet_user",
103+
"sql": [
104+
"SELECT pgflow.create_flow('greet_user');",
105+
"SELECT pgflow.add_step('greet_user', 'full_name', 'single', 0, NULL);",
106+
"SELECT pgflow.add_step('greet_user', 'greeting', 'single', 0, ARRAY['full_name']);"
107+
]
108+
}
109+
```
110+
111+
**Flow Not Found (404):**
112+
```json
113+
{
114+
"error": "Flow Not Found",
115+
"message": "Flow 'unknown' not found. Did you add it to supabase/functions/pgflow/index.ts?"
116+
}
117+
```
118+
119+
**Compilation Error (500):**
120+
```json
121+
{
122+
"error": "Compilation Error",
123+
"message": "Detailed error message from compileFlow()"
124+
}
125+
```

0 commit comments

Comments
 (0)