schematic-pg 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -14
- package/dist/cli/db.js +2 -0
- package/dist/cli/dev.d.ts +1 -1
- package/dist/cli/dev.js +147 -7
- package/dist/cli/init.js +5 -2
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +1 -3
- package/dist/cli/wait-for-database.d.ts +9 -0
- package/dist/cli/wait-for-database.js +39 -0
- package/dist/cli.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,11 +201,12 @@ npx schematic-pg init my-app
|
|
|
201
201
|
cd my-app
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
Edit `app.schema`, then
|
|
204
|
+
Edit `app.schema`, then start the full dev loop:
|
|
205
205
|
|
|
206
206
|
```bash
|
|
207
207
|
make dev
|
|
208
|
-
# → starts PostgreSQL, generates code, bootstraps the DB,
|
|
208
|
+
# → starts PostgreSQL, generates code, bootstraps the DB, runs the dev server,
|
|
209
|
+
# and watches app.schema for changes (regenerate + bootstrap + restart)
|
|
209
210
|
# → http://localhost:3000
|
|
210
211
|
```
|
|
211
212
|
|
|
@@ -213,17 +214,22 @@ Or run each step individually:
|
|
|
213
214
|
|
|
214
215
|
```bash
|
|
215
216
|
# Start PostgreSQL (PostGIS-enabled, matches .env defaults)
|
|
216
|
-
docker compose up -d
|
|
217
|
+
docker compose up -d --wait
|
|
217
218
|
|
|
218
|
-
# Generate
|
|
219
|
-
npx schematic-pg generate
|
|
220
|
-
|
|
221
|
-
# Apply DDL to the database and snapshot schema state
|
|
222
|
-
npx schematic-pg db:bootstrap
|
|
223
|
-
|
|
224
|
-
# Regenerate client + API and start the server
|
|
219
|
+
# Generate, bootstrap, start server, and watch app.schema (default)
|
|
225
220
|
npx schematic-pg dev
|
|
226
221
|
# → http://localhost:3000
|
|
222
|
+
|
|
223
|
+
# One-shot dev server without schema watching:
|
|
224
|
+
npx schematic-pg dev --no-watch
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Manual split when you need finer control:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npx schematic-pg generate
|
|
231
|
+
npx schematic-pg db:bootstrap
|
|
232
|
+
npx schematic-pg dev --no-watch
|
|
227
233
|
```
|
|
228
234
|
|
|
229
235
|
The `init` command creates everything you need to get running:
|
|
@@ -233,7 +239,7 @@ The `init` command creates everything you need to get running:
|
|
|
233
239
|
| `app.schema` | Starter schema (one `User` model) — edit this |
|
|
234
240
|
| `.env` | `DATABASE_URL`, JWT settings |
|
|
235
241
|
| `docker-compose.yml` | Local PostGIS PostgreSQL on `:5432` |
|
|
236
|
-
| `Makefile` | `make dev` — docker compose
|
|
242
|
+
| `Makefile` | `make dev` — docker compose (with health wait) + `schematic-pg dev` |
|
|
237
243
|
| `tsconfig.json` | TypeScript config for `generated/` and `src/routes/` |
|
|
238
244
|
| `package.json` | `schematic-pg` + runtime deps (`hono`, `pg`, `zod`, …) |
|
|
239
245
|
| `src/routes/health.ts` | Example custom route mounted at `/health` |
|
|
@@ -261,7 +267,7 @@ Generated code imports the runtime from the `schematic-pg` package (`schematic-p
|
|
|
261
267
|
| `JWT_ROLE_CLAIM` | `role` | JWT claim mapped to `auth.role` |
|
|
262
268
|
| `JWT_USER_ID_CLAIM` | `sub` | JWT claim mapped to `auth.user.id` |
|
|
263
269
|
|
|
264
|
-
Set these in `.env` before running `
|
|
270
|
+
Set these in `.env` before running `dev` or `db:bootstrap`.
|
|
265
271
|
|
|
266
272
|
---
|
|
267
273
|
|
|
@@ -289,13 +295,22 @@ Run `generate:client` before `generate:api` when using the split commands — ro
|
|
|
289
295
|
### Development server
|
|
290
296
|
|
|
291
297
|
```bash
|
|
292
|
-
schematic-pg dev [schema]
|
|
298
|
+
schematic-pg dev [schema] [--no-watch]
|
|
293
299
|
```
|
|
294
300
|
|
|
301
|
+
`dev` runs the full local loop:
|
|
302
|
+
|
|
303
|
+
1. `generate` — writes `schema.sql` and `generated/*`
|
|
304
|
+
2. `db:bootstrap` — waits for Postgres, applies DDL, snapshots schema state
|
|
305
|
+
3. Starts `generated/app.ts`
|
|
306
|
+
4. Watches `app.schema` (default) — on change, re-runs generate, bootstrap, and server restart
|
|
307
|
+
|
|
308
|
+
Pass `--no-watch` for a one-shot run without file watching.
|
|
309
|
+
|
|
295
310
|
Equivalent npm scripts in a project created by `init`:
|
|
296
311
|
|
|
297
312
|
```bash
|
|
298
|
-
make dev # docker compose up -d
|
|
313
|
+
make dev # docker compose up -d --wait + schematic-pg dev
|
|
299
314
|
npm run dev # schematic-pg dev
|
|
300
315
|
npm run generate # schematic-pg generate
|
|
301
316
|
```
|
package/dist/cli/db.js
CHANGED
|
@@ -5,6 +5,7 @@ import { applyPendingMigrations } from '../db/migrate.js';
|
|
|
5
5
|
import { createMigration, getAppliedMigrationFilenames, listMigrationFiles } from '../db/migrations.js';
|
|
6
6
|
import { snapshotExists } from '../db/schema-state.js';
|
|
7
7
|
import { resolveSchemaPath } from './paths.js';
|
|
8
|
+
import { waitForDatabase } from './wait-for-database.js';
|
|
8
9
|
function parseDiffArgs(args) {
|
|
9
10
|
let schemaPath = resolveSchemaPath();
|
|
10
11
|
let name;
|
|
@@ -50,6 +51,7 @@ export async function runDbBootstrap(schemaPath) {
|
|
|
50
51
|
const resolvedSchemaPath = resolveSchemaPath(schemaPath);
|
|
51
52
|
const client = new DatabaseClient();
|
|
52
53
|
try {
|
|
54
|
+
await waitForDatabase({ client });
|
|
53
55
|
await bootstrapDatabase(resolvedSchemaPath, client);
|
|
54
56
|
process.stdout.write(`Database bootstrapped from ${resolvedSchemaPath}\n`);
|
|
55
57
|
}
|
package/dist/cli/dev.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runDev(
|
|
1
|
+
export declare function runDev(args?: string[]): Promise<void>;
|
package/dist/cli/dev.js
CHANGED
|
@@ -1,10 +1,150 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { watch } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { runDbBootstrap } from './db.js';
|
|
5
|
+
import { generateAll } from './generate.js';
|
|
6
|
+
import { DEFAULT_OUTPUT_DIR, resolveSchemaPath } from './paths.js';
|
|
7
|
+
const WATCH_DEBOUNCE_MS = 300;
|
|
8
|
+
const SERVER_STOP_TIMEOUT_MS = 5000;
|
|
9
|
+
function parseDevArgs(args) {
|
|
10
|
+
let schemaPath = resolveSchemaPath();
|
|
11
|
+
let watchSchema = true;
|
|
12
|
+
for (const arg of args) {
|
|
13
|
+
if (arg === '--no-watch') {
|
|
14
|
+
watchSchema = false;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!arg.startsWith('--')) {
|
|
18
|
+
schemaPath = resolveSchemaPath(arg);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return { schemaPath, watchSchema };
|
|
22
|
+
}
|
|
23
|
+
function createDebouncer(fn, ms) {
|
|
24
|
+
let timer;
|
|
25
|
+
return () => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
timer = setTimeout(() => {
|
|
28
|
+
void fn();
|
|
29
|
+
}, ms);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function startServer(appPath) {
|
|
33
|
+
return spawn(process.execPath, ['--import', 'tsx', appPath], {
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
cwd: process.cwd(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function stopServer(serverProcess) {
|
|
39
|
+
if (!serverProcess || serverProcess.exitCode !== null || serverProcess.killed) {
|
|
40
|
+
return Promise.resolve();
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
serverProcess.once('exit', () => resolve());
|
|
44
|
+
if (process.platform === 'win32') {
|
|
45
|
+
serverProcess.kill();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
serverProcess.kill('SIGTERM');
|
|
49
|
+
}
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
if (serverProcess.exitCode === null && !serverProcess.killed) {
|
|
52
|
+
serverProcess.kill('SIGKILL');
|
|
53
|
+
}
|
|
54
|
+
}, SERVER_STOP_TIMEOUT_MS);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function waitForServerExit(serverProcess) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
serverProcess.once('exit', () => resolve());
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export async function runDev(args = []) {
|
|
63
|
+
const { schemaPath, watchSchema } = parseDevArgs(args);
|
|
8
64
|
const appPath = path.resolve(DEFAULT_OUTPUT_DIR, 'app.ts');
|
|
9
|
-
|
|
65
|
+
let serverProcess = null;
|
|
66
|
+
let syncInProgress = false;
|
|
67
|
+
let restarting = false;
|
|
68
|
+
let shuttingDown = false;
|
|
69
|
+
async function syncAndServe() {
|
|
70
|
+
if (syncInProgress) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
syncInProgress = true;
|
|
74
|
+
try {
|
|
75
|
+
await generateAll(schemaPath);
|
|
76
|
+
await runDbBootstrap(schemaPath);
|
|
77
|
+
await stopServer(serverProcess);
|
|
78
|
+
serverProcess = startServer(appPath);
|
|
79
|
+
serverProcess.on('exit', (code, signal) => {
|
|
80
|
+
if (restarting || shuttingDown) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!watchSchema) {
|
|
84
|
+
if (code !== 0 && code !== null) {
|
|
85
|
+
process.exitCode = code;
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (code !== 0 && code !== null) {
|
|
90
|
+
process.stderr.write(`Dev server exited with code ${code}\n`);
|
|
91
|
+
process.exitCode = code;
|
|
92
|
+
shuttingDown = true;
|
|
93
|
+
}
|
|
94
|
+
else if (signal) {
|
|
95
|
+
process.stderr.write(`Dev server terminated by signal ${signal}\n`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
syncInProgress = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function reload() {
|
|
104
|
+
if (shuttingDown || syncInProgress) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
process.stderr.write('\nSchema changed — regenerating, bootstrapping, and restarting...\n');
|
|
108
|
+
restarting = true;
|
|
109
|
+
try {
|
|
110
|
+
await syncAndServe();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
restarting = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const scheduleReload = createDebouncer(reload, WATCH_DEBOUNCE_MS);
|
|
117
|
+
async function shutdown() {
|
|
118
|
+
if (shuttingDown) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
shuttingDown = true;
|
|
122
|
+
await stopServer(serverProcess);
|
|
123
|
+
}
|
|
124
|
+
process.once('SIGINT', () => {
|
|
125
|
+
void shutdown().finally(() => {
|
|
126
|
+
process.exit(process.exitCode ?? 0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
process.once('SIGTERM', () => {
|
|
130
|
+
void shutdown().finally(() => {
|
|
131
|
+
process.exit(process.exitCode ?? 0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
await syncAndServe();
|
|
135
|
+
if (!watchSchema) {
|
|
136
|
+
if (serverProcess) {
|
|
137
|
+
await waitForServerExit(serverProcess);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
watch(schemaPath, scheduleReload);
|
|
142
|
+
await new Promise((resolve) => {
|
|
143
|
+
const interval = setInterval(() => {
|
|
144
|
+
if (shuttingDown) {
|
|
145
|
+
clearInterval(interval);
|
|
146
|
+
resolve();
|
|
147
|
+
}
|
|
148
|
+
}, 100);
|
|
149
|
+
});
|
|
10
150
|
}
|
package/dist/cli/init.js
CHANGED
|
@@ -105,8 +105,11 @@ export async function runInit(args) {
|
|
|
105
105
|
console.log(' make dev');
|
|
106
106
|
console.log('');
|
|
107
107
|
console.log(' # or run individually:');
|
|
108
|
-
console.log(' docker compose up -d');
|
|
108
|
+
console.log(' docker compose up -d --wait');
|
|
109
|
+
console.log(` npx ${PACKAGE_NAME} dev # generate + bootstrap + server + schema watch`);
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' # split steps (dev already includes generate, bootstrap, and watch):');
|
|
109
112
|
console.log(` npx ${PACKAGE_NAME} generate`);
|
|
110
113
|
console.log(` npx ${PACKAGE_NAME} db:bootstrap`);
|
|
111
|
-
console.log(` npx ${PACKAGE_NAME} dev`);
|
|
114
|
+
console.log(` npx ${PACKAGE_NAME} dev --no-watch`);
|
|
112
115
|
}
|
package/dist/cli/templates.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export declare const ENV_TEMPLATE = "DATABASE_URL=postgresql://postgrest:postgre
|
|
|
3
3
|
export declare const GITIGNORE_TEMPLATE = "node_modules/\ndist/\n.env\ndocker_data/\n.DS_Store\n*.log\nnpm-debug.log*\n";
|
|
4
4
|
export declare const DOCKER_COMPOSE_TEMPLATE = "services:\n postgres:\n image: postgis/postgis:16-3.4\n container_name: schematic-pg-postgres\n restart: unless-stopped\n ports:\n - \"5432:5432\"\n environment:\n POSTGRES_USER: postgrest\n POSTGRES_PASSWORD: postgrest\n POSTGRES_DB: postgrest\n volumes:\n - ./docker_data/postgres:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgrest -d postgrest\"]\n interval: 5s\n timeout: 5s\n retries: 5\n";
|
|
5
5
|
export declare const TSCONFIG_TEMPLATE = "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"outDir\": \"dist\",\n \"rootDir\": \".\"\n },\n \"include\": [\"generated/**/*\", \"src/**/*\"]\n}\n";
|
|
6
|
-
export declare const MAKEFILE_TEMPLATE = ".PHONY: dev\n\ndev:\n\tdocker compose up -d
|
|
6
|
+
export declare const MAKEFILE_TEMPLATE = ".PHONY: dev\n\ndev:\n\tdocker compose up -d --wait\n\tnpx schematic-pg dev\n";
|
|
7
7
|
export declare const HEALTH_ROUTE_TEMPLATE = "import { Hono } from 'hono';\nimport type { AppEnv } from 'schematic-pg/api/types';\n\nconst router = new Hono<AppEnv>();\nrouter.get('/', (c) => c.json({ ok: true }));\nexport default router;\n";
|
|
8
8
|
export declare function createPackageJsonTemplate(projectName: string): string;
|
package/dist/cli/templates.js
CHANGED
|
@@ -65,9 +65,7 @@ export const TSCONFIG_TEMPLATE = `{
|
|
|
65
65
|
export const MAKEFILE_TEMPLATE = `.PHONY: dev
|
|
66
66
|
|
|
67
67
|
dev:
|
|
68
|
-
\tdocker compose up -d
|
|
69
|
-
\tnpx ${PACKAGE_NAME} generate
|
|
70
|
-
\tnpx ${PACKAGE_NAME} db:bootstrap
|
|
68
|
+
\tdocker compose up -d --wait
|
|
71
69
|
\tnpx ${PACKAGE_NAME} dev
|
|
72
70
|
`;
|
|
73
71
|
export const HEALTH_ROUTE_TEMPLATE = `import { Hono } from 'hono';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DatabaseClient } from '../db/client.js';
|
|
2
|
+
export type WaitForDatabaseOptions = {
|
|
3
|
+
maxAttempts?: number;
|
|
4
|
+
intervalMs?: number;
|
|
5
|
+
client?: Pick<DatabaseClient, 'query'>;
|
|
6
|
+
sleep?: (ms: number) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
export declare function pingDatabase(client: Pick<DatabaseClient, 'query'>): Promise<void>;
|
|
9
|
+
export declare function waitForDatabase(options?: WaitForDatabaseOptions): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DatabaseClient } from '../db/client.js';
|
|
2
|
+
export async function pingDatabase(client) {
|
|
3
|
+
const result = await client.query('SELECT 1 AS ok');
|
|
4
|
+
const ok = result.rows[0]?.ok;
|
|
5
|
+
if (ok !== 1) {
|
|
6
|
+
throw new Error(`Unexpected ping result: ${String(ok)}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function waitForDatabase(options = {}) {
|
|
10
|
+
const maxAttempts = options.maxAttempts ?? 30;
|
|
11
|
+
const intervalMs = options.intervalMs ?? 1000;
|
|
12
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
13
|
+
const ownClient = options.client ? null : new DatabaseClient();
|
|
14
|
+
const client = options.client ?? ownClient;
|
|
15
|
+
try {
|
|
16
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
await pingDatabase(client);
|
|
19
|
+
if (attempt > 1) {
|
|
20
|
+
process.stderr.write(`Database ready (attempt ${attempt}/${maxAttempts})\n`);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (attempt === maxAttempts) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
throw new Error(`Database not ready after ${maxAttempts} attempts (${intervalMs}ms interval): ${message}`);
|
|
28
|
+
}
|
|
29
|
+
process.stderr.write(`Waiting for database (${attempt}/${maxAttempts})...\n`);
|
|
30
|
+
await sleep(intervalMs);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (ownClient) {
|
|
36
|
+
await ownClient.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ Commands:
|
|
|
12
12
|
generate:sql [schema] Generate SQL DDL to stdout
|
|
13
13
|
generate:client [schema] Generate db client files
|
|
14
14
|
generate:api [schema] Generate API files
|
|
15
|
-
dev [schema]
|
|
15
|
+
dev [schema] [--no-watch] Generate, bootstrap DB, start server, watch schema
|
|
16
16
|
db:ping Test database connection
|
|
17
17
|
db:bootstrap [schema] Apply DDL and snapshot schema state
|
|
18
18
|
db:diff [schema] Show schema diff (--name <name> to write migration)
|
|
@@ -50,7 +50,7 @@ async function main() {
|
|
|
50
50
|
await generateApi(schemaPath);
|
|
51
51
|
break;
|
|
52
52
|
case 'dev':
|
|
53
|
-
await runDev(
|
|
53
|
+
await runDev(args);
|
|
54
54
|
break;
|
|
55
55
|
case 'db:ping':
|
|
56
56
|
await runDbPing();
|