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 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 generate code and start the API:
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, and runs the dev server
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 schema.sql + generated/ (db client, routes, policies, Zod schemas)
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 + generate + db:bootstrap + dev |
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 `db:bootstrap` or `dev`.
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] # generate:client + generate:api, then start generated/app.ts
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 + generate + db:bootstrap + dev
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(schemaPath?: string): Promise<void>;
1
+ export declare function runDev(args?: string[]): Promise<void>;
package/dist/cli/dev.js CHANGED
@@ -1,10 +1,150 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { spawn } from 'node:child_process';
2
+ import { watch } from 'node:fs';
2
3
  import path from 'node:path';
3
- import { generateApi, generateClient } from './generate.js';
4
- import { DEFAULT_OUTPUT_DIR } from './paths.js';
5
- export async function runDev(schemaPath) {
6
- await generateClient(schemaPath);
7
- await generateApi(schemaPath);
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
- execFileSync(process.execPath, ['--import', 'tsx', appPath], { stdio: 'inherit', cwd: process.cwd() });
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
  }
@@ -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\n\tnpx schematic-pg generate\n\tnpx schematic-pg db:bootstrap\n\tnpx schematic-pg dev\n";
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;
@@ -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] Regenerate client + API and start server
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(schemaPath);
53
+ await runDev(args);
54
54
  break;
55
55
  case 'db:ping':
56
56
  await runDbPing();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schematic-pg",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Single-file backend framework for PostgreSQL and Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",