@onreza/sqlx-js 0.2.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 +73 -29
- package/ROADMAP.md +2 -1
- package/dist/bin/sqlx-js.js +51 -11
- package/dist/src/cache.d.ts +1 -0
- package/dist/src/cache.js +128 -1
- package/dist/src/codegen.d.ts +2 -1
- package/dist/src/codegen.js +36 -17
- 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 +48 -15
- 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 +14 -0
- package/dist/src/pg/functions.d.ts +4 -0
- package/dist/src/pg/functions.js +150 -0
- package/dist/src/pg/oids.js +5 -2
- package/dist/src/pg/param-map.d.ts +2 -1
- package/dist/src/pg/param-map.js +181 -40
- package/dist/src/pg/schema.d.ts +1 -0
- package/dist/src/pg/schema.js +3 -0
- package/package.json +1 -1
package/dist/src/codegen.js
CHANGED
|
@@ -16,36 +16,38 @@ 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();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
const inlinePairs = [];
|
|
35
|
+
for (const e of entries) {
|
|
36
|
+
const emitInline = e.hasInline === true || (e.hasInline !== false && (!e.filePaths || e.filePaths.length === 0));
|
|
37
|
+
if (!emitInline)
|
|
38
|
+
continue;
|
|
39
|
+
const queries = e.inlineQueries && e.inlineQueries.length > 0 ? e.inlineQueries : [e.query];
|
|
40
|
+
for (const query of queries)
|
|
41
|
+
inlinePairs.push({ query, entry: e });
|
|
42
|
+
}
|
|
43
|
+
for (const { query, entry } of inlinePairs.slice().sort((a, b) => a.query.localeCompare(b.query))) {
|
|
44
|
+
if (inlineSeen.has(query))
|
|
43
45
|
continue;
|
|
44
|
-
inlineSeen.add(
|
|
45
|
-
const { params, row } = entrySignature(
|
|
46
|
-
if (
|
|
47
|
-
lines.push(` /** Nullability inference degraded: ${
|
|
48
|
-
lines.push(` ${JSON.stringify(
|
|
46
|
+
inlineSeen.add(query);
|
|
47
|
+
const { params, row } = entrySignature(entry);
|
|
48
|
+
if (entry.degraded)
|
|
49
|
+
lines.push(` /** Nullability inference degraded: ${entry.degraded.reason}. All result columns conservatively typed as nullable. */`);
|
|
50
|
+
lines.push(` ${JSON.stringify(query)}: { params: ${params}; row: ${row} };`);
|
|
49
51
|
}
|
|
50
52
|
lines.push(" }");
|
|
51
53
|
lines.push("");
|
|
@@ -68,5 +70,22 @@ function emitModule(lines, moduleName, entries) {
|
|
|
68
70
|
lines.push(` ${JSON.stringify(path)}: { params: ${params}; row: ${row} };`);
|
|
69
71
|
}
|
|
70
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(" }");
|
|
71
84
|
lines.push("}");
|
|
72
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,10 +6,13 @@ 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]);
|
|
12
14
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
15
|
+
const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
|
|
13
16
|
function enumUnion(values) {
|
|
14
17
|
if (values.length === 0)
|
|
15
18
|
return "never";
|
|
@@ -56,13 +59,28 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
|
56
59
|
if (JSON_OIDS.has(paramOid) || JSON_ARRAY_OIDS.has(paramOid)) {
|
|
57
60
|
const target = paramMap.get(paramIndex);
|
|
58
61
|
if (target) {
|
|
59
|
-
const
|
|
62
|
+
const column = resolveTargetColumn(target, schema);
|
|
63
|
+
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
60
64
|
if (decl)
|
|
61
65
|
return JSON_ARRAY_OIDS.has(paramOid) ? `(${decl})[]` : decl;
|
|
62
66
|
}
|
|
67
|
+
return JSON_ARRAY_OIDS.has(paramOid) ? `(${JSON_INPUT})[]` : JSON_INPUT;
|
|
63
68
|
}
|
|
64
69
|
return resolveTs(paramOid, (oid) => schema.customType(oid));
|
|
65
70
|
}
|
|
71
|
+
function resolveTargetColumn(target, schema) {
|
|
72
|
+
if (target.column)
|
|
73
|
+
return target.column;
|
|
74
|
+
if (target.columnIndex === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
const oid = schema.resolveTable(target.schema, target.table);
|
|
77
|
+
if (oid === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
const cols = schema.columnsOf(oid);
|
|
80
|
+
if (!cols)
|
|
81
|
+
return undefined;
|
|
82
|
+
return [...cols.values()].sort((a, b) => a.attnum - b.attnum)[target.columnIndex - 1]?.name;
|
|
83
|
+
}
|
|
66
84
|
function resolveParamNullable(paramIndex, pm, schema) {
|
|
67
85
|
if (pm.forceNullable.has(paramIndex))
|
|
68
86
|
return true;
|
|
@@ -73,7 +91,10 @@ function resolveParamNullable(paramIndex, pm, schema) {
|
|
|
73
91
|
const oid = schema.resolveTable(t.schema, t.table);
|
|
74
92
|
if (oid === undefined)
|
|
75
93
|
return false;
|
|
76
|
-
const
|
|
94
|
+
const column = resolveTargetColumn(t, schema);
|
|
95
|
+
if (!column)
|
|
96
|
+
return false;
|
|
97
|
+
const col = schema.columnsOf(oid)?.get(column);
|
|
77
98
|
if (!col)
|
|
78
99
|
return false;
|
|
79
100
|
return !col.notNull;
|
|
@@ -100,6 +121,15 @@ function snippet(query, max = 80) {
|
|
|
100
121
|
const oneLine = query.replace(/\s+/g, " ").trim();
|
|
101
122
|
return oneLine.length > max ? oneLine.slice(0, max) + "…" : oneLine;
|
|
102
123
|
}
|
|
124
|
+
function siteUsage(sites) {
|
|
125
|
+
const inlineQueries = Array.from(new Set(sites.filter((s) => s.kind !== "file").map((s) => s.query))).sort();
|
|
126
|
+
const filePaths = Array.from(new Set(sites.filter((s) => s.kind === "file").map((s) => s.sqlFilePath).filter(Boolean))).sort();
|
|
127
|
+
return {
|
|
128
|
+
hasInline: inlineQueries.length > 0,
|
|
129
|
+
...(inlineQueries.length > 0 ? { inlineQueries } : {}),
|
|
130
|
+
...(filePaths.length > 0 ? { filePaths } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
103
133
|
export async function openSession(opts) {
|
|
104
134
|
const userCfg = await loadConfig(opts.root);
|
|
105
135
|
const cfg = parseDatabaseUrl(opts.databaseUrl);
|
|
@@ -284,13 +314,9 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
284
314
|
forceNullable: new Set(),
|
|
285
315
|
dmlBound: new Set(),
|
|
286
316
|
};
|
|
287
|
-
const fileSites = r.sites.filter((s) => s.kind === "file");
|
|
288
|
-
const hasInline = r.sites.some((s) => s.kind !== "file");
|
|
289
|
-
const filePaths = fileSites.length > 0
|
|
290
|
-
? Array.from(new Set(fileSites.map((s) => s.sqlFilePath))).sort()
|
|
291
|
-
: undefined;
|
|
292
317
|
const entry = {
|
|
293
318
|
query: r.query,
|
|
319
|
+
...siteUsage(r.sites),
|
|
294
320
|
paramOids: r.paramOids,
|
|
295
321
|
paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
|
|
296
322
|
paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
|
|
@@ -306,8 +332,6 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
306
332
|
};
|
|
307
333
|
}),
|
|
308
334
|
hasResultSet: r.fields.length > 0,
|
|
309
|
-
hasInline,
|
|
310
|
-
...(filePaths ? { filePaths } : {}),
|
|
311
335
|
...(analysis.degraded ? { degraded: analysis.degraded } : {}),
|
|
312
336
|
};
|
|
313
337
|
cache.write(r.fp, entry);
|
|
@@ -322,8 +346,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
322
346
|
if (pruned > 0)
|
|
323
347
|
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
324
348
|
}
|
|
325
|
-
|
|
326
|
-
|
|
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 };
|
|
327
356
|
}
|
|
328
357
|
export async function runPrepare(opts) {
|
|
329
358
|
if (opts.check) {
|
|
@@ -351,9 +380,13 @@ export async function runPrepare(opts) {
|
|
|
351
380
|
console.error(`\nsqlx-js prepare --check: ${stale} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
|
|
352
381
|
process.exit(1);
|
|
353
382
|
}
|
|
354
|
-
const entries = [...unique.values()].map((u) =>
|
|
355
|
-
|
|
356
|
-
|
|
383
|
+
const entries = [...unique.values()].map((u) => {
|
|
384
|
+
const entry = cache.read(u.fp);
|
|
385
|
+
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
386
|
+
}).filter((e) => e !== null);
|
|
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`);
|
|
357
390
|
return;
|
|
358
391
|
}
|
|
359
392
|
const session = await openSession(opts);
|
|
@@ -364,7 +397,7 @@ export async function runPrepare(opts) {
|
|
|
364
397
|
await session.client.end();
|
|
365
398
|
process.exit(1);
|
|
366
399
|
}
|
|
367
|
-
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}`);
|
|
368
401
|
}
|
|
369
402
|
finally {
|
|
370
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,20 @@ export interface KnownQueries {
|
|
|
4
4
|
}
|
|
5
5
|
export interface KnownFileQueries {
|
|
6
6
|
}
|
|
7
|
+
export interface KnownFunctions {
|
|
8
|
+
}
|
|
9
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
10
|
+
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
|
11
|
+
export type JsonObject = {
|
|
12
|
+
readonly [key: string]: JsonValue;
|
|
13
|
+
};
|
|
14
|
+
export type JsonArray = readonly JsonValue[];
|
|
15
|
+
export type JsonInput = string | number | boolean | JsonInputObject | JsonInputArray;
|
|
16
|
+
export type JsonInputValue = JsonPrimitive | JsonInputObject | JsonInputArray;
|
|
17
|
+
export type JsonInputObject = {
|
|
18
|
+
readonly [key: string]: JsonInputValue | undefined;
|
|
19
|
+
};
|
|
20
|
+
export type JsonInputArray = readonly JsonInputValue[];
|
|
7
21
|
export type { SqlxJsConfig } from "./config.js";
|
|
8
22
|
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
9
23
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|