@onreza/sqlx-js 0.3.0 → 0.5.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 +129 -36
- package/ROADMAP.md +3 -4
- package/dist/bin/sqlx-js.js +137 -26
- package/dist/src/artifacts.d.ts +9 -0
- package/dist/src/artifacts.js +26 -0
- package/dist/src/cache.d.ts +17 -0
- package/dist/src/cache.js +83 -1
- package/dist/src/codegen.d.ts +2 -1
- package/dist/src/codegen.js +34 -5
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +196 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +36 -9
- package/dist/src/commands/migrate.js +7 -22
- package/dist/src/commands/pgschema.d.ts +31 -0
- package/dist/src/commands/pgschema.js +208 -0
- package/dist/src/commands/prepare.d.ts +40 -5
- package/dist/src/commands/prepare.js +344 -58
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +27 -0
- package/dist/src/config.js +141 -11
- package/dist/src/function-cache.d.ts +18 -0
- package/dist/src/function-cache.js +38 -0
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.js +2 -1
- 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/dist/src/postgres-runtime.d.ts +3 -0
- package/dist/src/postgres-runtime.js +66 -29
- package/dist/src/runtime.d.ts +36 -3
- package/dist/src/runtime.js +91 -22
- package/dist/src/scan/scanner.d.ts +9 -2
- package/dist/src/scan/scanner.js +79 -28
- package/dist/src/typed.d.ts +10 -0
- package/package.json +6 -5
package/dist/src/config.js
CHANGED
|
@@ -1,18 +1,148 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
export function defineConfig(config) {
|
|
7
|
+
return config;
|
|
8
|
+
}
|
|
9
|
+
export function loadRootEnv(root) {
|
|
10
|
+
const path = join(root, ".env");
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return undefined;
|
|
13
|
+
const parsed = parseEnv(readFileSync(path, "utf8"));
|
|
14
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
15
|
+
if (process.env[key] === undefined)
|
|
16
|
+
process.env[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
export function configPath(root) {
|
|
21
|
+
for (const name of ["sqlx-js.config.mts", "sqlx-js.config.ts", "sqlx-js.config.mjs", "sqlx-js.config.js"]) {
|
|
5
22
|
const p = join(root, name);
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
if (existsSync(p))
|
|
24
|
+
return p;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export async function loadConfig(root) {
|
|
29
|
+
const path = configPath(root);
|
|
30
|
+
if (!path)
|
|
31
|
+
return {};
|
|
32
|
+
const url = pathToFileURL(path);
|
|
33
|
+
url.searchParams.set("mtime", String(statSync(path).mtimeMs));
|
|
34
|
+
const mod = await import(url.href);
|
|
35
|
+
if (!("default" in mod)) {
|
|
36
|
+
throw new Error(`sqlx-js: ${path} must default-export a config object`);
|
|
37
|
+
}
|
|
38
|
+
return validateConfig(mod.default, path);
|
|
39
|
+
}
|
|
40
|
+
function validateStringRecord(value, name, path) {
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
42
|
+
throw new Error(`sqlx-js: ${path} ${name} must be an object of string values`);
|
|
43
|
+
}
|
|
44
|
+
for (const [key, item] of Object.entries(value)) {
|
|
45
|
+
if (typeof item !== "string") {
|
|
46
|
+
throw new Error(`sqlx-js: ${path} ${name}.${key} must be a string`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function validateStringArray(value, name, path) {
|
|
51
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
52
|
+
throw new Error(`sqlx-js: ${path} ${name} must be an array of strings`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateConfig(value, path) {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
57
|
+
throw new Error(`sqlx-js: ${path} must default-export a config object`);
|
|
58
|
+
}
|
|
59
|
+
const config = value;
|
|
60
|
+
if (config.jsonbTypes !== undefined)
|
|
61
|
+
validateStringRecord(config.jsonbTypes, "jsonbTypes", path);
|
|
62
|
+
if (config.customTypes !== undefined)
|
|
63
|
+
validateStringRecord(config.customTypes, "customTypes", path);
|
|
64
|
+
if (config.scan !== undefined) {
|
|
65
|
+
if (!config.scan || typeof config.scan !== "object" || Array.isArray(config.scan)) {
|
|
66
|
+
throw new Error(`sqlx-js: ${path} scan must be an object`);
|
|
12
67
|
}
|
|
13
|
-
|
|
68
|
+
const scan = config.scan;
|
|
69
|
+
if (scan.include !== undefined)
|
|
70
|
+
validateStringArray(scan.include, "scan.include", path);
|
|
71
|
+
if (scan.exclude !== undefined)
|
|
72
|
+
validateStringArray(scan.exclude, "scan.exclude", path);
|
|
14
73
|
}
|
|
15
|
-
|
|
74
|
+
if (config.schema !== undefined) {
|
|
75
|
+
if (!config.schema || typeof config.schema !== "object" || Array.isArray(config.schema)) {
|
|
76
|
+
throw new Error(`sqlx-js: ${path} schema must be an object`);
|
|
77
|
+
}
|
|
78
|
+
const schema = config.schema;
|
|
79
|
+
if (schema.provider !== undefined && schema.provider !== "builtin" && schema.provider !== "pgschema") {
|
|
80
|
+
throw new Error(`sqlx-js: ${path} schema.provider must be builtin or pgschema`);
|
|
81
|
+
}
|
|
82
|
+
for (const key of ["file", "command"]) {
|
|
83
|
+
if (schema[key] !== undefined && typeof schema[key] !== "string") {
|
|
84
|
+
throw new Error(`sqlx-js: ${path} schema.${key} must be a string`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (schema.schemas !== undefined)
|
|
88
|
+
validateStringArray(schema.schemas, "schema.schemas", path);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
export function prepareConfigHash(cfg) {
|
|
93
|
+
const value = stableValue({
|
|
94
|
+
jsonbTypes: cfg.jsonbTypes ?? {},
|
|
95
|
+
customTypes: cfg.customTypes ?? {},
|
|
96
|
+
});
|
|
97
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
export function configHash(cfg) {
|
|
100
|
+
return createHash("sha256").update(JSON.stringify(stableValue(cfg))).digest("hex").slice(0, 16);
|
|
101
|
+
}
|
|
102
|
+
function stableValue(value) {
|
|
103
|
+
if (Array.isArray(value))
|
|
104
|
+
return value.map(stableValue);
|
|
105
|
+
if (!value || typeof value !== "object")
|
|
106
|
+
return value;
|
|
107
|
+
return Object.fromEntries(Object.entries(value)
|
|
108
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
109
|
+
.map(([key, item]) => [key, stableValue(item)]));
|
|
110
|
+
}
|
|
111
|
+
export async function loadConfigInfo(root) {
|
|
112
|
+
const path = configPath(root);
|
|
113
|
+
if (!path)
|
|
114
|
+
return { config: {} };
|
|
115
|
+
return { config: await loadConfig(root), path };
|
|
116
|
+
}
|
|
117
|
+
export function assertSupportedRuntime() {
|
|
118
|
+
const bun = process.versions.bun;
|
|
119
|
+
if (bun) {
|
|
120
|
+
if (majorMinorLessThan(bun, 1, 3)) {
|
|
121
|
+
throw new Error(`sqlx-js requires Bun >=1.3, current ${bun}`);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const node = process.versions.node;
|
|
126
|
+
if (majorMinorLessThan(node, 24, 0)) {
|
|
127
|
+
throw new Error(`sqlx-js requires Node.js >=24, current ${node}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function majorMinorLessThan(version, minMajor, minMinor) {
|
|
131
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
132
|
+
return major < minMajor || (major === minMajor && minor < minMinor);
|
|
133
|
+
}
|
|
134
|
+
export function runtimeVersion() {
|
|
135
|
+
if (process.versions.bun)
|
|
136
|
+
return { runtime: "bun", version: process.versions.bun };
|
|
137
|
+
return { runtime: "node", version: process.versions.node };
|
|
138
|
+
}
|
|
139
|
+
export function nativeTypeScriptEnabled() {
|
|
140
|
+
if (process.versions.bun)
|
|
141
|
+
return true;
|
|
142
|
+
return process.features.typescript;
|
|
143
|
+
}
|
|
144
|
+
export function envFilePath(root) {
|
|
145
|
+
return join(root, ".env");
|
|
16
146
|
}
|
|
17
147
|
export function lookupJsonbType(cfg, schema, table, column) {
|
|
18
148
|
const types = cfg.jsonbTypes;
|
|
@@ -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 = {
|
|
@@ -16,11 +18,13 @@ export type JsonInputObject = {
|
|
|
16
18
|
readonly [key: string]: JsonInputValue | undefined;
|
|
17
19
|
};
|
|
18
20
|
export type JsonInputArray = readonly JsonInputValue[];
|
|
19
|
-
export
|
|
21
|
+
export { defineConfig } from "./config.js";
|
|
22
|
+
export type { ScanConfig, SqlxJsConfig } from "./config.js";
|
|
20
23
|
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
21
24
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
22
25
|
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
23
26
|
export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook } from "./runtime.js";
|
|
27
|
+
export type { ExecuteResult, JsonParameter, PgArrayParameter } from "./runtime.js";
|
|
24
28
|
export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
|
|
25
29
|
export type TypedFile = TypedFileFor<KnownFileQueries>;
|
|
26
30
|
export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
|
|
@@ -32,4 +36,4 @@ export declare const getClient: typeof rt.getClient;
|
|
|
32
36
|
export declare const setClient: typeof rt.setClient;
|
|
33
37
|
export declare const createClient: typeof rt.createClient;
|
|
34
38
|
export declare const close: typeof rt.close;
|
|
35
|
-
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
|
|
39
|
+
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as rt from "./postgres-runtime.js";
|
|
2
|
+
export { defineConfig } from "./config.js";
|
|
2
3
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
3
4
|
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
4
5
|
export const sql = rt.sql;
|
|
@@ -7,4 +8,4 @@ export const getClient = rt.getClient;
|
|
|
7
8
|
export const setClient = rt.setClient;
|
|
8
9
|
export const createClient = rt.createClient;
|
|
9
10
|
export const close = rt.close;
|
|
10
|
-
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
|
|
11
|
+
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id, json, array } from "./runtime.js";
|
|
@@ -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
|
@@ -7,11 +7,14 @@ export type PostgresOptions = postgres.Options<Record<string, postgres.PostgresT
|
|
|
7
7
|
export type CreateClientOptions = PostgresOptions & {
|
|
8
8
|
onQuery?: OnQueryHook;
|
|
9
9
|
statementTimeoutMs?: number;
|
|
10
|
+
fileRoot?: string;
|
|
10
11
|
};
|
|
11
12
|
export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
|
|
12
13
|
export declare function getClient(): PostgresClient;
|
|
13
14
|
export declare function setClient(client: PostgresClient, options?: {
|
|
14
15
|
onQuery?: OnQueryHook;
|
|
16
|
+
prepare?: boolean;
|
|
17
|
+
fileRoot?: string;
|
|
15
18
|
}): void;
|
|
16
19
|
export declare function close(): Promise<void>;
|
|
17
20
|
export declare const sql: import("./runtime.js").SqlRoot;
|
|
@@ -1,28 +1,45 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
|
-
import {
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createSqlRuntime, encodePgArrayLiteral, isPrimitiveArrayElement, parameterKind, parsePgArrayLiteral, } from "./runtime.js";
|
|
3
4
|
import { arrayElementOid, builtinArrayOids } from "./pg/oids.js";
|
|
4
5
|
const HOOKS = Symbol.for("sqlx-js.hooks");
|
|
6
|
+
function resolvedFileRoot(value) {
|
|
7
|
+
return resolve(value ?? process.env.SQLX_JS_FILE_ROOT ?? process.cwd());
|
|
8
|
+
}
|
|
5
9
|
class PostgresRuntimeClient {
|
|
6
10
|
client;
|
|
7
11
|
onQuery;
|
|
8
|
-
|
|
12
|
+
prepare;
|
|
13
|
+
fileRoot;
|
|
14
|
+
constructor(client, onQuery, prepare = true, fileRoot = resolvedFileRoot()) {
|
|
9
15
|
this.client = client;
|
|
10
16
|
this.onQuery = onQuery;
|
|
17
|
+
this.prepare = prepare;
|
|
18
|
+
this.fileRoot = fileRoot;
|
|
11
19
|
}
|
|
12
20
|
async query(query, params) {
|
|
13
|
-
return await this.client.unsafe(query, params, { prepare:
|
|
21
|
+
return await this.client.unsafe(query, params, { prepare: this.prepare });
|
|
14
22
|
}
|
|
15
23
|
transformParam(param) {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
return this.client.
|
|
19
|
-
|
|
24
|
+
const kind = parameterKind(param);
|
|
25
|
+
if (kind === "json")
|
|
26
|
+
return this.client.json(param.value);
|
|
27
|
+
if (kind === "array") {
|
|
28
|
+
const value = [...param.value];
|
|
29
|
+
if (value.every(isPrimitiveArrayElement))
|
|
30
|
+
return encodePgArrayLiteral(value);
|
|
31
|
+
if (value.every((item) => item === null || parameterKind(item) === "json")) {
|
|
32
|
+
return this.client.array(value, 3807);
|
|
33
|
+
}
|
|
34
|
+
return this.client.array(value);
|
|
35
|
+
}
|
|
36
|
+
return param;
|
|
20
37
|
}
|
|
21
38
|
async transaction(fn) {
|
|
22
39
|
if (!("begin" in this.client))
|
|
23
40
|
throw new Error("sqlx-js.transaction: nested transactions are not supported");
|
|
24
41
|
return await this.client.begin(async (tx) => {
|
|
25
|
-
return await fn(new PostgresRuntimeClient(tx, this.onQuery));
|
|
42
|
+
return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.prepare, this.fileRoot));
|
|
26
43
|
});
|
|
27
44
|
}
|
|
28
45
|
async close() {
|
|
@@ -74,6 +91,7 @@ const STRING_ARRAY_ELEMENT_OIDS = new Set([
|
|
|
74
91
|
4451,
|
|
75
92
|
4536,
|
|
76
93
|
]);
|
|
94
|
+
const JSON_ELEMENT_OIDS = new Set([114, 3802]);
|
|
77
95
|
function parseSimpleArrayElement(oid) {
|
|
78
96
|
switch (oid) {
|
|
79
97
|
case 16:
|
|
@@ -90,28 +108,21 @@ function parseSimpleArrayElement(oid) {
|
|
|
90
108
|
return STRING_ARRAY_ELEMENT_OIDS.has(oid) ? (value) => value : undefined;
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
|
-
function postgresNativeArrayElementOid(value) {
|
|
94
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
95
|
-
return undefined;
|
|
96
|
-
let oid;
|
|
97
|
-
for (const item of value) {
|
|
98
|
-
if (item === null)
|
|
99
|
-
continue;
|
|
100
|
-
const itemOid = item instanceof Uint8Array ? 17 : undefined;
|
|
101
|
-
if (itemOid === undefined)
|
|
102
|
-
return undefined;
|
|
103
|
-
if (oid !== undefined && oid !== itemOid)
|
|
104
|
-
return undefined;
|
|
105
|
-
oid = itemOid;
|
|
106
|
-
}
|
|
107
|
-
return oid;
|
|
108
|
-
}
|
|
109
111
|
function postgresTypes() {
|
|
110
112
|
const types = { bigint: postgres.BigInt };
|
|
111
113
|
for (const oid of builtinArrayOids()) {
|
|
112
114
|
const elementOid = arrayElementOid(oid);
|
|
113
115
|
if (elementOid === undefined)
|
|
114
116
|
continue;
|
|
117
|
+
if (JSON_ELEMENT_OIDS.has(elementOid)) {
|
|
118
|
+
types[`array_${oid}`] = {
|
|
119
|
+
to: oid,
|
|
120
|
+
from: [oid],
|
|
121
|
+
serialize: (value) => Array.isArray(value) ? encodePgArrayLiteral(value) : String(value),
|
|
122
|
+
parse: (value) => parsePgArrayLiteral(value, JSON.parse),
|
|
123
|
+
};
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
115
126
|
const parseElement = parseSimpleArrayElement(elementOid);
|
|
116
127
|
if (!parseElement)
|
|
117
128
|
continue;
|
|
@@ -124,10 +135,30 @@ function postgresTypes() {
|
|
|
124
135
|
}
|
|
125
136
|
return types;
|
|
126
137
|
}
|
|
138
|
+
function typeOids(value) {
|
|
139
|
+
if (value === null || value === undefined)
|
|
140
|
+
return [];
|
|
141
|
+
return Array.isArray(value) ? value : [value];
|
|
142
|
+
}
|
|
143
|
+
function installJsonArrayCodecs(client) {
|
|
144
|
+
const options = client.options;
|
|
145
|
+
options.parsers ??= {};
|
|
146
|
+
options.serializers ??= {};
|
|
147
|
+
const configured = Object.values(options.types ?? {});
|
|
148
|
+
for (const oid of [199, 3807]) {
|
|
149
|
+
const hasParser = configured.some((type) => typeOids(type.from).includes(oid));
|
|
150
|
+
const hasSerializer = configured.some((type) => type.to === oid);
|
|
151
|
+
if (!hasParser)
|
|
152
|
+
options.parsers[oid] = (value) => parsePgArrayLiteral(value, JSON.parse);
|
|
153
|
+
if (!hasSerializer) {
|
|
154
|
+
options.serializers[oid] = (value) => Array.isArray(value) ? encodePgArrayLiteral(value) : String(value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
127
158
|
export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
128
159
|
if (!url)
|
|
129
160
|
throw new Error("sqlx-js: DATABASE_URL is not set");
|
|
130
|
-
const { onQuery, statementTimeoutMs, ...pgOptions } = options;
|
|
161
|
+
const { onQuery, statementTimeoutMs, fileRoot, ...pgOptions } = options;
|
|
131
162
|
const connection = statementTimeoutMs !== undefined
|
|
132
163
|
? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
|
|
133
164
|
: pgOptions.connection;
|
|
@@ -136,12 +167,17 @@ export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
|
136
167
|
...(connection ? { connection } : {}),
|
|
137
168
|
types: { ...postgresTypes(), ...(pgOptions.types ?? {}) },
|
|
138
169
|
});
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
client[HOOKS] = {
|
|
171
|
+
onQuery,
|
|
172
|
+
prepare: pgOptions.prepare ?? true,
|
|
173
|
+
fileRoot: resolvedFileRoot(fileRoot),
|
|
174
|
+
};
|
|
141
175
|
return client;
|
|
142
176
|
}
|
|
143
177
|
function createDefaultClient() {
|
|
144
|
-
|
|
178
|
+
const client = createClient();
|
|
179
|
+
const attached = client[HOOKS];
|
|
180
|
+
return new PostgresRuntimeClient(client, attached.onQuery, attached.prepare, attached.fileRoot);
|
|
145
181
|
}
|
|
146
182
|
function getRuntimeClient() {
|
|
147
183
|
defaultClient ??= createDefaultClient();
|
|
@@ -151,8 +187,9 @@ export function getClient() {
|
|
|
151
187
|
return getRuntimeClient().client;
|
|
152
188
|
}
|
|
153
189
|
export function setClient(client, options) {
|
|
190
|
+
installJsonArrayCodecs(client);
|
|
154
191
|
const attached = client[HOOKS];
|
|
155
|
-
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery);
|
|
192
|
+
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot));
|
|
156
193
|
}
|
|
157
194
|
export async function close() {
|
|
158
195
|
if (defaultClient) {
|