@onreza/sqlx-js 0.3.0 → 0.4.0
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 +45 -3
- package/ROADMAP.md +2 -1
- package/dist/bin/sqlx-js.js +47 -7
- package/dist/src/codegen.d.ts +2 -1
- package/dist/src/codegen.js +20 -3
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +36 -5
- package/dist/src/commands/pgschema.d.ts +25 -0
- package/dist/src/commands/pgschema.js +178 -0
- package/dist/src/commands/prepare.d.ts +1 -0
- package/dist/src/commands/prepare.js +13 -5
- package/dist/src/config.d.ts +6 -0
- package/dist/src/function-cache.d.ts +18 -0
- package/dist/src/function-cache.js +38 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/pg/functions.d.ts +4 -0
- package/dist/src/pg/functions.js +150 -0
- package/dist/src/pg/oids.js +2 -0
- package/dist/src/pg/schema.d.ts +1 -0
- package/dist/src/pg/schema.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,8 +36,10 @@ const rows = await sql(
|
|
|
36
36
|
- **Linear migrations** with hash tampering detection.
|
|
37
37
|
- **Migration squash baselines** via `migrate squash`: generate a schema-only baseline from a shadow database, then hash-adopt it on already-migrated databases.
|
|
38
38
|
- **Runtime `migrate()`** with PostgreSQL advisory lock, safe for multi-replica startup.
|
|
39
|
+
- **Optional pgschema workflow** via `init --schema-provider pgschema` and `sqlx-js db install|check|plan|apply` for PostgreSQL schema-as-code projects.
|
|
39
40
|
- **Offline cache** committed to your repo. CI verifies via `prepare --check` without a database.
|
|
40
41
|
- **Schema snapshot + LLM manifest** via `schema dump` / `schema check`: tables, columns, constraints, indexes, types, and function/procedure metadata are introspected from PostgreSQL.
|
|
42
|
+
- **Generated function catalog** via `KnownFunctions`: `prepare` records user-schema PostgreSQL functions/procedures from `pg_proc` with approximate parameter and return TypeScript types.
|
|
41
43
|
- **Shadow database validation** via `migrate dev` / `migrate verify`: auto-create a disposable shadow DB, apply migrations, validate SQL, and drop it afterwards.
|
|
42
44
|
- **Safe identifier quoting** via `sql.id(...)`, backed by the committed schema snapshot whitelist.
|
|
43
45
|
- **Single runtime adapter**: Postgres.js backs the runtime on Node/Bun-compatible environments — no Bun.SQL-specific adapter to choose.
|
|
@@ -64,6 +66,16 @@ npx @onreza/sqlx-js init
|
|
|
64
66
|
|
|
65
67
|
Creates `sqlx-js.config.ts`, a `migrations/` directory, and `.env.example` if they don't already exist (it never overwrites existing files), then prints the next steps. Skip it if you prefer to wire things up manually.
|
|
66
68
|
|
|
69
|
+
For declarative PostgreSQL schema management, scaffold the pgschema workflow instead:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx @onreza/sqlx-js init --schema-provider pgschema
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This creates `schema.sql` and configures `schema.provider = "pgschema"` in `sqlx-js.config.ts`. The npm package does not bundle pgschema, but `sqlx-js db install` downloads the pinned pgschema binary into `node_modules/.cache/sqlx-js/pgschema/`; then `sqlx-js db check` verifies it.
|
|
76
|
+
|
|
77
|
+
The managed pgschema workflow supports Linux and macOS. On Windows, run sqlx-js under WSL/Linux/macOS or use the built-in `sqlx-js migrate` workflow.
|
|
78
|
+
|
|
67
79
|
### 1. Configure the database URL
|
|
68
80
|
|
|
69
81
|
```bash
|
|
@@ -334,7 +346,8 @@ In addition to `import { sql } from "@onreza/sqlx-js"`, the scanner recognises `
|
|
|
334
346
|
## CLI
|
|
335
347
|
|
|
336
348
|
```
|
|
337
|
-
sqlx-js init [--root <dir>]
|
|
349
|
+
sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
|
|
350
|
+
sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
|
|
338
351
|
sqlx-js prepare [--check | --watch] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
|
|
339
352
|
sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
|
|
340
353
|
sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
|
|
@@ -362,6 +375,7 @@ Regular `prepare` describes queries across a small connection pool (default 8, o
|
|
|
362
375
|
| `--schema <path>` | Schema snapshot path (default: `<root>/.sqlx-js/schema/schema.json`). |
|
|
363
376
|
| `--manifest <path>` | LLM schema manifest path (default: `<root>/.sqlx-js/schema/schema.md`). |
|
|
364
377
|
| `--no-manifest` | Skip writing the LLM schema manifest during `schema dump`. |
|
|
378
|
+
| `--schema-provider <name>` | For `init`: `builtin` (default) or `pgschema`. |
|
|
365
379
|
|
|
366
380
|
Flags that take a value accept both `--flag value` and `--flag=value` forms.
|
|
367
381
|
|
|
@@ -369,6 +383,20 @@ Flags that take a value accept both `--flag value` and `--flag=value` forms.
|
|
|
369
383
|
|
|
370
384
|
### Development and deployment flows
|
|
371
385
|
|
|
386
|
+
For complex PostgreSQL schemas with functions, triggers, RLS, grants, partitions, and other schema-level objects, prefer pgschema for DDL ownership and use sqlx-js for application-query typing:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
sqlx-js init --schema-provider pgschema
|
|
390
|
+
sqlx-js db install
|
|
391
|
+
sqlx-js db check
|
|
392
|
+
# edit schema.sql
|
|
393
|
+
sqlx-js db plan -- --output-json plan.json
|
|
394
|
+
sqlx-js db apply -- --auto-approve
|
|
395
|
+
sqlx-js prepare
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`sqlx-js db install` installs the pinned pgschema version used by this sqlx-js release. `sqlx-js db check`, `plan`, and `apply` use `schema.command` when configured; otherwise they prefer the managed binary under `node_modules/.cache/sqlx-js/pgschema/` and fall back to `pgschema` on `PATH`. `plan` and file-backed `apply` translate `DATABASE_URL` into `--host`, `--port`, `--db`, `--user`, `--file`, and `--schema` arguments, pass the password through `PGPASSWORD`, pass TLS settings through `PGSSLMODE` / `PGSSLROOTCERT` / `PGSSLCERT` / `PGSSLKEY`, and forward any arguments after `--` directly to pgschema. `sqlx-js db apply -- --plan plan.json` applies a reviewed pgschema plan without requiring the local `schema.sql` file. The schema provider is configured in `sqlx-js.config.ts`; by default the schema file is `schema.sql` and the schema is `public`. The pinned pgschema 1.12.0 CLI accepts a single `--schema` value, so sqlx-js rejects pgschema configs with more than one schema instead of silently applying only one.
|
|
399
|
+
|
|
372
400
|
Use `migrate dev` while developing migrations and SQL:
|
|
373
401
|
|
|
374
402
|
```bash
|
|
@@ -379,6 +407,8 @@ sqlx-js migrate dev
|
|
|
379
407
|
|
|
380
408
|
`migrate dev` creates a disposable shadow database, applies all migrations from scratch, validates that the latest migration's `.down.sql` restores the previous schema (squash baselines may omit `.down.sql`), prepares project SQL against the shadow schema, writes `.sqlx-js/` plus `sqlx-js-env.d.ts`, and drops the shadow database. This means you can keep editing a local WIP migration before it is merged. You do not need to drop your application database or create a new migration for every local edit.
|
|
381
409
|
|
|
410
|
+
The built-in `migrate` workflow is kept for simple projects and embedded application startup. PostgreSQL-heavy schema lifecycle features belong in pgschema rather than in sqlx-js.
|
|
411
|
+
|
|
382
412
|
Use `migrate verify` in PR/CI before merge:
|
|
383
413
|
|
|
384
414
|
```bash
|
|
@@ -449,6 +479,11 @@ Phases reported separately: `describe failed`, `analyze failed`, `paramMap faile
|
|
|
449
479
|
import type { SqlxJsConfig } from "@onreza/sqlx-js";
|
|
450
480
|
|
|
451
481
|
const config: SqlxJsConfig = {
|
|
482
|
+
schema: {
|
|
483
|
+
provider: "pgschema",
|
|
484
|
+
file: "schema.sql",
|
|
485
|
+
schemas: ["public"],
|
|
486
|
+
},
|
|
452
487
|
jsonbTypes: {
|
|
453
488
|
"users.settings": "SqlxJsJson.UserSettings",
|
|
454
489
|
"posts.meta": "SqlxJsJson.PostMeta",
|
|
@@ -459,6 +494,8 @@ const config: SqlxJsConfig = {
|
|
|
459
494
|
export default config;
|
|
460
495
|
```
|
|
461
496
|
|
|
497
|
+
The `schema` block is optional. Use `provider: "pgschema"` when sqlx-js should delegate schema planning/apply commands to pgschema. `command` can override the managed binary lookup and point at another executable. With the pinned pgschema 1.12.0 CLI, `schemas` must contain exactly one schema name.
|
|
498
|
+
|
|
462
499
|
Declare the referenced types anywhere in your project (`.d.ts` file is conventional):
|
|
463
500
|
|
|
464
501
|
```ts
|
|
@@ -535,7 +572,10 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
|
|
|
535
572
|
|
|
536
573
|
```yaml
|
|
537
574
|
- run: bun install
|
|
538
|
-
- run: sqlx-js migrate verify #
|
|
575
|
+
- run: sqlx-js migrate verify # built-in migration workflow
|
|
576
|
+
# or, when schema.provider is "pgschema":
|
|
577
|
+
- run: sqlx-js db install
|
|
578
|
+
- run: sqlx-js db plan -- --output-json plan.json
|
|
539
579
|
- run: sqlx-js prepare --check # fails if any query is missing from the committed cache
|
|
540
580
|
- run: sqlx-js schema check # fails if the committed schema snapshot is stale
|
|
541
581
|
- run: tsc --noEmit # fails if types are stale
|
|
@@ -543,7 +583,9 @@ Commit the generated `sqlx-js-env.d.ts` and the `.sqlx-js/` cache directory to y
|
|
|
543
583
|
- run: bun run build # emits publishable JS + declarations under dist/
|
|
544
584
|
```
|
|
545
585
|
|
|
546
|
-
The `migrate verify` step needs `DATABASE_URL` credentials that can either create a temporary database or use `--shadow-admin-url` / `--shadow-url`. It does not write `.sqlx-js/` or `sqlx-js-env.d.ts`. The `prepare --check` step then runs without a database; your committed offline cache is the source of truth. `schema check` intentionally uses a live database because it verifies the committed schema contract against PostgreSQL.
|
|
586
|
+
The `migrate verify` step needs `DATABASE_URL` credentials that can either create a temporary database or use `--shadow-admin-url` / `--shadow-url`. It does not write `.sqlx-js/` or `sqlx-js-env.d.ts`. For pgschema projects, `sqlx-js db plan` checks the desired `schema.sql` against the target database and leaves application query typing to `prepare`. The `prepare --check` step then runs without a database; your committed offline cache is the source of truth. `schema check` intentionally uses a live database because it verifies the committed schema contract against PostgreSQL.
|
|
587
|
+
|
|
588
|
+
The managed pgschema binary is installed under `node_modules/.cache/sqlx-js/pgschema/`, not `.sqlx-js/`, so it is not part of the committed offline cache.
|
|
547
589
|
|
|
548
590
|
## Contributing
|
|
549
591
|
|
package/ROADMAP.md
CHANGED
|
@@ -6,7 +6,8 @@ Items already shipped live in the [README](./README.md) feature list; this file
|
|
|
6
6
|
|
|
7
7
|
| Feature | ROI | Notes |
|
|
8
8
|
|---------|-----|-------|
|
|
9
|
-
|
|
|
9
|
+
| pgschema integration hardening | 8 | Basic `init --schema-provider pgschema` plus managed `sqlx-js db install/check/plan/apply` exists. Continue with better docs, CI examples, schema snapshot handoff, and migration guidance for projects that outgrow built-in migrations. |
|
|
10
|
+
| Built-in migration lifecycle maintenance | 5 | Keep `migrate run/dev/verify/revert/squash/archive` stable for simple projects and application startup, but avoid expanding it into a full PostgreSQL schema-as-code system. |
|
|
10
11
|
| Prisma migration assistant | 7 | Import Prisma Migrate SQL history and Prisma TypedSQL/raw SQL into `sqlx-js`; classify Prisma Client CRUD/nested-write sites as assisted/manual instead of promising a fully automatic ORM rewrite. |
|
|
11
12
|
| Self-join precision (unqualified ColumnRef) | 4 | `SELECT name FROM users u1 JOIN users u2 ON ...` with unqualified `name` can't be attributed to a specific alias. PG would reject ambiguous unqualified refs anyway, but explicit aliasing currently has no narrowing benefit in self-joins. |
|
|
12
13
|
| `INSERT INTO t VALUES (...)` without column list | 3 | Map params by `pg_attribute attnum` ordering. Rare in practice — most teams use explicit column lists. |
|
package/dist/bin/sqlx-js.js
CHANGED
|
@@ -7,6 +7,8 @@ import { runWatch } from "../src/commands/watch.js";
|
|
|
7
7
|
import { migrateArchiveList, migrateArchiveRestore, migrateCheck, migrateDev, migrateRun, migrateInfo, migrateRevert, migrateAdd, migrateSquash, migrateVerify, } from "../src/commands/migrate.js";
|
|
8
8
|
import { applyShadowMigrations, runSchemaCheck, runSchemaDump } from "../src/commands/schema.js";
|
|
9
9
|
import { runInit } from "../src/commands/init.js";
|
|
10
|
+
import { runPgschemaCommand, runPgschemaInstall } from "../src/commands/pgschema.js";
|
|
11
|
+
import { loadConfig } from "../src/config.js";
|
|
10
12
|
function packageVersion() {
|
|
11
13
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
14
|
for (const path of [join(here, "../package.json"), join(here, "../../package.json")]) {
|
|
@@ -23,7 +25,8 @@ function help() {
|
|
|
23
25
|
console.error(`sqlx-js — compile-time-checked SQL for TypeScript + Postgres (v${VERSION})
|
|
24
26
|
|
|
25
27
|
usage:
|
|
26
|
-
sqlx-js init [--root <dir>]
|
|
28
|
+
sqlx-js init [--root <dir>] [--schema-provider builtin|pgschema]
|
|
29
|
+
sqlx-js db install | check | plan | apply [--root <dir>] [-- <pgschema args>]
|
|
27
30
|
sqlx-js prepare [--check | --watch] [--root <dir>] [--dts <path>] [--no-prune] [--shadow-url <url>]
|
|
28
31
|
sqlx-js migrate dev [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | verify [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | run [--dry-run] [--json] [--lock-timeout <ms>] | info [--json] | check [--json] | revert [--dry-run] [--json] [--shadow-admin-url <url> | --shadow-url <url>] [--lock-timeout <ms>] | add <name> | squash <name> [--shadow-admin-url <url> | --shadow-url <url>] [--replace] [--pg-dump <path>] [--lock-timeout <ms>] | archive list | archive restore <name> [--force]
|
|
29
32
|
sqlx-js schema dump | check [--schema <path>] [--manifest <path>] [--no-manifest] [--shadow-url <url>]
|
|
@@ -52,23 +55,26 @@ flags:
|
|
|
52
55
|
--schema <path> schema snapshot path (default: <root>/.sqlx-js/schema/schema.json)
|
|
53
56
|
--manifest <path> LLM schema manifest path (default: <root>/.sqlx-js/schema/schema.md)
|
|
54
57
|
--no-manifest skip writing the LLM schema manifest during schema dump
|
|
58
|
+
--schema-provider <name> init schema workflow: builtin (default) or pgschema
|
|
55
59
|
`);
|
|
56
60
|
process.exit(2);
|
|
57
61
|
}
|
|
62
|
+
const passthroughIndex = process.argv.indexOf("--");
|
|
63
|
+
const cliArgv = passthroughIndex >= 0 ? process.argv.slice(0, passthroughIndex) : process.argv;
|
|
64
|
+
const passthroughArgs = passthroughIndex >= 0 ? process.argv.slice(passthroughIndex + 1) : [];
|
|
58
65
|
function arg(name, def) {
|
|
59
|
-
const argv = process.argv;
|
|
60
66
|
const eq = `${name}=`;
|
|
61
|
-
for (let i = 0; i <
|
|
62
|
-
const a =
|
|
67
|
+
for (let i = 0; i < cliArgv.length; i++) {
|
|
68
|
+
const a = cliArgv[i];
|
|
63
69
|
if (a === name)
|
|
64
|
-
return
|
|
70
|
+
return cliArgv[i + 1] ?? def;
|
|
65
71
|
if (a.startsWith(eq))
|
|
66
72
|
return a.slice(eq.length);
|
|
67
73
|
}
|
|
68
74
|
return def;
|
|
69
75
|
}
|
|
70
76
|
function flag(name) {
|
|
71
|
-
for (const a of
|
|
77
|
+
for (const a of cliArgv) {
|
|
72
78
|
if (a === name)
|
|
73
79
|
return true;
|
|
74
80
|
}
|
|
@@ -96,7 +102,41 @@ const schemaPath = schemaArg ? resolve(schemaArg) : join(root, ".sqlx-js/schema/
|
|
|
96
102
|
const manifestArg = arg("--manifest");
|
|
97
103
|
const manifestPath = manifestArg ? resolve(manifestArg) : join(root, ".sqlx-js/schema/schema.md");
|
|
98
104
|
if (cmd === "init") {
|
|
99
|
-
|
|
105
|
+
const provider = arg("--schema-provider", "builtin");
|
|
106
|
+
if (provider !== "builtin" && provider !== "pgschema") {
|
|
107
|
+
console.error("--schema-provider must be builtin or pgschema");
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
runInit({ root, schemaProvider: provider });
|
|
111
|
+
}
|
|
112
|
+
else if (cmd === "db") {
|
|
113
|
+
const sub = process.argv[3];
|
|
114
|
+
if (sub === "install") {
|
|
115
|
+
try {
|
|
116
|
+
await runPgschemaInstall({ root });
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error(e.message);
|
|
120
|
+
process.exit(2);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
if (sub !== "check" && sub !== "plan" && sub !== "apply")
|
|
125
|
+
help();
|
|
126
|
+
try {
|
|
127
|
+
runPgschemaCommand({
|
|
128
|
+
root,
|
|
129
|
+
databaseUrl,
|
|
130
|
+
config: await loadConfig(root),
|
|
131
|
+
subcommand: sub,
|
|
132
|
+
passthrough: passthroughArgs,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
console.error(e.message);
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
100
140
|
}
|
|
101
141
|
else if (cmd === "prepare") {
|
|
102
142
|
if (flag("--check") && shadowUrlArg) {
|
package/dist/src/codegen.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { type CacheEntry } from "./cache.js";
|
|
2
|
-
|
|
2
|
+
import type { FunctionEntry } from "./function-cache.js";
|
|
3
|
+
export declare function emitDts(outPath: string, entries: CacheEntry[], functions?: FunctionEntry[]): void;
|
package/dist/src/codegen.js
CHANGED
|
@@ -16,18 +16,18 @@ function entrySignature(e) {
|
|
|
16
16
|
});
|
|
17
17
|
return { params, row: `{ ${cols.join("; ")} }` };
|
|
18
18
|
}
|
|
19
|
-
export function emitDts(outPath, entries) {
|
|
19
|
+
export function emitDts(outPath, entries, functions = []) {
|
|
20
20
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
21
21
|
const lines = [];
|
|
22
22
|
lines.push("// AUTO-GENERATED by sqlx-js. Do not edit.");
|
|
23
23
|
lines.push("// Run `sqlx-js prepare` to regenerate.");
|
|
24
24
|
lines.push("");
|
|
25
|
-
emitModule(lines, "@onreza/sqlx-js", entries);
|
|
25
|
+
emitModule(lines, "@onreza/sqlx-js", entries, functions);
|
|
26
26
|
lines.push("");
|
|
27
27
|
lines.push("export {};");
|
|
28
28
|
writeFileSync(outPath, lines.join("\n") + "\n");
|
|
29
29
|
}
|
|
30
|
-
function emitModule(lines, moduleName, entries) {
|
|
30
|
+
function emitModule(lines, moduleName, entries, functions) {
|
|
31
31
|
lines.push(`declare module ${JSON.stringify(moduleName)} {`);
|
|
32
32
|
lines.push(" interface KnownQueries {");
|
|
33
33
|
const inlineSeen = new Set();
|
|
@@ -70,5 +70,22 @@ function emitModule(lines, moduleName, entries) {
|
|
|
70
70
|
lines.push(` ${JSON.stringify(path)}: { params: ${params}; row: ${row} };`);
|
|
71
71
|
}
|
|
72
72
|
lines.push(" }");
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push(" interface KnownFunctions {");
|
|
75
|
+
const functionSeen = new Set();
|
|
76
|
+
for (const fn of functions.slice().sort((a, b) => a.signature.localeCompare(b.signature))) {
|
|
77
|
+
if (functionSeen.has(fn.signature))
|
|
78
|
+
continue;
|
|
79
|
+
functionSeen.add(fn.signature);
|
|
80
|
+
const params = inputParams(fn);
|
|
81
|
+
lines.push(` ${JSON.stringify(fn.signature)}: { kind: ${JSON.stringify(fn.kind)}; params: ${params}; returns: ${fn.returns}; returnsSet: ${fn.returnsSet} };`);
|
|
82
|
+
}
|
|
83
|
+
lines.push(" }");
|
|
73
84
|
lines.push("}");
|
|
74
85
|
}
|
|
86
|
+
function inputParams(fn) {
|
|
87
|
+
const params = fn.params
|
|
88
|
+
.filter((p) => p.mode === "in" || p.mode === "inout" || p.mode === "variadic")
|
|
89
|
+
.map((p) => p.tsType);
|
|
90
|
+
return params.length === 0 ? "[]" : `[${params.join(", ")}]`;
|
|
91
|
+
}
|
|
@@ -11,6 +11,20 @@ const config: SqlxJsConfig = {
|
|
|
11
11
|
customTypes: {},
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export default config;
|
|
15
|
+
`;
|
|
16
|
+
const PGSCHEMA_CONFIG_TEMPLATE = `import type { SqlxJsConfig } from "@onreza/sqlx-js";
|
|
17
|
+
|
|
18
|
+
const config: SqlxJsConfig = {
|
|
19
|
+
schema: {
|
|
20
|
+
provider: "pgschema",
|
|
21
|
+
file: "schema.sql",
|
|
22
|
+
schemas: ["public"],
|
|
23
|
+
},
|
|
24
|
+
jsonbTypes: {},
|
|
25
|
+
customTypes: {},
|
|
26
|
+
};
|
|
27
|
+
|
|
14
28
|
export default config;
|
|
15
29
|
`;
|
|
16
30
|
const ENV_TEMPLATE = `# Connection string used by sqlx-js prepare/migrate and the runtime.
|
|
@@ -18,6 +32,11 @@ DATABASE_URL=postgres://user:password@localhost:5432/your_db
|
|
|
18
32
|
# Managed Postgres with TLS:
|
|
19
33
|
# DATABASE_URL=postgres://user:password@db.example.com:5432/your_db?sslmode=verify-full
|
|
20
34
|
`;
|
|
35
|
+
const PGSCHEMA_TEMPLATE = `-- Desired PostgreSQL schema managed by pgschema.
|
|
36
|
+
-- Edit this file, then run:
|
|
37
|
+
-- sqlx-js db plan -- --output-json plan.json
|
|
38
|
+
-- sqlx-js db apply -- --auto-approve
|
|
39
|
+
`;
|
|
21
40
|
export function runInit(opts) {
|
|
22
41
|
const log = opts.log ?? console.log;
|
|
23
42
|
const created = [];
|
|
@@ -41,8 +60,12 @@ export function runInit(opts) {
|
|
|
41
60
|
mkdirSync(full, { recursive: true });
|
|
42
61
|
created.push(`${rel}/`);
|
|
43
62
|
};
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
const schemaProvider = opts.schemaProvider ?? "builtin";
|
|
64
|
+
ensureFile("sqlx-js.config.ts", schemaProvider === "pgschema" ? PGSCHEMA_CONFIG_TEMPLATE : CONFIG_TEMPLATE);
|
|
65
|
+
if (schemaProvider === "pgschema")
|
|
66
|
+
ensureFile("schema.sql", PGSCHEMA_TEMPLATE);
|
|
67
|
+
else
|
|
68
|
+
ensureDir("migrations");
|
|
46
69
|
ensureFile(".env.example", ENV_TEMPLATE);
|
|
47
70
|
for (const f of created)
|
|
48
71
|
log(`created ${f}`);
|
|
@@ -51,7 +74,15 @@ export function runInit(opts) {
|
|
|
51
74
|
log("");
|
|
52
75
|
log("Next steps:");
|
|
53
76
|
log(" 1. Set DATABASE_URL (see .env.example).");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
if (schemaProvider === "pgschema") {
|
|
78
|
+
log(" 2. Install managed pgschema: sqlx-js db install");
|
|
79
|
+
log(" 3. Verify it: sqlx-js db check");
|
|
80
|
+
log(" 4. Edit schema.sql, then review: sqlx-js db plan");
|
|
81
|
+
log(" 5. After applying schema changes, run: sqlx-js prepare");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
log(" 2. Add a migration: sqlx-js migrate add init");
|
|
85
|
+
log(" 3. Make sure tsconfig.json \"include\" covers sqlx-js-env.d.ts.");
|
|
86
|
+
log(" 4. Develop locally: sqlx-js migrate dev");
|
|
87
|
+
}
|
|
57
88
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SqlxJsConfig } from "../config.js";
|
|
2
|
+
export type PgschemaSubcommand = "check" | "plan" | "apply";
|
|
3
|
+
export declare const PGSCHEMA_VERSION = "1.12.0";
|
|
4
|
+
export type PgschemaAsset = {
|
|
5
|
+
key: string;
|
|
6
|
+
name: string;
|
|
7
|
+
sha256: string;
|
|
8
|
+
};
|
|
9
|
+
export type PgschemaCommandOptions = {
|
|
10
|
+
root: string;
|
|
11
|
+
databaseUrl: string;
|
|
12
|
+
config: SqlxJsConfig;
|
|
13
|
+
subcommand: PgschemaSubcommand;
|
|
14
|
+
passthrough?: string[];
|
|
15
|
+
};
|
|
16
|
+
export type PgschemaInstallOptions = {
|
|
17
|
+
root: string;
|
|
18
|
+
asset?: PgschemaAsset;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
log?: (msg: string) => void;
|
|
21
|
+
};
|
|
22
|
+
export declare function resolvePgschemaAsset(platform?: NodeJS.Platform, arch?: NodeJS.Architecture): PgschemaAsset;
|
|
23
|
+
export declare function managedPgschemaPath(root: string, asset?: PgschemaAsset): string;
|
|
24
|
+
export declare function runPgschemaInstall(opts: PgschemaInstallOptions): Promise<void>;
|
|
25
|
+
export declare function runPgschemaCommand(opts: PgschemaCommandOptions): void;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { parseDatabaseUrl } from "../pg/wire.js";
|
|
6
|
+
export const PGSCHEMA_VERSION = "1.12.0";
|
|
7
|
+
const PGSCHEMA_BASE_URL = `https://github.com/pgplex/pgschema/releases/download/v${PGSCHEMA_VERSION}`;
|
|
8
|
+
const WINDOWS_UNSUPPORTED = "sqlx-js db: pgschema is not supported on Windows. Run sqlx-js under WSL/Linux/macOS or use the built-in sqlx-js migrate workflow.";
|
|
9
|
+
const PGSCHEMA_ASSETS = {
|
|
10
|
+
"darwin:x64": {
|
|
11
|
+
key: "darwin-amd64",
|
|
12
|
+
name: `pgschema-${PGSCHEMA_VERSION}-darwin-amd64`,
|
|
13
|
+
sha256: "c64b2ac24c4246344908910e892c4123be282bbb449f0b535079ff41d0f47c8f",
|
|
14
|
+
},
|
|
15
|
+
"darwin:arm64": {
|
|
16
|
+
key: "darwin-arm64",
|
|
17
|
+
name: `pgschema-${PGSCHEMA_VERSION}-darwin-arm64`,
|
|
18
|
+
sha256: "f01ea488f21700752d5747bc013c406daa583a68b631739f33af430d5d3ec449",
|
|
19
|
+
},
|
|
20
|
+
"linux:x64": {
|
|
21
|
+
key: "linux-amd64",
|
|
22
|
+
name: `pgschema-${PGSCHEMA_VERSION}-linux-amd64`,
|
|
23
|
+
sha256: "12610adf748b0dafe4e488ee7e9e68e6ffbef1f4e0f038dda36cf0138eede598",
|
|
24
|
+
},
|
|
25
|
+
"linux:arm64": {
|
|
26
|
+
key: "linux-arm64",
|
|
27
|
+
name: `pgschema-${PGSCHEMA_VERSION}-linux-arm64`,
|
|
28
|
+
sha256: "58ec57023954a0239cf9d607c4e5432da6dd0b279399d1c318204120619a221d",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function resolvePgschemaAsset(platform = process.platform, arch = process.arch) {
|
|
32
|
+
if (platform === "win32")
|
|
33
|
+
throw new Error(WINDOWS_UNSUPPORTED);
|
|
34
|
+
const asset = PGSCHEMA_ASSETS[`${platform}:${arch}`];
|
|
35
|
+
if (!asset)
|
|
36
|
+
throw new Error(`sqlx-js db install: unsupported platform ${platform}/${arch}`);
|
|
37
|
+
return asset;
|
|
38
|
+
}
|
|
39
|
+
function pgschemaConfig(config) {
|
|
40
|
+
if (config.schema?.provider !== "pgschema") {
|
|
41
|
+
throw new Error("sqlx-js db: set schema.provider = \"pgschema\" in sqlx-js.config.ts");
|
|
42
|
+
}
|
|
43
|
+
return config.schema;
|
|
44
|
+
}
|
|
45
|
+
export function managedPgschemaPath(root, asset = resolvePgschemaAsset()) {
|
|
46
|
+
return join(root, "node_modules/.cache/sqlx-js/pgschema", `v${PGSCHEMA_VERSION}`, asset.key, "pgschema");
|
|
47
|
+
}
|
|
48
|
+
function maybeManagedPgschemaPath(root) {
|
|
49
|
+
try {
|
|
50
|
+
const asset = resolvePgschemaAsset();
|
|
51
|
+
const managed = managedPgschemaPath(root, asset);
|
|
52
|
+
if (!existsSync(managed))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (sha256(readFileSync(managed)) !== asset.sha256) {
|
|
55
|
+
throw new Error(`sqlx-js db: managed pgschema checksum mismatch at ${managed}. Run sqlx-js db install.`);
|
|
56
|
+
}
|
|
57
|
+
chmodSync(managed, 0o755);
|
|
58
|
+
return managed;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e.message.includes("checksum mismatch"))
|
|
62
|
+
throw e;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function commandName(root, config) {
|
|
67
|
+
if (process.platform === "win32")
|
|
68
|
+
throw new Error(WINDOWS_UNSUPPORTED);
|
|
69
|
+
if (config.command)
|
|
70
|
+
return config.command;
|
|
71
|
+
const managed = maybeManagedPgschemaPath(root);
|
|
72
|
+
if (managed && existsSync(managed))
|
|
73
|
+
return managed;
|
|
74
|
+
return "pgschema";
|
|
75
|
+
}
|
|
76
|
+
function schemaFile(root, config) {
|
|
77
|
+
return resolve(root, config.file ?? "schema.sql");
|
|
78
|
+
}
|
|
79
|
+
function appliesPlan(subcommand, passthrough) {
|
|
80
|
+
return subcommand === "apply" && (passthrough ?? []).some((arg) => arg === "--plan" || arg.startsWith("--plan="));
|
|
81
|
+
}
|
|
82
|
+
function installHint(command) {
|
|
83
|
+
return `sqlx-js db: ${command} was not found. Run sqlx-js db install or set schema.command in sqlx-js.config.ts.`;
|
|
84
|
+
}
|
|
85
|
+
function run(command, args, env) {
|
|
86
|
+
const child = spawnSync(command, args, { encoding: "utf8", env, stdio: "inherit" });
|
|
87
|
+
if (child.error) {
|
|
88
|
+
const code = child.error.code;
|
|
89
|
+
if (code === "ENOENT")
|
|
90
|
+
throw new Error(installHint(command));
|
|
91
|
+
throw child.error;
|
|
92
|
+
}
|
|
93
|
+
if (child.signal)
|
|
94
|
+
throw new Error(`sqlx-js db: ${command} terminated by signal ${child.signal}`);
|
|
95
|
+
if (child.status && child.status !== 0)
|
|
96
|
+
process.exit(child.status);
|
|
97
|
+
}
|
|
98
|
+
function pgschemaEnv(db) {
|
|
99
|
+
return {
|
|
100
|
+
...process.env,
|
|
101
|
+
...(db.password ? { PGPASSWORD: db.password } : {}),
|
|
102
|
+
...(db.sslmode ? { PGSSLMODE: db.sslmode } : {}),
|
|
103
|
+
...(db.sslRootCert ? { PGSSLROOTCERT: db.sslRootCert } : {}),
|
|
104
|
+
...(db.sslCert ? { PGSSLCERT: db.sslCert } : {}),
|
|
105
|
+
...(db.sslKey ? { PGSSLKEY: db.sslKey } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function sha256(data) {
|
|
109
|
+
return createHash("sha256").update(data).digest("hex");
|
|
110
|
+
}
|
|
111
|
+
export async function runPgschemaInstall(opts) {
|
|
112
|
+
const asset = opts.asset ?? resolvePgschemaAsset();
|
|
113
|
+
const baseUrl = opts.baseUrl ?? PGSCHEMA_BASE_URL;
|
|
114
|
+
const log = opts.log ?? console.log;
|
|
115
|
+
const target = managedPgschemaPath(opts.root, asset);
|
|
116
|
+
if (existsSync(target) && sha256(readFileSync(target)) === asset.sha256) {
|
|
117
|
+
chmodSync(target, 0o755);
|
|
118
|
+
log(`pgschema v${PGSCHEMA_VERSION} already installed at ${target}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const url = `${baseUrl.replace(/\/$/, "")}/${asset.name}`;
|
|
122
|
+
const response = await fetch(url);
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`sqlx-js db install: failed to download pgschema v${PGSCHEMA_VERSION}: HTTP ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
127
|
+
const actual = sha256(bytes);
|
|
128
|
+
if (actual !== asset.sha256) {
|
|
129
|
+
throw new Error(`sqlx-js db install: checksum mismatch for ${asset.name}`);
|
|
130
|
+
}
|
|
131
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
132
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
133
|
+
try {
|
|
134
|
+
writeFileSync(tmp, bytes, { mode: 0o755 });
|
|
135
|
+
chmodSync(tmp, 0o755);
|
|
136
|
+
renameSync(tmp, target);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
rmSync(tmp, { force: true });
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
142
|
+
writeFileSync(`${target}.json`, JSON.stringify({ version: PGSCHEMA_VERSION, asset: asset.name, sha256: asset.sha256 }, null, 2) + "\n");
|
|
143
|
+
log(`installed pgschema v${PGSCHEMA_VERSION} to ${target}`);
|
|
144
|
+
}
|
|
145
|
+
export function runPgschemaCommand(opts) {
|
|
146
|
+
const config = pgschemaConfig(opts.config);
|
|
147
|
+
const command = commandName(opts.root, config);
|
|
148
|
+
if (opts.subcommand === "check") {
|
|
149
|
+
run(command, ["--help"], process.env);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!opts.databaseUrl)
|
|
153
|
+
throw new Error("DATABASE_URL is required for sqlx-js db commands");
|
|
154
|
+
const file = appliesPlan(opts.subcommand, opts.passthrough) ? undefined : schemaFile(opts.root, config);
|
|
155
|
+
if (file && !existsSync(file))
|
|
156
|
+
throw new Error(`sqlx-js db: schema file not found: ${file}`);
|
|
157
|
+
const db = parseDatabaseUrl(opts.databaseUrl);
|
|
158
|
+
const schemas = pgschemaSchemas(config);
|
|
159
|
+
const args = [
|
|
160
|
+
opts.subcommand,
|
|
161
|
+
"--host", db.host,
|
|
162
|
+
"--port", String(db.port),
|
|
163
|
+
"--db", db.database,
|
|
164
|
+
"--user", db.user,
|
|
165
|
+
];
|
|
166
|
+
if (file)
|
|
167
|
+
args.push("--file", file);
|
|
168
|
+
args.push("--schema", schemas[0]);
|
|
169
|
+
args.push(...opts.passthrough ?? []);
|
|
170
|
+
run(command, args, pgschemaEnv(db));
|
|
171
|
+
}
|
|
172
|
+
function pgschemaSchemas(config) {
|
|
173
|
+
const schemas = config.schemas?.length ? config.schemas : ["public"];
|
|
174
|
+
if (schemas.length > 1) {
|
|
175
|
+
throw new Error("sqlx-js db: pgschema 1.12.0 supports exactly one --schema value; split plan/apply per schema or use a single schema in sqlx-js.config.ts");
|
|
176
|
+
}
|
|
177
|
+
return schemas;
|
|
178
|
+
}
|
|
@@ -6,6 +6,8 @@ import { scanProject } from "../scan/scanner.js";
|
|
|
6
6
|
import { Cache, fingerprint, effectiveNullable } from "../cache.js";
|
|
7
7
|
import { emitDts } from "../codegen.js";
|
|
8
8
|
import { loadConfig, lookupJsonbType } from "../config.js";
|
|
9
|
+
import { readFunctionCache, writeFunctionCache } from "../function-cache.js";
|
|
10
|
+
import { introspectFunctions } from "../pg/functions.js";
|
|
9
11
|
import { buildParamMap } from "../pg/param-map.js";
|
|
10
12
|
import { mergeExtensionTypes } from "../pg/extensions.js";
|
|
11
13
|
const JSON_OIDS = new Set([114, 3802]);
|
|
@@ -344,8 +346,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
344
346
|
if (pruned > 0)
|
|
345
347
|
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
346
348
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
let functions = [];
|
|
350
|
+
if (failures === 0) {
|
|
351
|
+
functions = await introspectFunctions(client, schema);
|
|
352
|
+
writeFunctionCache(opts.cacheDir, functions);
|
|
353
|
+
}
|
|
354
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
355
|
+
return { entries: entries.length, failures, pruned, functions: functions.length };
|
|
349
356
|
}
|
|
350
357
|
export async function runPrepare(opts) {
|
|
351
358
|
if (opts.check) {
|
|
@@ -377,8 +384,9 @@ export async function runPrepare(opts) {
|
|
|
377
384
|
const entry = cache.read(u.fp);
|
|
378
385
|
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
379
386
|
}).filter((e) => e !== null);
|
|
380
|
-
|
|
381
|
-
|
|
387
|
+
const functions = readFunctionCache(opts.cacheDir);
|
|
388
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
389
|
+
console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
|
|
382
390
|
return;
|
|
383
391
|
}
|
|
384
392
|
const session = await openSession(opts);
|
|
@@ -389,7 +397,7 @@ export async function runPrepare(opts) {
|
|
|
389
397
|
await session.client.end();
|
|
390
398
|
process.exit(1);
|
|
391
399
|
}
|
|
392
|
-
console.log(`\nprepared ${r.entries} unique query/queries → ${opts.dtsPath}`);
|
|
400
|
+
console.log(`\nprepared ${r.entries} unique query/queries, ${r.functions} function(s) → ${opts.dtsPath}`);
|
|
393
401
|
}
|
|
394
402
|
finally {
|
|
395
403
|
await session.client.end();
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export type SqlxJsConfig = {
|
|
2
2
|
jsonbTypes?: Record<string, string>;
|
|
3
3
|
customTypes?: Record<string, string>;
|
|
4
|
+
schema?: {
|
|
5
|
+
provider?: "builtin" | "pgschema";
|
|
6
|
+
file?: string;
|
|
7
|
+
schemas?: string[];
|
|
8
|
+
command?: string;
|
|
9
|
+
};
|
|
4
10
|
};
|
|
5
11
|
export declare function loadConfig(root: string): Promise<SqlxJsConfig>;
|
|
6
12
|
export declare function lookupJsonbType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type FunctionKind = "function" | "procedure" | "aggregate" | "window";
|
|
2
|
+
export type FunctionParamMode = "in" | "out" | "inout" | "variadic" | "table";
|
|
3
|
+
export type FunctionParamEntry = {
|
|
4
|
+
mode: FunctionParamMode;
|
|
5
|
+
tsType: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
};
|
|
8
|
+
export type FunctionEntry = {
|
|
9
|
+
schema: string;
|
|
10
|
+
name: string;
|
|
11
|
+
signature: string;
|
|
12
|
+
kind: FunctionKind;
|
|
13
|
+
params: FunctionParamEntry[];
|
|
14
|
+
returns: string;
|
|
15
|
+
returnsSet: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function readFunctionCache(cacheDir: string): FunctionEntry[];
|
|
18
|
+
export declare function writeFunctionCache(cacheDir: string, functions: FunctionEntry[]): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
function cachePath(cacheDir) {
|
|
5
|
+
return join(cacheDir, "functions", "functions.json");
|
|
6
|
+
}
|
|
7
|
+
function parseFunctionCache(raw) {
|
|
8
|
+
if (!raw || typeof raw !== "object")
|
|
9
|
+
return [];
|
|
10
|
+
const obj = raw;
|
|
11
|
+
if (obj.version !== 1 || !Array.isArray(obj.functions))
|
|
12
|
+
return [];
|
|
13
|
+
return obj.functions;
|
|
14
|
+
}
|
|
15
|
+
export function readFunctionCache(cacheDir) {
|
|
16
|
+
const path = cachePath(cacheDir);
|
|
17
|
+
if (!existsSync(path))
|
|
18
|
+
return [];
|
|
19
|
+
const text = readFileSync(path, "utf8");
|
|
20
|
+
return parseFunctionCache(JSON.parse(text));
|
|
21
|
+
}
|
|
22
|
+
export function writeFunctionCache(cacheDir, functions) {
|
|
23
|
+
const path = cachePath(cacheDir);
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
const payload = { version: 1, functions };
|
|
26
|
+
const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
|
|
27
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2));
|
|
28
|
+
try {
|
|
29
|
+
renameSync(tmp, path);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
try {
|
|
33
|
+
unlinkSync(tmp);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface KnownQueries {
|
|
|
4
4
|
}
|
|
5
5
|
export interface KnownFileQueries {
|
|
6
6
|
}
|
|
7
|
+
export interface KnownFunctions {
|
|
8
|
+
}
|
|
7
9
|
export type JsonPrimitive = string | number | boolean | null;
|
|
8
10
|
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
|
9
11
|
export type JsonObject = {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { decodeText } from "./wire.js";
|
|
2
|
+
const JSON_OIDS = new Set([114, 3802]);
|
|
3
|
+
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
4
|
+
const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
|
|
5
|
+
export async function introspectFunctions(client, schema) {
|
|
6
|
+
const rows = await loadFunctionRows(client);
|
|
7
|
+
const typeOids = new Set();
|
|
8
|
+
for (const row of rows) {
|
|
9
|
+
typeOids.add(row.returnOid);
|
|
10
|
+
for (const oid of row.inputArgOids)
|
|
11
|
+
typeOids.add(oid);
|
|
12
|
+
for (const oid of row.allArgOids ?? [])
|
|
13
|
+
typeOids.add(oid);
|
|
14
|
+
}
|
|
15
|
+
await schema.loadCustomTypes([...typeOids]);
|
|
16
|
+
return rows.map((row) => toEntry(row, schema)).sort((a, b) => a.signature.localeCompare(b.signature));
|
|
17
|
+
}
|
|
18
|
+
async function loadFunctionRows(client) {
|
|
19
|
+
const result = await client.simpleQueryAll(`
|
|
20
|
+
SELECT
|
|
21
|
+
n.nspname,
|
|
22
|
+
p.proname,
|
|
23
|
+
p.prokind::text,
|
|
24
|
+
pg_get_function_identity_arguments(p.oid),
|
|
25
|
+
to_json(
|
|
26
|
+
CASE
|
|
27
|
+
WHEN p.proargtypes::text = '' THEN ARRAY[]::oid[]
|
|
28
|
+
ELSE string_to_array(p.proargtypes::text, ' ')::oid[]
|
|
29
|
+
END
|
|
30
|
+
)::text,
|
|
31
|
+
to_json(p.proallargtypes)::text,
|
|
32
|
+
to_json(p.proargmodes)::text,
|
|
33
|
+
to_json(p.proargnames)::text,
|
|
34
|
+
p.prorettype::int8,
|
|
35
|
+
p.proretset
|
|
36
|
+
FROM pg_proc p
|
|
37
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
38
|
+
WHERE ${userSchemaFilter("n")}
|
|
39
|
+
ORDER BY n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)
|
|
40
|
+
`);
|
|
41
|
+
return result.rows.map((row) => ({
|
|
42
|
+
schema: decodeText(row[0]),
|
|
43
|
+
name: decodeText(row[1]),
|
|
44
|
+
kind: functionKind(decodeText(row[2])),
|
|
45
|
+
identityArguments: decodeText(row[3]) ?? "",
|
|
46
|
+
inputArgOids: parseNumberJsonArray(decodeText(row[4])),
|
|
47
|
+
allArgOids: parseNullableNumberJsonArray(decodeText(row[5] ?? null)),
|
|
48
|
+
argModes: parseNullableStringJsonArray(decodeText(row[6] ?? null)),
|
|
49
|
+
argNames: parseNullableStringJsonArray(decodeText(row[7] ?? null)),
|
|
50
|
+
returnOid: Number(decodeText(row[8])),
|
|
51
|
+
returnsSet: decodeText(row[9]) === "t",
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
function toEntry(row, schema) {
|
|
55
|
+
const allArgOids = row.allArgOids ?? row.inputArgOids;
|
|
56
|
+
const modes = row.argModes ?? allArgOids.map(() => "i");
|
|
57
|
+
const params = allArgOids.map((oid, i) => {
|
|
58
|
+
const mode = paramMode(modes[i]);
|
|
59
|
+
const resultTsType = outputTsType(oid, schema);
|
|
60
|
+
return {
|
|
61
|
+
mode,
|
|
62
|
+
tsType: mode === "out" || mode === "table" ? resultTsType : inputTsType(oid, schema),
|
|
63
|
+
...(mode === "inout" ? { resultTsType } : {}),
|
|
64
|
+
...(row.argNames?.[i] ? { name: row.argNames[i] } : {}),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
const output = params.filter((p) => p.mode === "out" || p.mode === "inout" || p.mode === "table");
|
|
68
|
+
return {
|
|
69
|
+
schema: row.schema,
|
|
70
|
+
name: row.name,
|
|
71
|
+
signature: `${row.schema}.${row.name}(${row.identityArguments})`,
|
|
72
|
+
kind: row.kind,
|
|
73
|
+
params: params.map(persistedParam),
|
|
74
|
+
returns: returnTsType(row, output, schema),
|
|
75
|
+
returnsSet: row.returnsSet,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function persistedParam(param) {
|
|
79
|
+
return {
|
|
80
|
+
mode: param.mode,
|
|
81
|
+
tsType: param.tsType,
|
|
82
|
+
...(param.name ? { name: param.name } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function returnTsType(row, output, schema) {
|
|
86
|
+
if (output.length > 0)
|
|
87
|
+
return outputObject(output);
|
|
88
|
+
if (row.kind === "procedure")
|
|
89
|
+
return "void";
|
|
90
|
+
return nullableReturn(schema.tsType(row.returnOid));
|
|
91
|
+
}
|
|
92
|
+
function outputObject(output) {
|
|
93
|
+
const fields = output.map((p, i) => {
|
|
94
|
+
const name = p.name && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(p.name) ? p.name : JSON.stringify(p.name ?? `column${i + 1}`);
|
|
95
|
+
return `${name}: ${nullableReturn(p.resultTsType ?? p.tsType)}`;
|
|
96
|
+
});
|
|
97
|
+
return `{ ${fields.join("; ")} }`;
|
|
98
|
+
}
|
|
99
|
+
function inputTsType(oid, schema) {
|
|
100
|
+
if (JSON_OIDS.has(oid))
|
|
101
|
+
return JSON_INPUT;
|
|
102
|
+
if (JSON_ARRAY_OIDS.has(oid))
|
|
103
|
+
return `(${JSON_INPUT})[]`;
|
|
104
|
+
return schema.tsType(oid);
|
|
105
|
+
}
|
|
106
|
+
function outputTsType(oid, schema) {
|
|
107
|
+
return schema.tsType(oid);
|
|
108
|
+
}
|
|
109
|
+
function nullableReturn(tsType) {
|
|
110
|
+
if (tsType === "void")
|
|
111
|
+
return tsType;
|
|
112
|
+
return `${tsType} | null`;
|
|
113
|
+
}
|
|
114
|
+
function parseNumberJsonArray(raw) {
|
|
115
|
+
if (!raw)
|
|
116
|
+
return [];
|
|
117
|
+
const parsed = JSON.parse(raw);
|
|
118
|
+
return Array.isArray(parsed) ? parsed.map(Number).filter((n) => Number.isFinite(n)) : [];
|
|
119
|
+
}
|
|
120
|
+
function parseNullableNumberJsonArray(raw) {
|
|
121
|
+
if (raw === null)
|
|
122
|
+
return null;
|
|
123
|
+
return parseNumberJsonArray(raw);
|
|
124
|
+
}
|
|
125
|
+
function parseNullableStringJsonArray(raw) {
|
|
126
|
+
if (raw === null)
|
|
127
|
+
return null;
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
return Array.isArray(parsed) ? parsed.map((v) => (typeof v === "string" ? v : "")) : [];
|
|
130
|
+
}
|
|
131
|
+
function paramMode(raw) {
|
|
132
|
+
switch (raw) {
|
|
133
|
+
case "o": return "out";
|
|
134
|
+
case "b": return "inout";
|
|
135
|
+
case "v": return "variadic";
|
|
136
|
+
case "t": return "table";
|
|
137
|
+
default: return "in";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function functionKind(raw) {
|
|
141
|
+
switch (raw) {
|
|
142
|
+
case "p": return "procedure";
|
|
143
|
+
case "a": return "aggregate";
|
|
144
|
+
case "w": return "window";
|
|
145
|
+
default: return "function";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function userSchemaFilter(alias) {
|
|
149
|
+
return `${alias}.nspname <> 'information_schema' AND ${alias}.nspname NOT LIKE 'pg\\_%' ESCAPE '\\'`;
|
|
150
|
+
}
|
package/dist/src/pg/oids.js
CHANGED
package/dist/src/pg/schema.d.ts
CHANGED
package/dist/src/pg/schema.js
CHANGED