@onreza/sqlx-js 0.1.0 → 0.3.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 +128 -37
- package/ROADMAP.md +2 -3
- package/dist/bin/sqlx-js.js +97 -14
- package/dist/src/cache.d.ts +1 -0
- package/dist/src/cache.js +128 -1
- package/dist/src/codegen.js +16 -16
- package/dist/src/commands/init.d.ts +5 -0
- package/dist/src/commands/init.js +57 -0
- package/dist/src/commands/migrate.d.ts +190 -3
- package/dist/src/commands/migrate.js +1383 -46
- package/dist/src/commands/prepare.d.ts +16 -2
- package/dist/src/commands/prepare.js +116 -33
- package/dist/src/commands/schema.js +4 -0
- package/dist/src/commands/watch.js +1 -1
- package/dist/src/index.d.ts +14 -2
- package/dist/src/pg/narrow.js +8 -0
- package/dist/src/pg/oids.js +3 -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 +17 -1
- package/dist/src/pg/schema.js +48 -2
- package/dist/src/pg/wire.d.ts +14 -1
- package/dist/src/pg/wire.js +81 -3
- package/dist/src/postgres-runtime.d.ts +9 -2
- package/dist/src/postgres-runtime.js +20 -5
- package/dist/src/runtime.d.ts +12 -0
- package/dist/src/runtime.js +83 -3
- package/dist/src/scan/scanner.js +1 -1
- package/package.json +5 -8
- package/dist/src/bun-runtime.d.ts +0 -7
- package/dist/src/bun-runtime.js +0 -45
- package/dist/src/bun.d.ts +0 -22
- package/dist/src/bun.js +0 -9
package/dist/src/pg/schema.d.ts
CHANGED
|
@@ -24,7 +24,23 @@ export type ScalarArrayInfo = {
|
|
|
24
24
|
name: string;
|
|
25
25
|
element: ScalarInfo;
|
|
26
26
|
};
|
|
27
|
-
export type
|
|
27
|
+
export type CompositeField = {
|
|
28
|
+
name: string;
|
|
29
|
+
tsType: string;
|
|
30
|
+
nullable: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type CompositeInfo = {
|
|
33
|
+
kind: "composite";
|
|
34
|
+
name: string;
|
|
35
|
+
fields: CompositeField[];
|
|
36
|
+
};
|
|
37
|
+
export type CompositeArrayInfo = {
|
|
38
|
+
kind: "compositeArray";
|
|
39
|
+
name: string;
|
|
40
|
+
element: CompositeInfo;
|
|
41
|
+
};
|
|
42
|
+
export type CustomTypeInfo = EnumInfo | EnumArrayInfo | ScalarInfo | ScalarArrayInfo | CompositeInfo | CompositeArrayInfo;
|
|
43
|
+
export declare function compositeLiteral(info: CompositeInfo): string;
|
|
28
44
|
export declare class SchemaCache {
|
|
29
45
|
private client;
|
|
30
46
|
private byOidNum;
|
package/dist/src/pg/schema.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { decodeText } from "./wire.js";
|
|
2
2
|
import { isBuiltinOid, oidToTs } from "./oids.js";
|
|
3
|
+
export function compositeLiteral(info) {
|
|
4
|
+
if (info.fields.length === 0)
|
|
5
|
+
return "Record<string, unknown>";
|
|
6
|
+
const parts = info.fields.map((f) => {
|
|
7
|
+
const name = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(f.name) ? f.name : JSON.stringify(f.name);
|
|
8
|
+
return `${name}: ${f.tsType}${f.nullable ? " | null" : ""}`;
|
|
9
|
+
});
|
|
10
|
+
return `{ ${parts.join("; ")} }`;
|
|
11
|
+
}
|
|
3
12
|
export class SchemaCache {
|
|
4
13
|
client;
|
|
5
14
|
byOidNum = new Map();
|
|
@@ -130,11 +139,12 @@ export class SchemaCache {
|
|
|
130
139
|
for (const oid of need)
|
|
131
140
|
this.typesProbed.add(oid);
|
|
132
141
|
const list1 = [...new Set(need)].join(",");
|
|
133
|
-
const sql1 = `SELECT oid::int8, typname, typtype, typcategory, typelem::int8, typbasetype::int8 FROM pg_type WHERE oid IN (${list1})`;
|
|
142
|
+
const sql1 = `SELECT oid::int8, typname, typtype, typcategory, typelem::int8, typbasetype::int8, typrelid::int8 FROM pg_type WHERE oid IN (${list1})`;
|
|
134
143
|
const r1 = await this.client.simpleQueryAll(sql1);
|
|
135
144
|
const enumOids = [];
|
|
136
145
|
const arrayInfos = [];
|
|
137
146
|
const domainInfos = [];
|
|
147
|
+
const compositeInfos = [];
|
|
138
148
|
for (const row of r1.rows) {
|
|
139
149
|
const oid = Number(decodeText(row[0]));
|
|
140
150
|
const name = decodeText(row[1]);
|
|
@@ -142,6 +152,7 @@ export class SchemaCache {
|
|
|
142
152
|
const typcategory = decodeText(row[3]);
|
|
143
153
|
const typelem = Number(decodeText(row[4]));
|
|
144
154
|
const typbasetype = Number(decodeText(row[5]));
|
|
155
|
+
const typrelid = Number(decodeText(row[6]));
|
|
145
156
|
if (typtype === "e") {
|
|
146
157
|
enumOids.push(oid);
|
|
147
158
|
this.customTypes.set(oid, { kind: "enum", name, values: [] });
|
|
@@ -155,10 +166,29 @@ export class SchemaCache {
|
|
|
155
166
|
else if (typtype === "d" && typbasetype > 0) {
|
|
156
167
|
domainInfos.push({ oid, name, baseOid: typbasetype });
|
|
157
168
|
}
|
|
169
|
+
else if (typtype === "c" && typrelid > 0) {
|
|
170
|
+
compositeInfos.push({ oid, name, relOid: typrelid });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const compositeAttrs = new Map();
|
|
174
|
+
if (compositeInfos.length > 0) {
|
|
175
|
+
const relList = [...new Set(compositeInfos.map((c) => c.relOid))].join(",");
|
|
176
|
+
const sqlc = `SELECT c.oid::int8, a.attname, a.atttypid::int8, a.attnotnull FROM pg_attribute a JOIN pg_type c ON c.typrelid = a.attrelid WHERE a.attrelid IN (${relList}) AND a.attnum > 0 AND NOT a.attisdropped ORDER BY c.oid, a.attnum`;
|
|
177
|
+
const rc = await this.client.simpleQueryAll(sqlc);
|
|
178
|
+
for (const row of rc.rows) {
|
|
179
|
+
const typoid = Number(decodeText(row[0]));
|
|
180
|
+
const attname = decodeText(row[1]);
|
|
181
|
+
const atttypid = Number(decodeText(row[2]));
|
|
182
|
+
const notNull = decodeText(row[3]) === "t";
|
|
183
|
+
const arr = compositeAttrs.get(typoid) ?? [];
|
|
184
|
+
arr.push({ name: attname, typeOid: atttypid, notNull });
|
|
185
|
+
compositeAttrs.set(typoid, arr);
|
|
186
|
+
}
|
|
158
187
|
}
|
|
159
188
|
const elemsToProbe = arrayInfos.map((a) => a.elemOid).filter((o) => !this.typesProbed.has(o));
|
|
160
189
|
const basesToProbe = domainInfos.map((d) => d.baseOid).filter((o) => !this.typesProbed.has(o) && !isBuiltinOid(o));
|
|
161
|
-
const
|
|
190
|
+
const fieldsToProbe = [...compositeAttrs.values()].flat().map((f) => f.typeOid).filter((o) => !this.typesProbed.has(o) && !isBuiltinOid(o));
|
|
191
|
+
const recurse = [...new Set([...elemsToProbe, ...basesToProbe, ...fieldsToProbe])];
|
|
162
192
|
if (recurse.length > 0)
|
|
163
193
|
await this.loadCustomTypes(recurse);
|
|
164
194
|
for (const { oid, name, baseOid } of domainInfos) {
|
|
@@ -167,6 +197,15 @@ export class SchemaCache {
|
|
|
167
197
|
this.customTypes.set(oid, { kind: "scalar", name, tsType: resolved });
|
|
168
198
|
}
|
|
169
199
|
}
|
|
200
|
+
for (const { oid, name } of compositeInfos) {
|
|
201
|
+
const attrs = compositeAttrs.get(oid) ?? [];
|
|
202
|
+
const fields = attrs.map((a) => ({
|
|
203
|
+
name: a.name,
|
|
204
|
+
tsType: this.resolveBaseTs(a.typeOid) ?? "unknown",
|
|
205
|
+
nullable: !a.notNull,
|
|
206
|
+
}));
|
|
207
|
+
this.customTypes.set(oid, { kind: "composite", name, fields });
|
|
208
|
+
}
|
|
170
209
|
for (const { arrayOid, arrayName, elemOid } of arrayInfos) {
|
|
171
210
|
const elem = this.customTypes.get(elemOid);
|
|
172
211
|
if (elem && elem.kind === "enum") {
|
|
@@ -175,6 +214,9 @@ export class SchemaCache {
|
|
|
175
214
|
else if (elem && elem.kind === "scalar") {
|
|
176
215
|
this.customTypes.set(arrayOid, { kind: "scalarArray", name: arrayName, element: elem });
|
|
177
216
|
}
|
|
217
|
+
else if (elem && elem.kind === "composite") {
|
|
218
|
+
this.customTypes.set(arrayOid, { kind: "compositeArray", name: arrayName, element: elem });
|
|
219
|
+
}
|
|
178
220
|
}
|
|
179
221
|
if (enumOids.length > 0) {
|
|
180
222
|
const list2 = enumOids.join(",");
|
|
@@ -211,6 +253,10 @@ export class SchemaCache {
|
|
|
211
253
|
return "string[]";
|
|
212
254
|
return `(${info.element.values.map((v) => JSON.stringify(v)).join(" | ")})[]`;
|
|
213
255
|
}
|
|
256
|
+
if (info.kind === "composite")
|
|
257
|
+
return compositeLiteral(info);
|
|
258
|
+
if (info.kind === "compositeArray")
|
|
259
|
+
return `(${compositeLiteral(info.element)})[]`;
|
|
214
260
|
return undefined;
|
|
215
261
|
}
|
|
216
262
|
customType(oid) {
|
package/dist/src/pg/wire.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export type ConnConfig = {
|
|
|
9
9
|
sslmode?: SslMode;
|
|
10
10
|
applicationName?: string;
|
|
11
11
|
connectTimeoutMs?: number;
|
|
12
|
+
statementTimeoutMs?: number;
|
|
13
|
+
sslRootCert?: string;
|
|
14
|
+
sslCert?: string;
|
|
15
|
+
sslKey?: string;
|
|
12
16
|
};
|
|
13
17
|
export declare function parseDatabaseUrl(url: string): ConnConfig;
|
|
14
18
|
export type ServerMessage = {
|
|
@@ -91,6 +95,9 @@ export declare class PgClient {
|
|
|
91
95
|
constructor(cfg: ConnConfig);
|
|
92
96
|
get usingTls(): boolean;
|
|
93
97
|
connect(): Promise<void>;
|
|
98
|
+
private destroySocket;
|
|
99
|
+
private connectInner;
|
|
100
|
+
private abortConnect;
|
|
94
101
|
private attachHandlers;
|
|
95
102
|
private deliver;
|
|
96
103
|
private flushWaiters;
|
|
@@ -120,12 +127,18 @@ export declare function computeScramProof(password: string, salt: Uint8Array, it
|
|
|
120
127
|
};
|
|
121
128
|
export declare class PgError extends Error {
|
|
122
129
|
fields: Record<string, string>;
|
|
123
|
-
constructor(fields: Record<string, string
|
|
130
|
+
constructor(fields: Record<string, string>, options?: {
|
|
131
|
+
cause?: unknown;
|
|
132
|
+
});
|
|
124
133
|
get code(): string | undefined;
|
|
125
134
|
get position(): number | undefined;
|
|
126
135
|
get hint(): string | undefined;
|
|
127
136
|
get detail(): string | undefined;
|
|
128
137
|
get severity(): string | undefined;
|
|
138
|
+
get schema(): string | undefined;
|
|
139
|
+
get table(): string | undefined;
|
|
140
|
+
get column(): string | undefined;
|
|
141
|
+
get constraint(): string | undefined;
|
|
129
142
|
}
|
|
130
143
|
export declare class ConnectionLostError extends Error {
|
|
131
144
|
readonly cause: Error;
|
package/dist/src/pg/wire.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, createHmac, pbkdf2Sync, randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { connect as netConnect } from "node:net";
|
|
3
4
|
import { connect as tlsConnect } from "node:tls";
|
|
4
5
|
const textEncoder = new TextEncoder();
|
|
@@ -31,6 +32,21 @@ export function parseDatabaseUrl(url) {
|
|
|
31
32
|
if (Number.isFinite(n) && n > 0)
|
|
32
33
|
cfg.connectTimeoutMs = n * 1000;
|
|
33
34
|
}
|
|
35
|
+
const st = params.get("statement_timeout");
|
|
36
|
+
if (st) {
|
|
37
|
+
const n = Number(st);
|
|
38
|
+
if (Number.isFinite(n) && n >= 0)
|
|
39
|
+
cfg.statementTimeoutMs = n;
|
|
40
|
+
}
|
|
41
|
+
const sslRootCert = params.get("sslrootcert");
|
|
42
|
+
if (sslRootCert)
|
|
43
|
+
cfg.sslRootCert = sslRootCert;
|
|
44
|
+
const sslCert = params.get("sslcert");
|
|
45
|
+
if (sslCert)
|
|
46
|
+
cfg.sslCert = sslCert;
|
|
47
|
+
const sslKey = params.get("sslkey");
|
|
48
|
+
if (sslKey)
|
|
49
|
+
cfg.sslKey = sslKey;
|
|
34
50
|
return cfg;
|
|
35
51
|
}
|
|
36
52
|
export class MessageReader {
|
|
@@ -229,6 +245,14 @@ function frame(tag, body) {
|
|
|
229
245
|
function isTlsRequired(mode) {
|
|
230
246
|
return mode === "require" || mode === "verify-ca" || mode === "verify-full";
|
|
231
247
|
}
|
|
248
|
+
function readCertFile(kind, path) {
|
|
249
|
+
try {
|
|
250
|
+
return readFileSync(path);
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
throw new Error(`sqlx-js: cannot read ${kind} at ${path}: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
232
256
|
async function openPlainSocket(host, port, timeoutMs) {
|
|
233
257
|
return new Promise((resolve, reject) => {
|
|
234
258
|
const sock = netConnect({ host, port });
|
|
@@ -275,6 +299,9 @@ async function performSslHandshake(sock, cfg, mode) {
|
|
|
275
299
|
servername: cfg.host,
|
|
276
300
|
rejectUnauthorized: mode === "verify-full" || mode === "verify-ca",
|
|
277
301
|
checkServerIdentity: mode === "verify-full" ? undefined : () => undefined,
|
|
302
|
+
...(cfg.sslRootCert ? { ca: readCertFile("sslrootcert", cfg.sslRootCert) } : {}),
|
|
303
|
+
...(cfg.sslCert ? { cert: readCertFile("sslcert", cfg.sslCert) } : {}),
|
|
304
|
+
...(cfg.sslKey ? { key: readCertFile("sslkey", cfg.sslKey) } : {}),
|
|
278
305
|
});
|
|
279
306
|
t.once("secureConnect", () => resolve(t));
|
|
280
307
|
t.once("error", (err) => {
|
|
@@ -308,9 +335,42 @@ export class PgClient {
|
|
|
308
335
|
}
|
|
309
336
|
get usingTls() { return this.tlsEnabled; }
|
|
310
337
|
async connect() {
|
|
311
|
-
const mode = this.cfg.sslmode ?? "prefer";
|
|
312
338
|
const timeoutMs = this.cfg.connectTimeoutMs ?? 15000;
|
|
339
|
+
let aborted = false;
|
|
340
|
+
let timer;
|
|
341
|
+
const deadline = new Promise((_, reject) => {
|
|
342
|
+
timer = setTimeout(() => {
|
|
343
|
+
aborted = true;
|
|
344
|
+
const err = new Error(`sqlx-js: connect timeout to ${this.cfg.host}:${this.cfg.port} after ${timeoutMs}ms (includes TLS + authentication)`);
|
|
345
|
+
this.closed = true;
|
|
346
|
+
this.closeReason ??= err;
|
|
347
|
+
this.destroySocket();
|
|
348
|
+
reject(err);
|
|
349
|
+
}, timeoutMs);
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
await Promise.race([this.connectInner(timeoutMs, () => aborted), deadline]);
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
if (timer)
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
destroySocket() {
|
|
360
|
+
try {
|
|
361
|
+
this.sock?.destroy();
|
|
362
|
+
}
|
|
363
|
+
catch { /* ignore */ }
|
|
364
|
+
}
|
|
365
|
+
// Cooperative cancellation: a socket can finish connecting after the deadline
|
|
366
|
+
// has already rejected. Re-check `aborted` at each await boundary so a late
|
|
367
|
+
// connection is torn down instead of leaking.
|
|
368
|
+
async connectInner(timeoutMs, aborted) {
|
|
369
|
+
const mode = this.cfg.sslmode ?? "prefer";
|
|
313
370
|
const plain = await openPlainSocket(this.cfg.host, this.cfg.port, timeoutMs);
|
|
371
|
+
this.sock = plain;
|
|
372
|
+
if (aborted())
|
|
373
|
+
return this.abortConnect();
|
|
314
374
|
let socket = plain;
|
|
315
375
|
if (mode !== "disable") {
|
|
316
376
|
const result = await performSslHandshake(plain, this.cfg, mode);
|
|
@@ -318,11 +378,17 @@ export class PgClient {
|
|
|
318
378
|
this.tlsEnabled = result.tls;
|
|
319
379
|
}
|
|
320
380
|
this.sock = socket;
|
|
381
|
+
if (aborted())
|
|
382
|
+
return this.abortConnect();
|
|
321
383
|
this.attachHandlers();
|
|
322
384
|
await this.startup();
|
|
323
385
|
await this.authenticate();
|
|
324
386
|
await this.awaitReady();
|
|
325
387
|
}
|
|
388
|
+
abortConnect() {
|
|
389
|
+
this.destroySocket();
|
|
390
|
+
throw this.closeReason ?? new Error("sqlx-js: connect aborted");
|
|
391
|
+
}
|
|
326
392
|
attachHandlers() {
|
|
327
393
|
const onData = (chunk) => {
|
|
328
394
|
for (const m of this.reader.push(chunk))
|
|
@@ -380,6 +446,9 @@ export class PgClient {
|
|
|
380
446
|
if (this.cfg.applicationName) {
|
|
381
447
|
pairs.push(cstr("application_name"), cstr(this.cfg.applicationName));
|
|
382
448
|
}
|
|
449
|
+
if (this.cfg.statementTimeoutMs !== undefined) {
|
|
450
|
+
pairs.push(cstr("statement_timeout"), cstr(String(this.cfg.statementTimeoutMs)));
|
|
451
|
+
}
|
|
383
452
|
pairs.push(new Uint8Array([0]));
|
|
384
453
|
const body = concat([writeInt32(196608), concat(pairs)]);
|
|
385
454
|
this.write(frame(null, body));
|
|
@@ -678,16 +747,25 @@ function stringifyMessage(m) {
|
|
|
678
747
|
}
|
|
679
748
|
export class PgError extends Error {
|
|
680
749
|
fields;
|
|
681
|
-
constructor(fields) {
|
|
682
|
-
super(fields.M ?? "postgres error");
|
|
750
|
+
constructor(fields, options) {
|
|
751
|
+
super(fields.M ?? "postgres error", options);
|
|
683
752
|
this.fields = fields;
|
|
684
753
|
this.name = "PgError";
|
|
754
|
+
// Bun attaches own `line`/`column` properties to every Error instance, which
|
|
755
|
+
// would shadow the prototype getters below. Drop them so `.column` resolves
|
|
756
|
+
// to the PG column name, not the engine's source position.
|
|
757
|
+
delete this.line;
|
|
758
|
+
delete this.column;
|
|
685
759
|
}
|
|
686
760
|
get code() { return this.fields.C; }
|
|
687
761
|
get position() { return this.fields.P ? Number(this.fields.P) : undefined; }
|
|
688
762
|
get hint() { return this.fields.H; }
|
|
689
763
|
get detail() { return this.fields.D; }
|
|
690
764
|
get severity() { return this.fields.S; }
|
|
765
|
+
get schema() { return this.fields.s; }
|
|
766
|
+
get table() { return this.fields.t; }
|
|
767
|
+
get column() { return this.fields.c; }
|
|
768
|
+
get constraint() { return this.fields.n; }
|
|
691
769
|
}
|
|
692
770
|
export class ConnectionLostError extends Error {
|
|
693
771
|
cause;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
|
+
import { type OnQueryHook } from "./runtime.js";
|
|
2
3
|
export type PostgresClient = postgres.Sql<{
|
|
3
4
|
bigint: bigint;
|
|
4
5
|
}>;
|
|
5
6
|
export type PostgresOptions = postgres.Options<Record<string, postgres.PostgresType>>;
|
|
6
|
-
export
|
|
7
|
+
export type CreateClientOptions = PostgresOptions & {
|
|
8
|
+
onQuery?: OnQueryHook;
|
|
9
|
+
statementTimeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
|
|
7
12
|
export declare function getClient(): PostgresClient;
|
|
8
|
-
export declare function setClient(client: PostgresClient
|
|
13
|
+
export declare function setClient(client: PostgresClient, options?: {
|
|
14
|
+
onQuery?: OnQueryHook;
|
|
15
|
+
}): void;
|
|
9
16
|
export declare function close(): Promise<void>;
|
|
10
17
|
export declare const sql: import("./runtime.js").SqlRoot;
|
|
11
18
|
export declare const unsafe: (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
2
|
import { createSqlRuntime, encodeParam, encodePgArrayLiteral, parsePgArrayLiteral } from "./runtime.js";
|
|
3
3
|
import { arrayElementOid, builtinArrayOids } from "./pg/oids.js";
|
|
4
|
+
const HOOKS = Symbol.for("sqlx-js.hooks");
|
|
4
5
|
class PostgresRuntimeClient {
|
|
5
6
|
client;
|
|
6
|
-
|
|
7
|
+
onQuery;
|
|
8
|
+
constructor(client, onQuery) {
|
|
7
9
|
this.client = client;
|
|
10
|
+
this.onQuery = onQuery;
|
|
8
11
|
}
|
|
9
12
|
async query(query, params) {
|
|
10
13
|
return await this.client.unsafe(query, params, { prepare: true });
|
|
@@ -19,7 +22,7 @@ class PostgresRuntimeClient {
|
|
|
19
22
|
if (!("begin" in this.client))
|
|
20
23
|
throw new Error("sqlx-js.transaction: nested transactions are not supported");
|
|
21
24
|
return await this.client.begin(async (tx) => {
|
|
22
|
-
return await fn(new PostgresRuntimeClient(tx));
|
|
25
|
+
return await fn(new PostgresRuntimeClient(tx, this.onQuery));
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
28
|
async close() {
|
|
@@ -124,7 +127,18 @@ function postgresTypes() {
|
|
|
124
127
|
export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
125
128
|
if (!url)
|
|
126
129
|
throw new Error("sqlx-js: DATABASE_URL is not set");
|
|
127
|
-
|
|
130
|
+
const { onQuery, statementTimeoutMs, ...pgOptions } = options;
|
|
131
|
+
const connection = statementTimeoutMs !== undefined
|
|
132
|
+
? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
|
|
133
|
+
: pgOptions.connection;
|
|
134
|
+
const client = postgres(url, {
|
|
135
|
+
...pgOptions,
|
|
136
|
+
...(connection ? { connection } : {}),
|
|
137
|
+
types: { ...postgresTypes(), ...(pgOptions.types ?? {}) },
|
|
138
|
+
});
|
|
139
|
+
if (onQuery)
|
|
140
|
+
client[HOOKS] = { onQuery };
|
|
141
|
+
return client;
|
|
128
142
|
}
|
|
129
143
|
function createDefaultClient() {
|
|
130
144
|
return new PostgresRuntimeClient(createClient());
|
|
@@ -136,8 +150,9 @@ function getRuntimeClient() {
|
|
|
136
150
|
export function getClient() {
|
|
137
151
|
return getRuntimeClient().client;
|
|
138
152
|
}
|
|
139
|
-
export function setClient(client) {
|
|
140
|
-
|
|
153
|
+
export function setClient(client, options) {
|
|
154
|
+
const attached = client[HOOKS];
|
|
155
|
+
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery);
|
|
141
156
|
}
|
|
142
157
|
export async function close() {
|
|
143
158
|
if (defaultClient) {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { PgError } from "./pg/wire.js";
|
|
2
|
+
export type OnQueryEvent = {
|
|
3
|
+
query: string;
|
|
4
|
+
params: unknown[];
|
|
5
|
+
durationMs: number;
|
|
6
|
+
rowCount?: number;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
};
|
|
9
|
+
export type OnQueryHook = (event: OnQueryEvent) => void;
|
|
1
10
|
export type RuntimeClient = {
|
|
2
11
|
query: (query: string, params: unknown[]) => Promise<unknown[]>;
|
|
3
12
|
transformParam?: (param: unknown) => unknown;
|
|
4
13
|
transaction: <R>(fn: (client: RuntimeClient) => Promise<R>) => Promise<R>;
|
|
5
14
|
close: () => Promise<void>;
|
|
15
|
+
onQuery?: OnQueryHook;
|
|
6
16
|
};
|
|
7
17
|
type AnyFn = (...args: unknown[]) => Promise<unknown[]>;
|
|
8
18
|
type AnyOneFn = (...args: unknown[]) => Promise<unknown>;
|
|
@@ -21,6 +31,7 @@ export declare class TooManyRowsError extends Error {
|
|
|
21
31
|
actual: number;
|
|
22
32
|
constructor(actual: number, expected?: "1" | "0 or 1");
|
|
23
33
|
}
|
|
34
|
+
export declare function toPgError(e: unknown): PgError | null;
|
|
24
35
|
export declare const _internal: {
|
|
25
36
|
renameRows: typeof renameRows;
|
|
26
37
|
encodeParam: typeof encodeParam;
|
|
@@ -29,6 +40,7 @@ export declare const _internal: {
|
|
|
29
40
|
loadSqlFile: typeof loadSqlFile;
|
|
30
41
|
buildSetTransaction: typeof buildSetTransaction;
|
|
31
42
|
clearIdentifierCache: typeof clearIdentifierCache;
|
|
43
|
+
toPgError: typeof toPgError;
|
|
32
44
|
};
|
|
33
45
|
declare function loadSqlFile(path: string): string;
|
|
34
46
|
export declare function clearSqlFileCache(): void;
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { PgClient, parseDatabaseUrl } from "./pg/wire.js";
|
|
3
|
+
import { PgClient, parseDatabaseUrl, PgError } from "./pg/wire.js";
|
|
4
4
|
import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./commands/migrate.js";
|
|
5
5
|
const SUFFIX = /[!?]$/;
|
|
6
6
|
function renameRows(rows) {
|
|
@@ -148,6 +148,63 @@ export class TooManyRowsError extends Error {
|
|
|
148
148
|
this.actual = actual;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
// SQLSTATE is exactly five characters from [0-9A-Z]; lowercase or other shapes
|
|
152
|
+
// are never valid, so transport codes like "EPIPE" must not match on shape alone.
|
|
153
|
+
const SQLSTATE = /^[0-9A-Z]{5}$/;
|
|
154
|
+
function firstString(...candidates) {
|
|
155
|
+
for (const value of candidates) {
|
|
156
|
+
if (typeof value === "string" && value.length > 0)
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
export function toPgError(e) {
|
|
162
|
+
if (e instanceof PgError)
|
|
163
|
+
return e;
|
|
164
|
+
if (e === null || typeof e !== "object")
|
|
165
|
+
return null;
|
|
166
|
+
const o = e;
|
|
167
|
+
const code = typeof o.code === "string" ? o.code : undefined;
|
|
168
|
+
// `_name` variants are postgres.js; bare forms are node-postgres. Bare
|
|
169
|
+
// `column`/`schema`/`table` can also collide with runtime-added Error
|
|
170
|
+
// properties, so we read namespaced variants first and only accept strings.
|
|
171
|
+
const severity = firstString(o.severity, o.severity_local);
|
|
172
|
+
// A genuine database error is identified by the driver's branded name
|
|
173
|
+
// (Postgres.js) or by a SQLSTATE-shaped code paired with a severity. Transport
|
|
174
|
+
// and system errors (EPIPE, ECONNREFUSED, CONNECTION_ENDED) carry neither, so
|
|
175
|
+
// they pass through untouched instead of masquerading as a PgError.
|
|
176
|
+
const isDatabaseError = o.name === "PostgresError" ||
|
|
177
|
+
(code !== undefined && SQLSTATE.test(code) && severity !== undefined);
|
|
178
|
+
if (!isDatabaseError)
|
|
179
|
+
return null;
|
|
180
|
+
const fields = {};
|
|
181
|
+
if (typeof o.message === "string" && o.message.length > 0)
|
|
182
|
+
fields.M = o.message;
|
|
183
|
+
if (code)
|
|
184
|
+
fields.C = code;
|
|
185
|
+
if (typeof o.detail === "string" && o.detail.length > 0)
|
|
186
|
+
fields.D = o.detail;
|
|
187
|
+
if (typeof o.hint === "string" && o.hint.length > 0)
|
|
188
|
+
fields.H = o.hint;
|
|
189
|
+
const position = firstString(o.position) ?? (typeof o.position === "number" && Number.isFinite(o.position) ? String(o.position) : undefined);
|
|
190
|
+
if (position)
|
|
191
|
+
fields.P = position;
|
|
192
|
+
if (severity)
|
|
193
|
+
fields.S = severity;
|
|
194
|
+
const table = firstString(o.table_name, o.table);
|
|
195
|
+
if (table)
|
|
196
|
+
fields.t = table;
|
|
197
|
+
const column = firstString(o.column_name, o.column);
|
|
198
|
+
if (column)
|
|
199
|
+
fields.c = column;
|
|
200
|
+
const constraint = firstString(o.constraint_name, o.constraint);
|
|
201
|
+
if (constraint)
|
|
202
|
+
fields.n = constraint;
|
|
203
|
+
const schema = firstString(o.schema_name, o.schema);
|
|
204
|
+
if (schema)
|
|
205
|
+
fields.s = schema;
|
|
206
|
+
return new PgError(fields, { cause: e });
|
|
207
|
+
}
|
|
151
208
|
export const _internal = {
|
|
152
209
|
renameRows,
|
|
153
210
|
encodeParam,
|
|
@@ -156,13 +213,32 @@ export const _internal = {
|
|
|
156
213
|
loadSqlFile,
|
|
157
214
|
buildSetTransaction,
|
|
158
215
|
clearIdentifierCache,
|
|
216
|
+
toPgError,
|
|
159
217
|
};
|
|
160
218
|
async function runQuery(client, query, params) {
|
|
161
219
|
const encoded = params.length === 0
|
|
162
220
|
? params
|
|
163
221
|
: params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
|
|
164
|
-
const
|
|
165
|
-
|
|
222
|
+
const onQuery = client.onQuery;
|
|
223
|
+
if (!onQuery) {
|
|
224
|
+
try {
|
|
225
|
+
return renameRows(await client.query(query, encoded));
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
throw toPgError(e) ?? e;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const start = performance.now();
|
|
232
|
+
try {
|
|
233
|
+
const rows = await client.query(query, encoded);
|
|
234
|
+
onQuery({ query, params, durationMs: performance.now() - start, rowCount: rows.length });
|
|
235
|
+
return renameRows(rows);
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
const error = toPgError(e) ?? e;
|
|
239
|
+
onQuery({ query, params, durationMs: performance.now() - start, error });
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
166
242
|
}
|
|
167
243
|
async function runOne(client, query, params) {
|
|
168
244
|
const rows = await runQuery(client, query, params);
|
|
@@ -422,6 +498,10 @@ export async function migrate(opts = {}) {
|
|
|
422
498
|
log(`migrate: applied ${String(e.version).padStart(4, "0")}_${e.name}`);
|
|
423
499
|
appliedAny = true;
|
|
424
500
|
}
|
|
501
|
+
else if (e.kind === "adopted") {
|
|
502
|
+
log(`migrate: adopted ${String(e.version).padStart(4, "0")}_${e.name} (${e.replaced} replaced)`);
|
|
503
|
+
appliedAny = true;
|
|
504
|
+
}
|
|
425
505
|
else if (e.kind === "tampered") {
|
|
426
506
|
throw new Error(`sqlx-js.migrate: ${e.version}_${e.name} hash mismatch (applied ${e.applied.slice(0, 16)}… vs current ${e.current.slice(0, 16)}…)`);
|
|
427
507
|
}
|
package/dist/src/scan/scanner.js
CHANGED
|
@@ -2,7 +2,7 @@ import ts from "typescript";
|
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
const EXCLUDE_DIRS = new Set(["node_modules", ".git", ".sqlx-js", "dist", "build", ".next"]);
|
|
5
|
-
const SQLX_MODULES = new Set(["@onreza/sqlx-js"
|
|
5
|
+
const SQLX_MODULES = new Set(["@onreza/sqlx-js"]);
|
|
6
6
|
const EXT = /\.(ts|tsx|mts|cts)$/;
|
|
7
7
|
function walk(dir, out) {
|
|
8
8
|
for (const name of readdirSync(dir)) {
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onreza/sqlx-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by sqlx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
7
10
|
"sideEffects": false,
|
|
8
11
|
"main": "./dist/src/index.js",
|
|
9
12
|
"types": "./dist/src/index.d.ts",
|
|
@@ -12,12 +15,6 @@
|
|
|
12
15
|
"types": "./dist/src/index.d.ts",
|
|
13
16
|
"import": "./dist/src/index.js",
|
|
14
17
|
"default": "./dist/src/index.js"
|
|
15
|
-
},
|
|
16
|
-
"./bun": {
|
|
17
|
-
"types": "./dist/src/bun.d.ts",
|
|
18
|
-
"bun": "./dist/src/bun.js",
|
|
19
|
-
"import": "./dist/src/bun.js",
|
|
20
|
-
"default": "./dist/src/bun.js"
|
|
21
18
|
}
|
|
22
19
|
},
|
|
23
20
|
"bin": {
|
|
@@ -52,7 +49,7 @@
|
|
|
52
49
|
"migrations"
|
|
53
50
|
],
|
|
54
51
|
"scripts": {
|
|
55
|
-
"prepare": "[ -d .git ]
|
|
52
|
+
"prepare": "(test -f dist/bin/sqlx-js.js || bun run build) && ([ ! -d .git ] || lefthook install >/dev/null 2>&1 || true)",
|
|
56
53
|
"test": "bun test tests",
|
|
57
54
|
"test:typecheck": "tsc --noEmit",
|
|
58
55
|
"test:example": "bunx tsc -p example --noEmit",
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { SQL } from "bun";
|
|
2
|
-
export type BunClient = SQL;
|
|
3
|
-
export declare function getClient(): BunClient;
|
|
4
|
-
export declare function setClient(client: BunClient): void;
|
|
5
|
-
export declare function close(): Promise<void>;
|
|
6
|
-
export declare const sql: import("./runtime.js").SqlRoot;
|
|
7
|
-
export declare const unsafe: (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
|
package/dist/src/bun-runtime.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { SQL } from "bun";
|
|
2
|
-
import { createSqlRuntime } from "./runtime.js";
|
|
3
|
-
class BunRuntimeClient {
|
|
4
|
-
client;
|
|
5
|
-
constructor(client) {
|
|
6
|
-
this.client = client;
|
|
7
|
-
}
|
|
8
|
-
async query(query, params) {
|
|
9
|
-
return await this.client.unsafe(query, params);
|
|
10
|
-
}
|
|
11
|
-
async transaction(fn) {
|
|
12
|
-
return await this.client.begin(async (tx) => {
|
|
13
|
-
return await fn(new BunRuntimeClient(tx));
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
async close() {
|
|
17
|
-
await this.client.close();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
let defaultClient = null;
|
|
21
|
-
function createDefaultClient() {
|
|
22
|
-
const url = process.env.DATABASE_URL;
|
|
23
|
-
if (!url)
|
|
24
|
-
throw new Error("sqlx-js: DATABASE_URL is not set");
|
|
25
|
-
return new BunRuntimeClient(new SQL({ url, bigint: true }));
|
|
26
|
-
}
|
|
27
|
-
function getRuntimeClient() {
|
|
28
|
-
defaultClient ??= createDefaultClient();
|
|
29
|
-
return defaultClient;
|
|
30
|
-
}
|
|
31
|
-
export function getClient() {
|
|
32
|
-
return getRuntimeClient().client;
|
|
33
|
-
}
|
|
34
|
-
export function setClient(client) {
|
|
35
|
-
defaultClient = new BunRuntimeClient(client);
|
|
36
|
-
}
|
|
37
|
-
export async function close() {
|
|
38
|
-
if (defaultClient) {
|
|
39
|
-
await defaultClient.close();
|
|
40
|
-
defaultClient = null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const runtime = createSqlRuntime(getRuntimeClient);
|
|
44
|
-
export const sql = runtime.sql;
|
|
45
|
-
export const unsafe = runtime.unsafe;
|
package/dist/src/bun.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import * as rt from "./bun-runtime.js";
|
|
2
|
-
import type { Typed as TypedFor, TypedFile as TypedFileFor, TypedSql as TypedSqlFor } from "./typed.js";
|
|
3
|
-
export interface KnownQueries {
|
|
4
|
-
}
|
|
5
|
-
export interface KnownFileQueries {
|
|
6
|
-
}
|
|
7
|
-
export type { SqlxJsConfig } from "./config.js";
|
|
8
|
-
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
9
|
-
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
10
|
-
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
11
|
-
export type { TransactionOptions, MigrateOptions } from "./runtime.js";
|
|
12
|
-
export type { BunClient } from "./bun-runtime.js";
|
|
13
|
-
export type TypedFile = TypedFileFor<KnownFileQueries>;
|
|
14
|
-
export type TypedSql = TypedSqlFor<KnownQueries, KnownFileQueries>;
|
|
15
|
-
export type Typed = TypedFor<KnownQueries, KnownFileQueries, import("./runtime.js").TransactionOptions>;
|
|
16
|
-
export declare const sql: Typed;
|
|
17
|
-
export type Unsafe = (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
18
|
-
export declare const unsafe: Unsafe;
|
|
19
|
-
export declare const getClient: typeof rt.getClient;
|
|
20
|
-
export declare const setClient: typeof rt.setClient;
|
|
21
|
-
export declare const close: typeof rt.close;
|
|
22
|
-
export { migrate, clearSqlFileCache, encodePgArrayLiteral, id } from "./runtime.js";
|