@onreza/sqlx-js 0.8.0 → 0.9.1
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 +98 -14
- package/ROADMAP.md +1 -1
- package/dist/bin/sqlx-js.js +39 -2
- package/dist/src/cache.d.ts +4 -1
- package/dist/src/cache.js +12 -63
- package/dist/src/commands/init.js +8 -0
- package/dist/src/commands/prepare.d.ts +2 -0
- package/dist/src/commands/prepare.js +46 -35
- package/dist/src/commands/queries.d.ts +38 -0
- package/dist/src/commands/queries.js +143 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +29 -0
- package/dist/src/index.d.ts +14 -6
- package/dist/src/index.js +3 -1
- package/dist/src/pg/functions.d.ts +3 -1
- package/dist/src/pg/functions.js +11 -3
- package/dist/src/postgres-runtime.d.ts +2 -0
- package/dist/src/postgres-runtime.js +9 -6
- package/dist/src/query-id.d.ts +1 -0
- package/dist/src/query-id.js +61 -0
- package/dist/src/query.d.ts +62 -0
- package/dist/src/query.js +38 -0
- package/dist/src/runtime.d.ts +27 -3
- package/dist/src/runtime.js +67 -21
- package/dist/src/scan/scanner.d.ts +2 -0
- package/dist/src/scan/scanner.js +102 -17
- package/dist/src/typed.d.ts +29 -11
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ import { arrayElementOid, isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
|
8
8
|
import { ScanError, scanProject } from "../scan/scanner.js";
|
|
9
9
|
import { assertCacheManifest, Cache, fingerprint, effectiveNullable, portableCacheOid, writeCacheManifest, } from "../cache.js";
|
|
10
10
|
import { emitDts } from "../codegen.js";
|
|
11
|
-
import { loadConfig, lookupJsonbType, prepareConfigHash } from "../config.js";
|
|
11
|
+
import { loadConfig, lookupColumnType, lookupJsonbType, prepareConfigHash } from "../config.js";
|
|
12
12
|
import { functionCacheExists, readFunctionCache, writeFunctionCache } from "../function-cache.js";
|
|
13
13
|
import { introspectFunctions } from "../pg/functions.js";
|
|
14
14
|
import { buildParamMap } from "../pg/param-map.js";
|
|
@@ -18,7 +18,7 @@ import { containsUnknownType } from "../type-inspection.js";
|
|
|
18
18
|
import { originalPosition, rewriteNamedParameters } from "../sql-params.js";
|
|
19
19
|
const JSON_OIDS = new Set([114, 3802]);
|
|
20
20
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
21
|
-
const JSON_INPUT_VALUE =
|
|
21
|
+
const JSON_INPUT_VALUE = "unknown";
|
|
22
22
|
function jsonParameter(type) {
|
|
23
23
|
return `import("@onreza/sqlx-js").JsonParameter<${type}>`;
|
|
24
24
|
}
|
|
@@ -48,6 +48,12 @@ function resolveTs(oid, customLookup) {
|
|
|
48
48
|
}
|
|
49
49
|
return oidToTs(oid).ts;
|
|
50
50
|
}
|
|
51
|
+
function isScalarColumnType(oid, schema) {
|
|
52
|
+
if (JSON_OIDS.has(oid) || JSON_ARRAY_OIDS.has(oid) || arrayElementOid(oid) !== undefined)
|
|
53
|
+
return false;
|
|
54
|
+
const custom = schema.customType(oid);
|
|
55
|
+
return custom?.kind !== "enumArray" && custom?.kind !== "scalarArray" && custom?.kind !== "compositeArray";
|
|
56
|
+
}
|
|
51
57
|
function resolveColumnTs(f, schema, cfg) {
|
|
52
58
|
if (f.tableOid !== 0 && f.columnAttr !== 0) {
|
|
53
59
|
const tbl = schema.tableNameByOid(f.tableOid);
|
|
@@ -63,13 +69,25 @@ function resolveColumnTs(f, schema, cfg) {
|
|
|
63
69
|
if (decl)
|
|
64
70
|
return `(${decl})[]`;
|
|
65
71
|
}
|
|
72
|
+
if (isScalarColumnType(f.typeOid, schema)) {
|
|
73
|
+
const decl = lookupColumnType(cfg, tbl.schema, tbl.name, colName);
|
|
74
|
+
if (decl)
|
|
75
|
+
return decl;
|
|
76
|
+
}
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
return resolveTs(f.typeOid, (oid) => schema.customType(oid));
|
|
69
80
|
}
|
|
70
81
|
function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
82
|
+
const target = paramMap.get(paramIndex);
|
|
83
|
+
if (isScalarColumnType(paramOid, schema) && target) {
|
|
84
|
+
const column = resolveTargetColumn(target, schema);
|
|
85
|
+
const table = resolvedTargetTable(target, schema);
|
|
86
|
+
const decl = column && table ? lookupColumnType(cfg, table.schema, table.name, column) : undefined;
|
|
87
|
+
if (decl)
|
|
88
|
+
return decl;
|
|
89
|
+
}
|
|
71
90
|
if (JSON_OIDS.has(paramOid)) {
|
|
72
|
-
const target = paramMap.get(paramIndex);
|
|
73
91
|
if (target) {
|
|
74
92
|
const column = resolveTargetColumn(target, schema);
|
|
75
93
|
const table = resolvedTargetTable(target, schema);
|
|
@@ -80,7 +98,6 @@ function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
|
80
98
|
return jsonParameter(JSON_INPUT_VALUE);
|
|
81
99
|
}
|
|
82
100
|
if (JSON_ARRAY_OIDS.has(paramOid)) {
|
|
83
|
-
const target = paramMap.get(paramIndex);
|
|
84
101
|
if (target) {
|
|
85
102
|
const column = resolveTargetColumn(target, schema);
|
|
86
103
|
const table = resolvedTargetTable(target, schema);
|
|
@@ -188,7 +205,17 @@ function fatal(phase, error) {
|
|
|
188
205
|
return new PrepareFatalError(phase, message, location, { cause: error });
|
|
189
206
|
}
|
|
190
207
|
function formatSite(s) {
|
|
191
|
-
return `${s.file}:${s.line}:${s.column}`;
|
|
208
|
+
return `${s.file}:${s.line}:${s.column}${s.queryName ? ` [${s.queryName}]` : ""}`;
|
|
209
|
+
}
|
|
210
|
+
function siteDiagnostic(site) {
|
|
211
|
+
return {
|
|
212
|
+
file: site.file,
|
|
213
|
+
line: site.line,
|
|
214
|
+
column: site.column,
|
|
215
|
+
query: site.query,
|
|
216
|
+
queryId: fingerprint(site.query),
|
|
217
|
+
...(site.queryName ? { queryName: site.queryName } : {}),
|
|
218
|
+
};
|
|
192
219
|
}
|
|
193
220
|
function snippet(query, max = 80) {
|
|
194
221
|
const oneLine = query.replace(/\s+/g, " ").trim();
|
|
@@ -224,10 +251,7 @@ function inferenceDiagnostics(entry, site, strict) {
|
|
|
224
251
|
severity: strict ? "error" : "warning",
|
|
225
252
|
phase: "inference",
|
|
226
253
|
message,
|
|
227
|
-
|
|
228
|
-
line: site.line,
|
|
229
|
-
column: site.column,
|
|
230
|
-
query: site.query,
|
|
254
|
+
...siteDiagnostic(site),
|
|
231
255
|
}));
|
|
232
256
|
}
|
|
233
257
|
export async function openSession(opts) {
|
|
@@ -372,10 +396,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
372
396
|
severity: "error",
|
|
373
397
|
phase: "result-shape",
|
|
374
398
|
message,
|
|
375
|
-
|
|
376
|
-
line: site.line,
|
|
377
|
-
column: site.column,
|
|
378
|
-
query: site.query,
|
|
399
|
+
...siteDiagnostic(site),
|
|
379
400
|
});
|
|
380
401
|
err(` ✗ ${formatSite(site)} — ${message}`);
|
|
381
402
|
err(` query: ${snippet(site.query)}`);
|
|
@@ -392,10 +413,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
392
413
|
severity: "error",
|
|
393
414
|
phase: "describe",
|
|
394
415
|
message: e.message,
|
|
395
|
-
|
|
396
|
-
line: site.line,
|
|
397
|
-
column: site.column,
|
|
398
|
-
query: site.query,
|
|
416
|
+
...siteDiagnostic(site),
|
|
399
417
|
...(e.code ? { code: e.code } : {}),
|
|
400
418
|
...(position ? { position } : {}),
|
|
401
419
|
...(e.hint ? { hint: e.hint } : {}),
|
|
@@ -416,10 +434,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
416
434
|
severity: "error",
|
|
417
435
|
phase: "describe",
|
|
418
436
|
message: e.message,
|
|
419
|
-
|
|
420
|
-
line: site.line,
|
|
421
|
-
column: site.column,
|
|
422
|
-
query: site.query,
|
|
437
|
+
...siteDiagnostic(site),
|
|
423
438
|
});
|
|
424
439
|
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
|
|
425
440
|
err(` query: ${snippet(site.query)}`);
|
|
@@ -457,10 +472,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
457
472
|
severity: "error",
|
|
458
473
|
phase: "analyze",
|
|
459
474
|
message: e.message,
|
|
460
|
-
|
|
461
|
-
line: site.line,
|
|
462
|
-
column: site.column,
|
|
463
|
-
query: site.query,
|
|
475
|
+
...siteDiagnostic(site),
|
|
464
476
|
});
|
|
465
477
|
err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
|
|
466
478
|
err(` query: ${snippet(site.query)}`);
|
|
@@ -476,10 +488,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
476
488
|
severity: "error",
|
|
477
489
|
phase: "param-map",
|
|
478
490
|
message: e.message,
|
|
479
|
-
|
|
480
|
-
line: site.line,
|
|
481
|
-
column: site.column,
|
|
482
|
-
query: site.query,
|
|
491
|
+
...siteDiagnostic(site),
|
|
483
492
|
});
|
|
484
493
|
err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
|
|
485
494
|
err(` query: ${snippet(site.query)}`);
|
|
@@ -580,12 +589,17 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
580
589
|
return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
|
|
581
590
|
}
|
|
582
591
|
let functions;
|
|
583
|
-
if (
|
|
592
|
+
if (userCfg.functionCatalog === false) {
|
|
593
|
+
functions = [];
|
|
594
|
+
}
|
|
595
|
+
else if (input.reuseFunctions && functionCacheExists(opts.cacheDir)) {
|
|
584
596
|
functions = readFunctionCache(opts.cacheDir);
|
|
585
597
|
}
|
|
586
598
|
else {
|
|
587
599
|
try {
|
|
588
|
-
functions = await introspectFunctions(client, schema
|
|
600
|
+
functions = await introspectFunctions(client, schema, {
|
|
601
|
+
includeExtensionOwned: userCfg.functionCatalog?.includeExtensionOwned === true,
|
|
602
|
+
});
|
|
589
603
|
}
|
|
590
604
|
catch (error) {
|
|
591
605
|
throw fatal("introspect", error);
|
|
@@ -663,10 +677,7 @@ export async function runPrepare(opts) {
|
|
|
663
677
|
severity: "error",
|
|
664
678
|
phase: "cache",
|
|
665
679
|
message: "query is not in the offline cache",
|
|
666
|
-
|
|
667
|
-
line: site.line,
|
|
668
|
-
column: site.column,
|
|
669
|
-
query,
|
|
680
|
+
...siteDiagnostic(site),
|
|
670
681
|
});
|
|
671
682
|
if (!opts.json) {
|
|
672
683
|
console.error(`stale: ${formatSite(site)} — query not in cache`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { QueryExecutionMode } from "../query.js";
|
|
2
|
+
export type QueriesPhase = "config" | "scan" | "cache" | "embed";
|
|
3
|
+
export declare class QueriesError extends Error {
|
|
4
|
+
readonly phase: QueriesPhase;
|
|
5
|
+
readonly file?: string | undefined;
|
|
6
|
+
readonly line?: number | undefined;
|
|
7
|
+
readonly column?: number | undefined;
|
|
8
|
+
constructor(phase: QueriesPhase, message: string, file?: string | undefined, line?: number | undefined, column?: number | undefined, options?: {
|
|
9
|
+
cause?: unknown;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export type QueryInventoryItem = {
|
|
13
|
+
queryId: string;
|
|
14
|
+
queryNames: string[];
|
|
15
|
+
query: string;
|
|
16
|
+
cardinalities: QueryExecutionMode[];
|
|
17
|
+
sqlFilePaths: string[];
|
|
18
|
+
callSites: {
|
|
19
|
+
file: string;
|
|
20
|
+
line: number;
|
|
21
|
+
column: number;
|
|
22
|
+
}[];
|
|
23
|
+
cacheStatus: "current" | "stale" | "missing";
|
|
24
|
+
};
|
|
25
|
+
export type QueryInventory = {
|
|
26
|
+
formatVersion: 1;
|
|
27
|
+
ok: true;
|
|
28
|
+
queries: QueryInventoryItem[];
|
|
29
|
+
orphanedCacheIds: string[];
|
|
30
|
+
};
|
|
31
|
+
export declare function buildQueryInventory(root: string, cacheDir: string): Promise<QueryInventory>;
|
|
32
|
+
export declare function emitEmbeddedSqlModule(path: string, inventory: QueryInventory): void;
|
|
33
|
+
export declare function runQueries(options: {
|
|
34
|
+
root: string;
|
|
35
|
+
cacheDir: string;
|
|
36
|
+
json?: boolean;
|
|
37
|
+
embedPath?: string;
|
|
38
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, relative } from "node:path";
|
|
4
|
+
import { Cache, CacheManifestStaleError, readCacheManifest } from "../cache.js";
|
|
5
|
+
import { loadConfig, prepareConfigHash } from "../config.js";
|
|
6
|
+
import { queryId } from "../query-id.js";
|
|
7
|
+
import { ScanError, scanProject } from "../scan/scanner.js";
|
|
8
|
+
export class QueriesError extends Error {
|
|
9
|
+
phase;
|
|
10
|
+
file;
|
|
11
|
+
line;
|
|
12
|
+
column;
|
|
13
|
+
constructor(phase, message, file, line, column, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.phase = phase;
|
|
16
|
+
this.file = file;
|
|
17
|
+
this.line = line;
|
|
18
|
+
this.column = column;
|
|
19
|
+
this.name = "QueriesError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function queriesError(phase, error) {
|
|
23
|
+
if (error instanceof QueriesError)
|
|
24
|
+
return error;
|
|
25
|
+
if (error instanceof ScanError) {
|
|
26
|
+
return new QueriesError(phase, error.message, error.file, error.line, error.column, { cause: error });
|
|
27
|
+
}
|
|
28
|
+
return new QueriesError(phase, error instanceof Error ? error.message : String(error), undefined, undefined, undefined, { cause: error });
|
|
29
|
+
}
|
|
30
|
+
export async function buildQueryInventory(root, cacheDir) {
|
|
31
|
+
let config;
|
|
32
|
+
try {
|
|
33
|
+
config = await loadConfig(root);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw queriesError("config", error);
|
|
37
|
+
}
|
|
38
|
+
let sites;
|
|
39
|
+
try {
|
|
40
|
+
sites = scanProject(root, config.scan);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw queriesError("scan", error);
|
|
44
|
+
}
|
|
45
|
+
const cache = new Cache(cacheDir);
|
|
46
|
+
let manifestCurrent = false;
|
|
47
|
+
try {
|
|
48
|
+
const manifest = readCacheManifest(cacheDir);
|
|
49
|
+
manifestCurrent = manifest?.configHash === prepareConfigHash(config);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!(error instanceof CacheManifestStaleError)) {
|
|
53
|
+
throw queriesError("cache", error);
|
|
54
|
+
}
|
|
55
|
+
manifestCurrent = false;
|
|
56
|
+
}
|
|
57
|
+
let cacheIds;
|
|
58
|
+
try {
|
|
59
|
+
cacheIds = cache.list().map(({ fp }) => fp);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw queriesError("cache", error);
|
|
63
|
+
}
|
|
64
|
+
const cached = new Set(cacheIds);
|
|
65
|
+
const grouped = new Map();
|
|
66
|
+
for (const site of sites) {
|
|
67
|
+
const id = queryId(site.query);
|
|
68
|
+
const group = grouped.get(id) ?? [];
|
|
69
|
+
group.push(site);
|
|
70
|
+
grouped.set(id, group);
|
|
71
|
+
}
|
|
72
|
+
const queries = [...grouped.entries()].map(([id, group]) => {
|
|
73
|
+
return {
|
|
74
|
+
queryId: id,
|
|
75
|
+
queryNames: [...new Set(group.flatMap((site) => site.queryName ? [site.queryName] : []))].sort(),
|
|
76
|
+
query: group[0].query,
|
|
77
|
+
cardinalities: [...new Set(group.map((site) => site.cardinality ?? "many"))].sort(),
|
|
78
|
+
sqlFilePaths: [...new Set(group.flatMap((site) => site.sqlFilePath ? [site.sqlFilePath] : []))].sort(),
|
|
79
|
+
callSites: group
|
|
80
|
+
.map(({ file, line, column }) => ({ file, line, column }))
|
|
81
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column),
|
|
82
|
+
cacheStatus: !cached.has(id) ? "missing" : manifestCurrent ? "current" : "stale",
|
|
83
|
+
};
|
|
84
|
+
}).sort((a, b) => a.queryId.localeCompare(b.queryId));
|
|
85
|
+
const active = new Set(queries.map((query) => query.queryId));
|
|
86
|
+
const orphanedCacheIds = cacheIds.filter((fp) => !active.has(fp)).sort();
|
|
87
|
+
return { formatVersion: 1, ok: true, queries, orphanedCacheIds };
|
|
88
|
+
}
|
|
89
|
+
export function emitEmbeddedSqlModule(path, inventory) {
|
|
90
|
+
const sqlFiles = Object.fromEntries(inventory.queries
|
|
91
|
+
.flatMap((query) => query.sqlFilePaths.map((file) => [file, query.query]))
|
|
92
|
+
.sort(([a], [b]) => a.localeCompare(b)));
|
|
93
|
+
const content = [
|
|
94
|
+
"// AUTO-GENERATED by sqlx-js. Do not edit.",
|
|
95
|
+
"// Run `sqlx-js queries --embed <path>` to regenerate.",
|
|
96
|
+
"",
|
|
97
|
+
`export const sqlxJsEmbeddedSql = ${JSON.stringify(sqlFiles, null, 2)} as const;`,
|
|
98
|
+
"",
|
|
99
|
+
].join("\n");
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
+
const tmp = `${path}.tmp-${randomBytes(4).toString("hex")}`;
|
|
102
|
+
writeFileSync(tmp, content);
|
|
103
|
+
try {
|
|
104
|
+
renameSync(tmp, path);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
try {
|
|
108
|
+
unlinkSync(tmp);
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export async function runQueries(options) {
|
|
115
|
+
const inventory = await buildQueryInventory(options.root, options.cacheDir);
|
|
116
|
+
if (options.embedPath) {
|
|
117
|
+
try {
|
|
118
|
+
emitEmbeddedSqlModule(options.embedPath, inventory);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw queriesError("embed", error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (options.json) {
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
...inventory,
|
|
127
|
+
...(options.embedPath ? { embeddedModule: relative(options.root, options.embedPath).replace(/\\/g, "/") } : {}),
|
|
128
|
+
}, null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const query of inventory.queries) {
|
|
132
|
+
const names = query.queryNames.length > 0 ? ` ${query.queryNames.join(",")}` : "";
|
|
133
|
+
console.log(`${query.queryId}${names} ${query.cardinalities.join(",")} ${query.cacheStatus}`);
|
|
134
|
+
for (const site of query.callSites)
|
|
135
|
+
console.log(` ${site.file}:${site.line}:${site.column}`);
|
|
136
|
+
}
|
|
137
|
+
if (inventory.queries.length === 0)
|
|
138
|
+
console.log("no sqlx-js queries found");
|
|
139
|
+
if (inventory.orphanedCacheIds.length > 0)
|
|
140
|
+
console.log(`orphaned cache: ${inventory.orphanedCacheIds.join(", ")}`);
|
|
141
|
+
if (options.embedPath)
|
|
142
|
+
console.log(`embedded SQL module: ${relative(options.root, options.embedPath).replace(/\\/g, "/")}`);
|
|
143
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -5,7 +5,11 @@ export type ScanConfig = {
|
|
|
5
5
|
};
|
|
6
6
|
export type SqlxJsConfig = {
|
|
7
7
|
jsonbTypes?: Record<string, string>;
|
|
8
|
+
columnTypes?: Record<string, string>;
|
|
8
9
|
customTypes?: Record<string, string>;
|
|
10
|
+
functionCatalog?: false | {
|
|
11
|
+
includeExtensionOwned?: boolean;
|
|
12
|
+
};
|
|
9
13
|
scan?: ScanConfig;
|
|
10
14
|
schema?: {
|
|
11
15
|
provider?: "builtin" | "pgschema";
|
|
@@ -32,3 +36,4 @@ export declare function runtimeVersion(): {
|
|
|
32
36
|
export declare function nativeTypeScriptEnabled(): boolean | string;
|
|
33
37
|
export declare function envFilePath(root: string): string;
|
|
34
38
|
export declare function lookupJsonbType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
|
|
39
|
+
export declare function lookupColumnType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
|
package/dist/src/config.js
CHANGED
|
@@ -65,8 +65,27 @@ function validateConfig(value, path) {
|
|
|
65
65
|
const config = value;
|
|
66
66
|
if (config.jsonbTypes !== undefined)
|
|
67
67
|
validateStringRecord(config.jsonbTypes, "jsonbTypes", path);
|
|
68
|
+
if (config.columnTypes !== undefined)
|
|
69
|
+
validateStringRecord(config.columnTypes, "columnTypes", path);
|
|
68
70
|
if (config.customTypes !== undefined)
|
|
69
71
|
validateStringRecord(config.customTypes, "customTypes", path);
|
|
72
|
+
if (config.jsonbTypes && config.columnTypes) {
|
|
73
|
+
const jsonKeys = Object.keys(config.jsonbTypes);
|
|
74
|
+
const columnKeys = Object.keys(config.columnTypes);
|
|
75
|
+
const conflicts = jsonKeys.filter((jsonKey) => columnKeys.some((columnKey) => jsonKey === columnKey || jsonKey.endsWith(`.${columnKey}`) || columnKey.endsWith(`.${jsonKey}`)));
|
|
76
|
+
if (conflicts.length > 0) {
|
|
77
|
+
throw new Error(`sqlx-js: ${path} maps the same column in jsonbTypes and columnTypes: ${conflicts.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (config.functionCatalog !== undefined && config.functionCatalog !== false) {
|
|
81
|
+
if (!config.functionCatalog || typeof config.functionCatalog !== "object" || Array.isArray(config.functionCatalog)) {
|
|
82
|
+
throw new Error(`sqlx-js: ${path} functionCatalog must be false or an object`);
|
|
83
|
+
}
|
|
84
|
+
const functionCatalog = config.functionCatalog;
|
|
85
|
+
if (functionCatalog.includeExtensionOwned !== undefined && typeof functionCatalog.includeExtensionOwned !== "boolean") {
|
|
86
|
+
throw new Error(`sqlx-js: ${path} functionCatalog.includeExtensionOwned must be a boolean`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
70
89
|
if (config.scan !== undefined) {
|
|
71
90
|
if (!config.scan || typeof config.scan !== "object" || Array.isArray(config.scan)) {
|
|
72
91
|
throw new Error(`sqlx-js: ${path} scan must be an object`);
|
|
@@ -100,7 +119,11 @@ function validateConfig(value, path) {
|
|
|
100
119
|
export function prepareConfigHash(cfg) {
|
|
101
120
|
const value = stableValue({
|
|
102
121
|
jsonbTypes: cfg.jsonbTypes ?? {},
|
|
122
|
+
columnTypes: cfg.columnTypes ?? {},
|
|
103
123
|
customTypes: cfg.customTypes ?? {},
|
|
124
|
+
functionCatalog: cfg.functionCatalog === false
|
|
125
|
+
? false
|
|
126
|
+
: { includeExtensionOwned: cfg.functionCatalog?.includeExtensionOwned === true },
|
|
104
127
|
});
|
|
105
128
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16);
|
|
106
129
|
}
|
|
@@ -159,3 +182,9 @@ export function lookupJsonbType(cfg, schema, table, column) {
|
|
|
159
182
|
return (types[`${schema}.${table}.${column}`] ??
|
|
160
183
|
types[`${table}.${column}`]);
|
|
161
184
|
}
|
|
185
|
+
export function lookupColumnType(cfg, schema, table, column) {
|
|
186
|
+
const types = cfg.columnTypes;
|
|
187
|
+
if (!types)
|
|
188
|
+
return undefined;
|
|
189
|
+
return types[`${schema}.${table}.${column}`] ?? types[`${table}.${column}`];
|
|
190
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as rt from "./postgres-runtime.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TypedFile as TypedFileFor, TypedForRegistry, TypedSqlForRegistry } from "./typed.js";
|
|
3
|
+
import type { QueryParamsFor, QueryResultFor, QueryRowFor } from "./query.js";
|
|
3
4
|
export interface KnownQueries {
|
|
4
5
|
}
|
|
5
6
|
export interface KnownFileQueries {
|
|
@@ -22,13 +23,13 @@ export { defineConfig } from "./config.js";
|
|
|
22
23
|
export type { ScanConfig, SqlxJsConfig } from "./config.js";
|
|
23
24
|
export type { SslMode, ConnConfig } from "./pg/wire.js";
|
|
24
25
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
25
|
-
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
26
|
+
export { NoRowsError, TooManyRowsError, SQLSTATE, isPgError } from "./runtime.js";
|
|
26
27
|
export type { TransactionOptions, MigrateOptions, OnQueryEvent, OnQueryHook, OnQueryHookError } from "./runtime.js";
|
|
27
|
-
export type { ExecuteResult, JsonParameter, PgArrayParameter } from "./runtime.js";
|
|
28
|
+
export type { ExecuteResult, JsonParameter, PgArrayParameter, JsonCompatible, KnownSqlState } from "./runtime.js";
|
|
28
29
|
export type { PostgresClient, PostgresOptions, CreateClientOptions } from "./postgres-runtime.js";
|
|
29
30
|
export type TypedFile = TypedFileFor<KnownFileQueries>;
|
|
30
|
-
export type TypedSql =
|
|
31
|
-
export type Typed =
|
|
31
|
+
export type TypedSql = TypedSqlForRegistry<DefaultQueryRegistry>;
|
|
32
|
+
export type Typed = TypedForRegistry<DefaultQueryRegistry, import("./runtime.js").TransactionOptions>;
|
|
32
33
|
export type QueryRegistry = {
|
|
33
34
|
queries: object;
|
|
34
35
|
fileQueries: object;
|
|
@@ -39,11 +40,18 @@ export interface DefaultQueryRegistry {
|
|
|
39
40
|
functions: KnownFunctions;
|
|
40
41
|
}
|
|
41
42
|
export type SqlClient<Registry extends QueryRegistry = DefaultQueryRegistry> = {
|
|
42
|
-
sql:
|
|
43
|
+
sql: TypedForRegistry<Registry, import("./runtime.js").TransactionOptions>;
|
|
43
44
|
unsafe: Unsafe;
|
|
44
45
|
client: import("./postgres-runtime.js").PostgresClient;
|
|
45
46
|
close: () => Promise<void>;
|
|
46
47
|
};
|
|
48
|
+
export type SqlExecutor<Registry extends QueryRegistry = DefaultQueryRegistry> = TypedSqlForRegistry<Registry>;
|
|
49
|
+
export type QueryParams<Definition, Registry extends QueryRegistry = DefaultQueryRegistry> = QueryParamsFor<Definition, Registry>;
|
|
50
|
+
export type QueryRow<Definition, Registry extends QueryRegistry = DefaultQueryRegistry> = QueryRowFor<Definition, Registry>;
|
|
51
|
+
export type QueryResult<Definition, Registry extends QueryRegistry = DefaultQueryRegistry> = QueryResultFor<Definition, Registry>;
|
|
52
|
+
export type { QueryDefinition, QueryExecutionMode } from "./query.js";
|
|
53
|
+
export { defineQuery } from "./query.js";
|
|
54
|
+
export { queryId } from "./query-id.js";
|
|
47
55
|
export declare const sql: Typed;
|
|
48
56
|
export type Unsafe = (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
49
57
|
export declare const unsafe: Unsafe;
|
package/dist/src/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as rt from "./postgres-runtime.js";
|
|
2
2
|
export { defineConfig } from "./config.js";
|
|
3
3
|
export { PgError, ConnectionLostError } from "./pg/wire.js";
|
|
4
|
-
export { NoRowsError, TooManyRowsError } from "./runtime.js";
|
|
4
|
+
export { NoRowsError, TooManyRowsError, SQLSTATE, isPgError } from "./runtime.js";
|
|
5
|
+
export { defineQuery } from "./query.js";
|
|
6
|
+
export { queryId } from "./query-id.js";
|
|
5
7
|
export const sql = rt.sql;
|
|
6
8
|
export const unsafe = rt.unsafe;
|
|
7
9
|
export const getClient = rt.getClient;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { FunctionEntry } from "../function-cache.js";
|
|
2
2
|
import { type PgClient } from "./wire.js";
|
|
3
3
|
import { SchemaCache } from "./schema.js";
|
|
4
|
-
export declare function introspectFunctions(client: PgClient, schema: SchemaCache
|
|
4
|
+
export declare function introspectFunctions(client: PgClient, schema: SchemaCache, options?: {
|
|
5
|
+
includeExtensionOwned?: boolean;
|
|
6
|
+
}): Promise<FunctionEntry[]>;
|
package/dist/src/pg/functions.js
CHANGED
|
@@ -2,8 +2,8 @@ import { decodeText } from "./wire.js";
|
|
|
2
2
|
const JSON_OIDS = new Set([114, 3802]);
|
|
3
3
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
4
4
|
const JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
|
|
5
|
-
export async function introspectFunctions(client, schema) {
|
|
6
|
-
const rows = await loadFunctionRows(client);
|
|
5
|
+
export async function introspectFunctions(client, schema, options = {}) {
|
|
6
|
+
const rows = await loadFunctionRows(client, options.includeExtensionOwned === true);
|
|
7
7
|
const typeOids = new Set();
|
|
8
8
|
for (const row of rows) {
|
|
9
9
|
typeOids.add(row.returnOid);
|
|
@@ -15,7 +15,7 @@ export async function introspectFunctions(client, schema) {
|
|
|
15
15
|
await schema.loadCustomTypes([...typeOids]);
|
|
16
16
|
return rows.map((row) => toEntry(row, schema)).sort((a, b) => a.signature.localeCompare(b.signature));
|
|
17
17
|
}
|
|
18
|
-
async function loadFunctionRows(client) {
|
|
18
|
+
async function loadFunctionRows(client, includeExtensionOwned) {
|
|
19
19
|
const result = await client.simpleQueryAll(`
|
|
20
20
|
SELECT
|
|
21
21
|
n.nspname,
|
|
@@ -36,6 +36,14 @@ async function loadFunctionRows(client) {
|
|
|
36
36
|
FROM pg_proc p
|
|
37
37
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
38
38
|
WHERE ${userSchemaFilter("n")}
|
|
39
|
+
${includeExtensionOwned ? "" : `AND NOT EXISTS (
|
|
40
|
+
SELECT 1
|
|
41
|
+
FROM pg_depend d
|
|
42
|
+
WHERE d.classid = 'pg_proc'::regclass
|
|
43
|
+
AND d.objid = p.oid
|
|
44
|
+
AND d.refclassid = 'pg_extension'::regclass
|
|
45
|
+
AND d.deptype = 'e'
|
|
46
|
+
)`}
|
|
39
47
|
ORDER BY n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)
|
|
40
48
|
`);
|
|
41
49
|
return result.rows.map((row) => ({
|
|
@@ -10,6 +10,7 @@ export type CreateClientOptions = PostgresOptions & {
|
|
|
10
10
|
statementTimeoutMs?: number;
|
|
11
11
|
fileRoot?: string;
|
|
12
12
|
reloadSqlFiles?: boolean;
|
|
13
|
+
sqlFiles?: Readonly<Record<string, string>>;
|
|
13
14
|
};
|
|
14
15
|
export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
|
|
15
16
|
export declare function getClient(): PostgresClient;
|
|
@@ -19,6 +20,7 @@ export declare function setClient(client: PostgresClient, options?: {
|
|
|
19
20
|
prepare?: boolean;
|
|
20
21
|
fileRoot?: string;
|
|
21
22
|
reloadSqlFiles?: boolean;
|
|
23
|
+
sqlFiles?: Readonly<Record<string, string>>;
|
|
22
24
|
}): void;
|
|
23
25
|
export declare function close(): Promise<void>;
|
|
24
26
|
export declare function createSqlClient(url?: string | undefined, options?: CreateClientOptions): {
|
|
@@ -13,13 +13,15 @@ class PostgresRuntimeClient {
|
|
|
13
13
|
prepare;
|
|
14
14
|
fileRoot;
|
|
15
15
|
reloadSqlFiles;
|
|
16
|
-
|
|
16
|
+
sqlFiles;
|
|
17
|
+
constructor(client, onQuery, onQueryHookError, prepare = true, fileRoot = resolvedFileRoot(), reloadSqlFiles = false, sqlFiles) {
|
|
17
18
|
this.client = client;
|
|
18
19
|
this.onQuery = onQuery;
|
|
19
20
|
this.onQueryHookError = onQueryHookError;
|
|
20
21
|
this.prepare = prepare;
|
|
21
22
|
this.fileRoot = fileRoot;
|
|
22
23
|
this.reloadSqlFiles = reloadSqlFiles;
|
|
24
|
+
this.sqlFiles = sqlFiles;
|
|
23
25
|
}
|
|
24
26
|
async query(query, params) {
|
|
25
27
|
return await this.client.unsafe(query, params, { prepare: this.prepare });
|
|
@@ -43,7 +45,7 @@ class PostgresRuntimeClient {
|
|
|
43
45
|
if (!("begin" in this.client))
|
|
44
46
|
throw new Error("sqlx-js.transaction: nested transactions are not supported");
|
|
45
47
|
return await this.client.begin(async (tx) => {
|
|
46
|
-
return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.onQueryHookError, this.prepare, this.fileRoot, this.reloadSqlFiles));
|
|
48
|
+
return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.onQueryHookError, this.prepare, this.fileRoot, this.reloadSqlFiles, this.sqlFiles));
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
async close() {
|
|
@@ -162,7 +164,7 @@ function installJsonArrayCodecs(client) {
|
|
|
162
164
|
export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
163
165
|
if (!url)
|
|
164
166
|
throw new Error("sqlx-js: DATABASE_URL is not set");
|
|
165
|
-
const { onQuery, onQueryHookError, statementTimeoutMs, fileRoot, reloadSqlFiles, ...pgOptions } = options;
|
|
167
|
+
const { onQuery, onQueryHookError, statementTimeoutMs, fileRoot, reloadSqlFiles, sqlFiles, ...pgOptions } = options;
|
|
166
168
|
const connection = statementTimeoutMs !== undefined
|
|
167
169
|
? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
|
|
168
170
|
: pgOptions.connection;
|
|
@@ -177,13 +179,14 @@ export function createClient(url = process.env.DATABASE_URL, options = {}) {
|
|
|
177
179
|
prepare: pgOptions.prepare ?? true,
|
|
178
180
|
fileRoot: resolvedFileRoot(fileRoot),
|
|
179
181
|
reloadSqlFiles: reloadSqlFiles ?? false,
|
|
182
|
+
sqlFiles,
|
|
180
183
|
};
|
|
181
184
|
return client;
|
|
182
185
|
}
|
|
183
186
|
function createDefaultClient() {
|
|
184
187
|
const client = createClient();
|
|
185
188
|
const attached = client[HOOKS];
|
|
186
|
-
return new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles);
|
|
189
|
+
return new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles, attached.sqlFiles);
|
|
187
190
|
}
|
|
188
191
|
function getRuntimeClient() {
|
|
189
192
|
defaultClient ??= createDefaultClient();
|
|
@@ -195,7 +198,7 @@ export function getClient() {
|
|
|
195
198
|
export function setClient(client, options) {
|
|
196
199
|
installJsonArrayCodecs(client);
|
|
197
200
|
const attached = client[HOOKS];
|
|
198
|
-
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.onQueryHookError ?? attached?.onQueryHookError, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot), options?.reloadSqlFiles ?? attached?.reloadSqlFiles ?? false);
|
|
201
|
+
defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.onQueryHookError ?? attached?.onQueryHookError, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot), options?.reloadSqlFiles ?? attached?.reloadSqlFiles ?? false, options?.sqlFiles ?? attached?.sqlFiles);
|
|
199
202
|
}
|
|
200
203
|
export async function close() {
|
|
201
204
|
if (defaultClient) {
|
|
@@ -206,7 +209,7 @@ export async function close() {
|
|
|
206
209
|
export function createSqlClient(url = process.env.DATABASE_URL, options = {}) {
|
|
207
210
|
const client = createClient(url, options);
|
|
208
211
|
const attached = client[HOOKS];
|
|
209
|
-
const runtimeClient = new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles);
|
|
212
|
+
const runtimeClient = new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles, attached.sqlFiles);
|
|
210
213
|
const runtime = createSqlRuntime(() => runtimeClient);
|
|
211
214
|
return {
|
|
212
215
|
...runtime,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function queryId(query: string): string;
|