Skip to content

Commit 3a2efa4

Browse files
committed
add migration installer API endpoints for no-CLI platforms
1 parent 03a3b6a commit 3a2efa4

File tree

21 files changed

+1079
-52
lines changed

21 files changed

+1079
-52
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pgflow/edge-worker': patch
3+
---
4+
5+
Add migration installer API endpoints for no-CLI platforms

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"private": true,
1818
"devDependencies": {
1919
"@axhxrx/nx-deno": "^1.0.2",
20-
"supabase": "^2.62.10",
2120
"@changesets/cli": "^2.28.1",
2221
"@decimalturn/toml-patch": "0.3.7",
2322
"@eslint/js": "^9.8.0",
@@ -45,7 +44,9 @@
4544
"netlify-cli": "^22.1.3",
4645
"nx": "21.2.1",
4746
"prettier": "^2.6.2",
47+
"supabase": "^2.62.10",
4848
"tslib": "^2.3.0",
49+
"tsx": "^4.20.6",
4950
"typescript": "5.8.3",
5051
"typescript-eslint": "8.34.1",
5152
"vite": "6.3.5",

pkgs/core/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

pkgs/edge-worker/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
supabase/migrations/*
22
supabase/functions/_dist/
33
supabase/seed.sql
4+
src/_generated/

pkgs/edge-worker/deno.lock

Lines changed: 17 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkgs/edge-worker/jsr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"src/**/*.ts"
2222
],
2323
"exclude": [
24+
"!src/_generated",
2425
"__tests__/**/*",
2526
"tests/",
2627
"supabase/",

pkgs/edge-worker/project.json

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@
77
"supabase:ci-marker": {
88
"executor": "nx:noop"
99
},
10+
"generate-migrations": {
11+
"executor": "nx:run-commands",
12+
"cache": true,
13+
"inputs": ["{workspaceRoot}/pkgs/core/supabase/migrations/*.sql", "{projectRoot}/scripts/generate-migrations.ts"],
14+
"outputs": ["{projectRoot}/src/_generated/migrations"],
15+
"options": {
16+
"cwd": "{projectRoot}",
17+
"command": "tsx scripts/generate-migrations.ts"
18+
}
19+
},
1020
"build": {
1121
"executor": "nx:noop",
12-
"dependsOn": ["^build"]
22+
"dependsOn": ["generate-migrations", "^build"]
1323
},
1424
"_build_disabled": {
1525
"executor": "@nx/js:tsc",
@@ -134,8 +144,18 @@
134144
"parallel": false
135145
}
136146
},
147+
"db-clean:ensure": {
148+
"executor": "nx:run-commands",
149+
"local": true,
150+
"cache": false,
151+
"options": {
152+
"cwd": "pkgs/edge-worker",
153+
"commands": ["./scripts/ensure-db-clean"],
154+
"parallel": false
155+
}
156+
},
137157
"test:unit": {
138-
"dependsOn": ["^build"],
158+
"dependsOn": ["build", "^build"],
139159
"executor": "nx:run-commands",
140160
"local": true,
141161
"inputs": ["default", "^production"],
@@ -148,7 +168,7 @@
148168
}
149169
},
150170
"test:integration": {
151-
"dependsOn": ["db:ensure", "^build"],
171+
"dependsOn": ["db:ensure", "build", "^build"],
152172
"executor": "nx:run-commands",
153173
"local": true,
154174
"inputs": ["default", "^production"],
@@ -160,9 +180,22 @@
160180
"parallel": false
161181
}
162182
},
183+
"test:migrations": {
184+
"dependsOn": ["db-clean:ensure", "build", "^build"],
185+
"executor": "nx:run-commands",
186+
"local": true,
187+
"inputs": ["default", "^production"],
188+
"options": {
189+
"cwd": "pkgs/edge-worker",
190+
"commands": [
191+
"deno test --config deno.test.json --allow-all --env=supabase/functions/.env tests/migrations/"
192+
],
193+
"parallel": false
194+
}
195+
},
163196
"serve:functions:e2e": {
164197
"executor": "nx:run-commands",
165-
"dependsOn": ["^build"],
198+
"dependsOn": ["build", "^build"],
166199
"continuous": true,
167200
"local": true,
168201
"cache": false,
@@ -188,12 +221,12 @@
188221
},
189222
"test": {
190223
"inputs": ["default", "^production"],
191-
"dependsOn": ["test:types", "test:unit"]
224+
"dependsOn": ["test:types", "test:unit", "test:migrations"]
192225
},
193226
"test:types": {
194227
"executor": "nx:run-commands",
195228
"cache": true,
196-
"dependsOn": ["^build"],
229+
"dependsOn": ["build", "^build"],
197230
"inputs": ["production", "^production"],
198231
"options": {
199232
"cwd": "{projectRoot}",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Clean up any existing containers
5+
echo "Shutting down existing clean database containers..."
6+
docker compose -f tests/db-clean/compose.yaml down --volumes --remove-orphans
7+
8+
# Start fresh containers
9+
echo "Starting clean database..."
10+
docker compose -f tests/db-clean/compose.yaml up --detach
11+
12+
# Wait for database to be ready
13+
echo "Waiting for database to be available..."
14+
./scripts/wait-for-localhost 5433
15+
16+
# Additional pause to ensure database is fully initialized
17+
echo "Waiting for database initialization..."
18+
sleep 3
19+
20+
echo "Clean database is ready on port 5433!"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Migration Generator Script for edge-worker
3+
*
4+
* Generates TypeScript modules from SQL migration files at build time.
5+
* This allows static imports of migration content that work across all environments
6+
* (local dev, JSR, npm, Supabase Edge Functions).
7+
*
8+
* The migrations are sourced from the core package and bundled directly into
9+
* edge-worker, making it fully self-contained without cross-package imports.
10+
*
11+
* Usage: tsx scripts/generate-migrations.ts
12+
*/
13+
14+
import * as fs from 'node:fs';
15+
import * as path from 'node:path';
16+
17+
const MIGRATIONS_DIR = '../core/supabase/migrations';
18+
const OUTPUT_DIR = 'src/_generated/migrations';
19+
const GENERATED_DIR = `${OUTPUT_DIR}/_generated`;
20+
const INDEX_FILE = `${OUTPUT_DIR}/index.ts`;
21+
const TYPES_FILE = `${OUTPUT_DIR}/types.ts`;
22+
23+
interface MigrationFile {
24+
timestamp: string;
25+
filename: string;
26+
content: string;
27+
}
28+
29+
function escapeBackticks(str: string): string {
30+
// Escape backticks and ${} template expressions
31+
return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
32+
}
33+
34+
function getMigrationFiles(): MigrationFile[] {
35+
const files = fs.readdirSync(MIGRATIONS_DIR);
36+
const migrations: MigrationFile[] = [];
37+
38+
for (const filename of files) {
39+
// Skip non-SQL files and atlas files
40+
if (!filename.endsWith('.sql') || filename.startsWith('atlas')) {
41+
continue;
42+
}
43+
44+
// Extract 14-digit timestamp from filename (e.g., "20250429164909_...")
45+
const match = filename.match(/^(\d{14})_/);
46+
if (!match) {
47+
console.warn(`Skipping ${filename}: no valid timestamp found`);
48+
continue;
49+
}
50+
51+
const content = fs.readFileSync(path.join(MIGRATIONS_DIR, filename), 'utf-8');
52+
migrations.push({
53+
timestamp: match[1],
54+
filename,
55+
content,
56+
});
57+
}
58+
59+
// Sort by timestamp (ascending)
60+
migrations.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
61+
62+
return migrations;
63+
}
64+
65+
function generateMigrationModule(migration: MigrationFile): string {
66+
const escapedContent = escapeBackticks(migration.content);
67+
return `import type { Migration } from '../types.ts';
68+
69+
export const migration: Migration = {
70+
timestamp: '${migration.timestamp}',
71+
filename: '${migration.filename}',
72+
content: \`${escapedContent}\`,
73+
};
74+
`;
75+
}
76+
77+
function generateTypesFile(): string {
78+
return `/**
79+
* Migration types for edge-worker
80+
*
81+
* Auto-generated by scripts/generate-migrations.ts
82+
* Do not edit manually.
83+
*/
84+
85+
/**
86+
* Represents a migration file loaded from @pgflow/core
87+
*/
88+
export interface Migration {
89+
/** 14-digit timestamp from filename, e.g., "20250429164909" (PK) */
90+
timestamp: string;
91+
/** Full filename, e.g., "20250429164909_pgflow_initial.sql" */
92+
filename: string;
93+
/** Full SQL content of the migration */
94+
content: string;
95+
}
96+
`;
97+
}
98+
99+
function generateIndexFile(migrations: MigrationFile[]): string {
100+
const imports = migrations
101+
.map((m) => `import { migration as m_${m.timestamp} } from './_generated/m_${m.timestamp}.ts';`)
102+
.join('\n');
103+
104+
const arrayItems = migrations.map((m) => ` m_${m.timestamp},`).join('\n');
105+
106+
return `/**
107+
* Auto-generated migration exports for edge-worker
108+
*
109+
* This file is regenerated by scripts/generate-migrations.ts
110+
* Do not edit manually.
111+
*/
112+
113+
import type { Migration } from './types.ts';
114+
${imports}
115+
116+
export const migrations: Migration[] = [
117+
${arrayItems}
118+
];
119+
120+
export function getMigrations(): Migration[] {
121+
return migrations;
122+
}
123+
124+
export type { Migration };
125+
`;
126+
}
127+
128+
function main(): void {
129+
console.log('Generating migration modules for edge-worker...');
130+
131+
// Ensure output directories exist
132+
fs.mkdirSync(GENERATED_DIR, { recursive: true });
133+
134+
// Clean existing generated migration files
135+
if (fs.existsSync(GENERATED_DIR)) {
136+
const existingFiles = fs.readdirSync(GENERATED_DIR);
137+
for (const file of existingFiles) {
138+
fs.unlinkSync(path.join(GENERATED_DIR, file));
139+
}
140+
}
141+
142+
// Get and process migrations
143+
const migrations = getMigrationFiles();
144+
console.log(`Found ${migrations.length} migrations`);
145+
146+
// Generate types file
147+
const typesContent = generateTypesFile();
148+
fs.writeFileSync(TYPES_FILE, typesContent);
149+
console.log(`Generated ${TYPES_FILE}`);
150+
151+
// Generate individual module files
152+
for (const migration of migrations) {
153+
const moduleContent = generateMigrationModule(migration);
154+
const outputPath = path.join(GENERATED_DIR, `m_${migration.timestamp}.ts`);
155+
fs.writeFileSync(outputPath, moduleContent);
156+
console.log(` Generated ${path.basename(outputPath)}`);
157+
}
158+
159+
// Generate index file
160+
const indexContent = generateIndexFile(migrations);
161+
fs.writeFileSync(INDEX_FILE, indexContent);
162+
console.log(`Generated ${INDEX_FILE}`);
163+
164+
console.log('Done!');
165+
}
166+
167+
main();

0 commit comments

Comments
 (0)