schematic-pg 0.1.4 → 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 +35 -10
- package/dist/api-generator/utils/filter-operators.d.ts +1 -0
- package/dist/api-generator/utils/filter-operators.js +7 -1
- package/dist/api-generator/zod-schema-generator.js +1 -2
- 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 +10 -3
- package/dist/cli/templates.d.ts +2 -1
- package/dist/cli/templates.js +15 -1
- 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,21 +201,35 @@ 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
|
-
|
|
208
|
-
|
|
207
|
+
make dev
|
|
208
|
+
# → starts PostgreSQL, generates code, bootstraps the DB, runs the dev server,
|
|
209
|
+
# and watches app.schema for changes (regenerate + bootstrap + restart)
|
|
210
|
+
# → http://localhost:3000
|
|
211
|
+
```
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
npx schematic-pg generate
|
|
213
|
+
Or run each step individually:
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
```bash
|
|
216
|
+
# Start PostgreSQL (PostGIS-enabled, matches .env defaults)
|
|
217
|
+
docker compose up -d --wait
|
|
215
218
|
|
|
216
|
-
#
|
|
219
|
+
# Generate, bootstrap, start server, and watch app.schema (default)
|
|
217
220
|
npx schematic-pg dev
|
|
218
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
|
|
219
233
|
```
|
|
220
234
|
|
|
221
235
|
The `init` command creates everything you need to get running:
|
|
@@ -225,6 +239,7 @@ The `init` command creates everything you need to get running:
|
|
|
225
239
|
| `app.schema` | Starter schema (one `User` model) — edit this |
|
|
226
240
|
| `.env` | `DATABASE_URL`, JWT settings |
|
|
227
241
|
| `docker-compose.yml` | Local PostGIS PostgreSQL on `:5432` |
|
|
242
|
+
| `Makefile` | `make dev` — docker compose (with health wait) + `schematic-pg dev` |
|
|
228
243
|
| `tsconfig.json` | TypeScript config for `generated/` and `src/routes/` |
|
|
229
244
|
| `package.json` | `schematic-pg` + runtime deps (`hono`, `pg`, `zod`, …) |
|
|
230
245
|
| `src/routes/health.ts` | Example custom route mounted at `/health` |
|
|
@@ -252,7 +267,7 @@ Generated code imports the runtime from the `schematic-pg` package (`schematic-p
|
|
|
252
267
|
| `JWT_ROLE_CLAIM` | `role` | JWT claim mapped to `auth.role` |
|
|
253
268
|
| `JWT_USER_ID_CLAIM` | `sub` | JWT claim mapped to `auth.user.id` |
|
|
254
269
|
|
|
255
|
-
Set these in `.env` before running `
|
|
270
|
+
Set these in `.env` before running `dev` or `db:bootstrap`.
|
|
256
271
|
|
|
257
272
|
---
|
|
258
273
|
|
|
@@ -280,12 +295,22 @@ Run `generate:client` before `generate:api` when using the split commands — ro
|
|
|
280
295
|
### Development server
|
|
281
296
|
|
|
282
297
|
```bash
|
|
283
|
-
schematic-pg dev [schema]
|
|
298
|
+
schematic-pg dev [schema] [--no-watch]
|
|
284
299
|
```
|
|
285
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
|
+
|
|
286
310
|
Equivalent npm scripts in a project created by `init`:
|
|
287
311
|
|
|
288
312
|
```bash
|
|
313
|
+
make dev # docker compose up -d --wait + schematic-pg dev
|
|
289
314
|
npm run dev # schematic-pg dev
|
|
290
315
|
npm run generate # schematic-pg generate
|
|
291
316
|
```
|
|
@@ -11,4 +11,5 @@ export declare function getFilterFieldKind(field: Field, schema: Schema): Filter
|
|
|
11
11
|
export declare function getFilterOperators(kind: FilterFieldKind): FilterOperator[];
|
|
12
12
|
export declare function buildFilterFieldMeta(field: Field, schema: Schema): FilterFieldMeta;
|
|
13
13
|
export declare function queryParamKey(fieldName: string, operator: FilterOperator): string;
|
|
14
|
+
export declare function toQueryBooleanZodType(): string;
|
|
14
15
|
export declare function toFilterZodType(type: TypeExpr, field: Field, schema: Schema, operator: FilterOperator, coerce?: boolean): string;
|
|
@@ -54,6 +54,12 @@ export function queryParamKey(fieldName, operator) {
|
|
|
54
54
|
}
|
|
55
55
|
return `${fieldName}_${operator}`;
|
|
56
56
|
}
|
|
57
|
+
export function toQueryBooleanZodType() {
|
|
58
|
+
return `z.preprocess(
|
|
59
|
+
(value) => (typeof value === 'string' ? value.toLowerCase() : value),
|
|
60
|
+
z.union([z.boolean(), z.enum(['true', 'false'])]).transform((value) => value === true || value === 'true'),
|
|
61
|
+
)`;
|
|
62
|
+
}
|
|
57
63
|
export function toFilterZodType(type, field, schema, operator, coerce = false) {
|
|
58
64
|
const prefix = coerce ? 'z.coerce.' : 'z.';
|
|
59
65
|
const enumType = schema.enums.find((enumDef) => enumDef.name === type.name);
|
|
@@ -77,7 +83,7 @@ export function toFilterZodType(type, field, schema, operator, coerce = false) {
|
|
|
77
83
|
case 'SMALLINT':
|
|
78
84
|
return `${prefix}number().int()`;
|
|
79
85
|
case 'BOOLEAN':
|
|
80
|
-
return
|
|
86
|
+
return toQueryBooleanZodType();
|
|
81
87
|
case 'TIMESTAMP':
|
|
82
88
|
return 'z.coerce.date()';
|
|
83
89
|
case 'DECIMAL':
|
|
@@ -86,8 +86,7 @@ export class ZodSchemaGenerator {
|
|
|
86
86
|
const lines = [];
|
|
87
87
|
for (const operator of meta.operators) {
|
|
88
88
|
const key = queryParamKey(field.name, operator);
|
|
89
|
-
const
|
|
90
|
-
const zodType = toFilterZodType(field.type, field, this.schema, operator, useCoerce);
|
|
89
|
+
const zodType = toFilterZodType(field.type, field, this.schema, operator, true);
|
|
91
90
|
lines.push(`${key}: ${zodType}.optional(),`);
|
|
92
91
|
}
|
|
93
92
|
return lines;
|
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
|
@@ -3,12 +3,13 @@ import { PACKAGE_NAME } from '../constants.js';
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { APP_SCHEMA_TEMPLATE, createPackageJsonTemplate, DOCKER_COMPOSE_TEMPLATE, ENV_TEMPLATE, GITIGNORE_TEMPLATE, HEALTH_ROUTE_TEMPLATE, TSCONFIG_TEMPLATE, } from './templates.js';
|
|
6
|
+
import { APP_SCHEMA_TEMPLATE, createPackageJsonTemplate, DOCKER_COMPOSE_TEMPLATE, ENV_TEMPLATE, GITIGNORE_TEMPLATE, HEALTH_ROUTE_TEMPLATE, MAKEFILE_TEMPLATE, TSCONFIG_TEMPLATE, } from './templates.js';
|
|
7
7
|
const INIT_FILES = [
|
|
8
8
|
{ relativePath: 'app.schema', content: APP_SCHEMA_TEMPLATE },
|
|
9
9
|
{ relativePath: '.env', content: ENV_TEMPLATE },
|
|
10
10
|
{ relativePath: '.gitignore', content: GITIGNORE_TEMPLATE },
|
|
11
11
|
{ relativePath: 'docker-compose.yml', content: DOCKER_COMPOSE_TEMPLATE },
|
|
12
|
+
{ relativePath: 'Makefile', content: MAKEFILE_TEMPLATE },
|
|
12
13
|
{ relativePath: 'tsconfig.json', content: TSCONFIG_TEMPLATE },
|
|
13
14
|
{ relativePath: 'src/routes/health.ts', content: HEALTH_ROUTE_TEMPLATE },
|
|
14
15
|
];
|
|
@@ -101,8 +102,14 @@ export async function runInit(args) {
|
|
|
101
102
|
if (skipInstall) {
|
|
102
103
|
console.log(' npm install');
|
|
103
104
|
}
|
|
104
|
-
console.log('
|
|
105
|
+
console.log(' make dev');
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(' # or run individually:');
|
|
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):');
|
|
105
112
|
console.log(` npx ${PACKAGE_NAME} generate`);
|
|
106
113
|
console.log(` npx ${PACKAGE_NAME} db:bootstrap`);
|
|
107
|
-
console.log(` npx ${PACKAGE_NAME} dev`);
|
|
114
|
+
console.log(` npx ${PACKAGE_NAME} dev --no-watch`);
|
|
108
115
|
}
|
package/dist/cli/templates.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export declare const APP_SCHEMA_TEMPLATE = "
|
|
1
|
+
export declare const APP_SCHEMA_TEMPLATE = "extensions {\n\n}\n\nenums {\n\n}\n\nmodels {\n model User {\n id: UUID @id @default(gen_random_uuid())\n email: VARCHAR(255) @unique\n name: VARCHAR(150)\n createdAt: TIMESTAMP @default(now())\n }\n}\n";
|
|
2
2
|
export declare const ENV_TEMPLATE = "DATABASE_URL=postgresql://postgrest:postgrest@localhost:5432/postgrest\nJWT_SECRET=\nJWT_ROLE_CLAIM=role\nJWT_USER_ID_CLAIM=sub\n";
|
|
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 --wait\n\tnpx schematic-pg dev\n";
|
|
6
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";
|
|
7
8
|
export declare function createPackageJsonTemplate(projectName: string): string;
|
package/dist/cli/templates.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { PACKAGE_NAME, PACKAGE_VERSION } from '../constants.js';
|
|
2
|
-
export const APP_SCHEMA_TEMPLATE = `
|
|
2
|
+
export const APP_SCHEMA_TEMPLATE = `extensions {
|
|
3
|
+
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
enums {
|
|
7
|
+
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
models {
|
|
3
11
|
model User {
|
|
4
12
|
id: UUID @id @default(gen_random_uuid())
|
|
5
13
|
email: VARCHAR(255) @unique
|
|
@@ -54,6 +62,12 @@ export const TSCONFIG_TEMPLATE = `{
|
|
|
54
62
|
"include": ["generated/**/*", "src/**/*"]
|
|
55
63
|
}
|
|
56
64
|
`;
|
|
65
|
+
export const MAKEFILE_TEMPLATE = `.PHONY: dev
|
|
66
|
+
|
|
67
|
+
dev:
|
|
68
|
+
\tdocker compose up -d --wait
|
|
69
|
+
\tnpx ${PACKAGE_NAME} dev
|
|
70
|
+
`;
|
|
57
71
|
export const HEALTH_ROUTE_TEMPLATE = `import { Hono } from 'hono';
|
|
58
72
|
import type { AppEnv } from '${PACKAGE_NAME}/api/types';
|
|
59
73
|
|
|
@@ -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();
|