@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
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { PgClient, parseDatabaseUrl, PgError } from "../pg/wire.js";
|
|
2
5
|
import { SchemaCache, compositeLiteral } from "../pg/schema.js";
|
|
3
6
|
import { analyzeQuery } from "../pg/analyze.js";
|
|
4
|
-
import { isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
5
|
-
import { scanProject } from "../scan/scanner.js";
|
|
6
|
-
import { Cache, fingerprint, effectiveNullable } from "../cache.js";
|
|
7
|
+
import { arrayElementOid, isBuiltinOid, oidToTs } from "../pg/oids.js";
|
|
8
|
+
import { ScanError, scanProject } from "../scan/scanner.js";
|
|
9
|
+
import { assertCacheManifest, Cache, fingerprint, effectiveNullable, portableCacheOid, writeCacheManifest, } from "../cache.js";
|
|
7
10
|
import { emitDts } from "../codegen.js";
|
|
8
|
-
import { loadConfig, lookupJsonbType } from "../config.js";
|
|
11
|
+
import { loadConfig, lookupJsonbType, prepareConfigHash } from "../config.js";
|
|
12
|
+
import { readFunctionCache, writeFunctionCache } from "../function-cache.js";
|
|
13
|
+
import { introspectFunctions } from "../pg/functions.js";
|
|
9
14
|
import { buildParamMap } from "../pg/param-map.js";
|
|
10
15
|
import { mergeExtensionTypes } from "../pg/extensions.js";
|
|
16
|
+
import { compareArtifacts } from "../artifacts.js";
|
|
11
17
|
const JSON_OIDS = new Set([114, 3802]);
|
|
12
18
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
13
|
-
const
|
|
19
|
+
const JSON_INPUT_VALUE = 'import("@onreza/sqlx-js").JsonInputValue';
|
|
20
|
+
function jsonParameter(type) {
|
|
21
|
+
return `import("@onreza/sqlx-js").JsonParameter<${type}>`;
|
|
22
|
+
}
|
|
23
|
+
function arrayParameter(type) {
|
|
24
|
+
return `import("@onreza/sqlx-js").PgArrayParameter<${type}>`;
|
|
25
|
+
}
|
|
14
26
|
function enumUnion(values) {
|
|
15
27
|
if (values.length === 0)
|
|
16
28
|
return "never";
|
|
@@ -54,15 +66,39 @@ function resolveColumnTs(f, schema, cfg) {
|
|
|
54
66
|
return resolveTs(f.typeOid, (oid) => schema.customType(oid));
|
|
55
67
|
}
|
|
56
68
|
function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
57
|
-
if (JSON_OIDS.has(paramOid)
|
|
69
|
+
if (JSON_OIDS.has(paramOid)) {
|
|
70
|
+
const target = paramMap.get(paramIndex);
|
|
71
|
+
if (target) {
|
|
72
|
+
const column = resolveTargetColumn(target, schema);
|
|
73
|
+
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
74
|
+
if (decl)
|
|
75
|
+
return jsonParameter(decl);
|
|
76
|
+
}
|
|
77
|
+
return jsonParameter(JSON_INPUT_VALUE);
|
|
78
|
+
}
|
|
79
|
+
if (JSON_ARRAY_OIDS.has(paramOid)) {
|
|
58
80
|
const target = paramMap.get(paramIndex);
|
|
59
81
|
if (target) {
|
|
60
82
|
const column = resolveTargetColumn(target, schema);
|
|
61
83
|
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
62
84
|
if (decl)
|
|
63
|
-
return
|
|
85
|
+
return arrayParameter(jsonParameter(decl));
|
|
64
86
|
}
|
|
65
|
-
return
|
|
87
|
+
return arrayParameter(jsonParameter(JSON_INPUT_VALUE));
|
|
88
|
+
}
|
|
89
|
+
const builtinElementOid = arrayElementOid(paramOid);
|
|
90
|
+
if (builtinElementOid !== undefined) {
|
|
91
|
+
return arrayParameter(resolveTs(builtinElementOid, (oid) => schema.customType(oid)));
|
|
92
|
+
}
|
|
93
|
+
const custom = schema.customType(paramOid);
|
|
94
|
+
if (custom?.kind === "enumArray")
|
|
95
|
+
return arrayParameter(enumUnion(custom.element.values));
|
|
96
|
+
if (custom?.kind === "scalarArray")
|
|
97
|
+
return arrayParameter(custom.element.tsType);
|
|
98
|
+
if (custom?.kind === "compositeArray")
|
|
99
|
+
return arrayParameter(compositeLiteral(custom.element));
|
|
100
|
+
if (custom) {
|
|
101
|
+
return resolveTs(paramOid, () => custom);
|
|
66
102
|
}
|
|
67
103
|
return resolveTs(paramOid, (oid) => schema.customType(oid));
|
|
68
104
|
}
|
|
@@ -112,6 +148,29 @@ function isAliasOrExpression(f, schema) {
|
|
|
112
148
|
const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
|
|
113
149
|
return real !== undefined && real !== f.name;
|
|
114
150
|
}
|
|
151
|
+
export class PrepareFatalError extends Error {
|
|
152
|
+
phase;
|
|
153
|
+
file;
|
|
154
|
+
line;
|
|
155
|
+
column;
|
|
156
|
+
constructor(phase, message, location = {}, options) {
|
|
157
|
+
super(message, options);
|
|
158
|
+
this.phase = phase;
|
|
159
|
+
this.name = "PrepareFatalError";
|
|
160
|
+
this.file = location.file;
|
|
161
|
+
this.line = location.line;
|
|
162
|
+
this.column = location.column;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function fatal(phase, error) {
|
|
166
|
+
if (error instanceof PrepareFatalError)
|
|
167
|
+
return error;
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
const location = error instanceof ScanError
|
|
170
|
+
? { file: error.file, line: error.line, column: error.column }
|
|
171
|
+
: {};
|
|
172
|
+
return new PrepareFatalError(phase, message, location, { cause: error });
|
|
173
|
+
}
|
|
115
174
|
function formatSite(s) {
|
|
116
175
|
return `${s.file}:${s.line}:${s.column}`;
|
|
117
176
|
}
|
|
@@ -129,10 +188,28 @@ function siteUsage(sites) {
|
|
|
129
188
|
};
|
|
130
189
|
}
|
|
131
190
|
export async function openSession(opts) {
|
|
132
|
-
|
|
133
|
-
|
|
191
|
+
let userCfg;
|
|
192
|
+
try {
|
|
193
|
+
userCfg = await loadConfig(opts.root);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw fatal("config", error);
|
|
197
|
+
}
|
|
198
|
+
let cfg;
|
|
199
|
+
try {
|
|
200
|
+
cfg = parseDatabaseUrl(opts.databaseUrl);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
throw fatal("connect", error);
|
|
204
|
+
}
|
|
134
205
|
const client = new PgClient(cfg);
|
|
135
|
-
|
|
206
|
+
try {
|
|
207
|
+
await client.connect();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
await client.end().catch(() => { });
|
|
211
|
+
throw fatal("connect", error);
|
|
212
|
+
}
|
|
136
213
|
const schema = new SchemaCache(client);
|
|
137
214
|
schema.setTypeRegistry(mergeExtensionTypes(userCfg.customTypes));
|
|
138
215
|
return { client, schema, userCfg };
|
|
@@ -190,8 +267,15 @@ export async function describeAll(cfg, sessionClient, queries, concurrency) {
|
|
|
190
267
|
return results;
|
|
191
268
|
}
|
|
192
269
|
export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
|
|
193
|
-
|
|
270
|
+
let sites;
|
|
271
|
+
try {
|
|
272
|
+
sites = scanProject(opts.root, session.userCfg.scan);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw fatal("scan", error);
|
|
276
|
+
}
|
|
194
277
|
log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
278
|
+
const diagnostics = [];
|
|
195
279
|
const cache = new Cache(opts.cacheDir);
|
|
196
280
|
const unique = new Map();
|
|
197
281
|
for (const s of sites) {
|
|
@@ -217,6 +301,18 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
217
301
|
failures++;
|
|
218
302
|
const e = outcome.error;
|
|
219
303
|
if (e instanceof PgError) {
|
|
304
|
+
diagnostics.push({
|
|
305
|
+
severity: "error",
|
|
306
|
+
phase: "describe",
|
|
307
|
+
message: e.message,
|
|
308
|
+
file: site.file,
|
|
309
|
+
line: site.line,
|
|
310
|
+
column: site.column,
|
|
311
|
+
query,
|
|
312
|
+
...(e.code ? { code: e.code } : {}),
|
|
313
|
+
...(e.position ? { position: e.position } : {}),
|
|
314
|
+
...(e.hint ? { hint: e.hint } : {}),
|
|
315
|
+
});
|
|
220
316
|
const extras = [];
|
|
221
317
|
if (e.position)
|
|
222
318
|
extras.push(`pos ${e.position}`);
|
|
@@ -229,6 +325,15 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
229
325
|
err(` query: ${snippet(query)}`);
|
|
230
326
|
}
|
|
231
327
|
else {
|
|
328
|
+
diagnostics.push({
|
|
329
|
+
severity: "error",
|
|
330
|
+
phase: "describe",
|
|
331
|
+
message: e.message,
|
|
332
|
+
file: site.file,
|
|
333
|
+
line: site.line,
|
|
334
|
+
column: site.column,
|
|
335
|
+
query,
|
|
336
|
+
});
|
|
232
337
|
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
|
|
233
338
|
err(` query: ${snippet(query)}`);
|
|
234
339
|
}
|
|
@@ -243,8 +348,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
243
348
|
}
|
|
244
349
|
}
|
|
245
350
|
}
|
|
246
|
-
|
|
247
|
-
|
|
351
|
+
try {
|
|
352
|
+
await schema.loadAttributes(allAttrRefs);
|
|
353
|
+
await schema.loadTableNamesByOid(allTableOids);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
throw fatal("introspect", error);
|
|
357
|
+
}
|
|
248
358
|
const analyses = new Map();
|
|
249
359
|
const paramMaps = new Map();
|
|
250
360
|
const failedFps = new Set();
|
|
@@ -256,6 +366,15 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
256
366
|
catch (e) {
|
|
257
367
|
failures++;
|
|
258
368
|
failedFps.add(r.fp);
|
|
369
|
+
diagnostics.push({
|
|
370
|
+
severity: "error",
|
|
371
|
+
phase: "analyze",
|
|
372
|
+
message: e.message,
|
|
373
|
+
file: site.file,
|
|
374
|
+
line: site.line,
|
|
375
|
+
column: site.column,
|
|
376
|
+
query: r.query,
|
|
377
|
+
});
|
|
259
378
|
err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
|
|
260
379
|
err(` query: ${snippet(r.query)}`);
|
|
261
380
|
continue;
|
|
@@ -266,31 +385,44 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
266
385
|
catch (e) {
|
|
267
386
|
failures++;
|
|
268
387
|
failedFps.add(r.fp);
|
|
388
|
+
diagnostics.push({
|
|
389
|
+
severity: "error",
|
|
390
|
+
phase: "param-map",
|
|
391
|
+
message: e.message,
|
|
392
|
+
file: site.file,
|
|
393
|
+
line: site.line,
|
|
394
|
+
column: site.column,
|
|
395
|
+
query: r.query,
|
|
396
|
+
});
|
|
269
397
|
err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
|
|
270
398
|
err(` query: ${snippet(r.query)}`);
|
|
271
399
|
}
|
|
272
400
|
}
|
|
273
|
-
const dmlTablesToLoad = new
|
|
401
|
+
const dmlTablesToLoad = new Map();
|
|
274
402
|
for (const pm of paramMaps.values()) {
|
|
275
403
|
for (const idx of pm.dmlBound) {
|
|
276
404
|
const t = pm.targets.get(idx);
|
|
277
|
-
if (t)
|
|
278
|
-
|
|
405
|
+
if (t) {
|
|
406
|
+
const schema = t.schema ?? "public";
|
|
407
|
+
dmlTablesToLoad.set(JSON.stringify([schema, t.table]), { schema, name: t.table });
|
|
408
|
+
}
|
|
279
409
|
}
|
|
280
410
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
411
|
+
try {
|
|
412
|
+
if (dmlTablesToLoad.size > 0) {
|
|
413
|
+
const names = [...dmlTablesToLoad.values()];
|
|
414
|
+
await schema.loadTableNames(names);
|
|
415
|
+
const oids = [];
|
|
416
|
+
for (const n of names) {
|
|
417
|
+
const oid = schema.resolveTable(n.schema, n.name);
|
|
418
|
+
if (oid !== undefined)
|
|
419
|
+
oids.push(oid);
|
|
420
|
+
}
|
|
421
|
+
await schema.loadColumnsForTables(oids);
|
|
292
422
|
}
|
|
293
|
-
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
throw fatal("introspect", error);
|
|
294
426
|
}
|
|
295
427
|
const unknownOids = new Set();
|
|
296
428
|
for (const r of raw) {
|
|
@@ -301,8 +433,14 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
301
433
|
if (!isBuiltinOid(f.typeOid))
|
|
302
434
|
unknownOids.add(f.typeOid);
|
|
303
435
|
}
|
|
304
|
-
|
|
436
|
+
try {
|
|
437
|
+
await schema.loadCustomTypes([...unknownOids]);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
throw fatal("introspect", error);
|
|
441
|
+
}
|
|
305
442
|
const entries = [];
|
|
443
|
+
const generated = [];
|
|
306
444
|
for (const r of raw) {
|
|
307
445
|
if (failedFps.has(r.fp))
|
|
308
446
|
continue;
|
|
@@ -315,7 +453,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
315
453
|
const entry = {
|
|
316
454
|
query: r.query,
|
|
317
455
|
...siteUsage(r.sites),
|
|
318
|
-
paramOids: r.paramOids,
|
|
456
|
+
paramOids: r.paramOids.map(portableCacheOid),
|
|
319
457
|
paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
|
|
320
458
|
paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
|
|
321
459
|
columns: r.fields.map((f, i) => {
|
|
@@ -323,7 +461,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
323
461
|
const treatAsOverride = parsed.override !== undefined && isAliasOrExpression(f, schema);
|
|
324
462
|
return {
|
|
325
463
|
name: parsed.name,
|
|
326
|
-
typeOid: f.typeOid,
|
|
464
|
+
typeOid: portableCacheOid(f.typeOid),
|
|
327
465
|
tsType: resolveColumnTs(f, schema, userCfg),
|
|
328
466
|
nullable: analysis.perColumnNullable[i] ?? true,
|
|
329
467
|
...(treatAsOverride ? { override: parsed.override } : {}),
|
|
@@ -332,26 +470,76 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
332
470
|
hasResultSet: r.fields.length > 0,
|
|
333
471
|
...(analysis.degraded ? { degraded: analysis.degraded } : {}),
|
|
334
472
|
};
|
|
335
|
-
cache.write(r.fp, entry);
|
|
336
473
|
entries.push(entry);
|
|
474
|
+
generated.push({ fp: r.fp, entry });
|
|
337
475
|
const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
|
|
338
476
|
const tag = entry.degraded ? ` [degraded: ${entry.degraded.reason}]` : "";
|
|
339
477
|
log(` ✓ ${formatSite(r.sites[0])} → ${r.paramOids.length} param(s), ${r.fields.length} col(s) [${nn} non-null]${tag}`);
|
|
340
478
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
479
|
+
if (failures > 0) {
|
|
480
|
+
return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
|
|
481
|
+
}
|
|
482
|
+
let functions;
|
|
483
|
+
try {
|
|
484
|
+
functions = await introspectFunctions(client, schema);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
throw fatal("introspect", error);
|
|
488
|
+
}
|
|
489
|
+
let pruned;
|
|
490
|
+
try {
|
|
491
|
+
pruned = cache.replaceAll(generated, opts.prune !== false).length;
|
|
344
492
|
if (pruned > 0)
|
|
345
493
|
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
494
|
+
writeFunctionCache(opts.cacheDir, functions);
|
|
495
|
+
writeCacheManifest(opts.cacheDir, prepareConfigHash(userCfg));
|
|
496
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
346
497
|
}
|
|
347
|
-
|
|
348
|
-
|
|
498
|
+
catch (error) {
|
|
499
|
+
throw fatal("cache", error);
|
|
500
|
+
}
|
|
501
|
+
return { sites: sites.length, entries: entries.length, failures, pruned, functions: functions.length, diagnostics };
|
|
349
502
|
}
|
|
350
503
|
export async function runPrepare(opts) {
|
|
504
|
+
if (opts.verify) {
|
|
505
|
+
const verification = await verifyPrepareArtifacts(opts, opts.json ? () => { } : console.log, opts.json ? () => { } : console.error);
|
|
506
|
+
if (opts.json) {
|
|
507
|
+
console.log(JSON.stringify({
|
|
508
|
+
formatVersion: 1,
|
|
509
|
+
ok: verification.ok,
|
|
510
|
+
mode: "verify",
|
|
511
|
+
...verification.result,
|
|
512
|
+
changed: verification.changed,
|
|
513
|
+
}, null, 2));
|
|
514
|
+
}
|
|
515
|
+
if (!verification.ok)
|
|
516
|
+
process.exitCode = 1;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
351
519
|
if (opts.check) {
|
|
352
|
-
|
|
353
|
-
|
|
520
|
+
let userCfg;
|
|
521
|
+
try {
|
|
522
|
+
userCfg = await loadConfig(opts.root);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
throw fatal("config", error);
|
|
526
|
+
}
|
|
527
|
+
let sites;
|
|
528
|
+
try {
|
|
529
|
+
sites = scanProject(opts.root, userCfg.scan);
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
throw fatal("scan", error);
|
|
533
|
+
}
|
|
534
|
+
if (!opts.json)
|
|
535
|
+
console.log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
354
536
|
const cache = new Cache(opts.cacheDir);
|
|
537
|
+
try {
|
|
538
|
+
assertCacheManifest(opts.cacheDir, prepareConfigHash(userCfg));
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
throw fatal("cache", error);
|
|
542
|
+
}
|
|
355
543
|
const unique = new Map();
|
|
356
544
|
for (const s of sites) {
|
|
357
545
|
const fp = fingerprint(s.query);
|
|
@@ -361,37 +549,135 @@ export async function runPrepare(opts) {
|
|
|
361
549
|
else
|
|
362
550
|
unique.set(fp, { fp, query: s.query, sites: [s] });
|
|
363
551
|
}
|
|
364
|
-
|
|
552
|
+
const diagnostics = [];
|
|
365
553
|
for (const { fp, query, sites: ss } of unique.values()) {
|
|
366
554
|
if (!cache.has(fp)) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
555
|
+
const site = ss[0];
|
|
556
|
+
diagnostics.push({
|
|
557
|
+
severity: "error",
|
|
558
|
+
phase: "cache",
|
|
559
|
+
message: "query is not in the offline cache",
|
|
560
|
+
file: site.file,
|
|
561
|
+
line: site.line,
|
|
562
|
+
column: site.column,
|
|
563
|
+
query,
|
|
564
|
+
});
|
|
565
|
+
if (!opts.json) {
|
|
566
|
+
console.error(`stale: ${formatSite(site)} — query not in cache`);
|
|
567
|
+
console.error(` query: ${snippet(query)}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (diagnostics.length > 0) {
|
|
572
|
+
if (opts.json) {
|
|
573
|
+
console.log(JSON.stringify({
|
|
574
|
+
formatVersion: 1,
|
|
575
|
+
ok: false,
|
|
576
|
+
mode: "check",
|
|
577
|
+
sites: sites.length,
|
|
578
|
+
entries: unique.size - diagnostics.length,
|
|
579
|
+
failures: diagnostics.length,
|
|
580
|
+
pruned: 0,
|
|
581
|
+
functions: 0,
|
|
582
|
+
diagnostics,
|
|
583
|
+
}, null, 2));
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
console.error(`\nsqlx-js prepare --check: ${diagnostics.length} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
|
|
370
587
|
}
|
|
588
|
+
process.exitCode = 1;
|
|
589
|
+
return;
|
|
371
590
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
591
|
+
let entries;
|
|
592
|
+
let functions;
|
|
593
|
+
try {
|
|
594
|
+
entries = [...unique.values()].map((u) => {
|
|
595
|
+
const entry = cache.read(u.fp);
|
|
596
|
+
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
597
|
+
}).filter((e) => e !== null);
|
|
598
|
+
functions = readFunctionCache(opts.cacheDir);
|
|
599
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
throw fatal("cache", error);
|
|
603
|
+
}
|
|
604
|
+
if (opts.json) {
|
|
605
|
+
console.log(JSON.stringify({
|
|
606
|
+
formatVersion: 1,
|
|
607
|
+
ok: true,
|
|
608
|
+
mode: "check",
|
|
609
|
+
sites: sites.length,
|
|
610
|
+
entries: entries.length,
|
|
611
|
+
failures: 0,
|
|
612
|
+
pruned: 0,
|
|
613
|
+
functions: functions.length,
|
|
614
|
+
diagnostics: [],
|
|
615
|
+
}, null, 2));
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
|
|
375
619
|
}
|
|
376
|
-
const entries = [...unique.values()].map((u) => {
|
|
377
|
-
const entry = cache.read(u.fp);
|
|
378
|
-
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
379
|
-
}).filter((e) => e !== null);
|
|
380
|
-
emitDts(opts.dtsPath, entries);
|
|
381
|
-
console.log(`ok — ${entries.length} unique queries, types regenerated`);
|
|
382
620
|
return;
|
|
383
621
|
}
|
|
384
622
|
const session = await openSession(opts);
|
|
385
623
|
try {
|
|
386
|
-
const r = await prepareOnce(opts, session);
|
|
624
|
+
const r = await prepareOnce(opts, session, opts.json ? () => { } : console.log, opts.json ? () => { } : console.error);
|
|
625
|
+
if (opts.json) {
|
|
626
|
+
console.log(JSON.stringify({ formatVersion: 1, ok: r.failures === 0, mode: "prepare", ...r }, null, 2));
|
|
627
|
+
}
|
|
387
628
|
if (r.failures > 0) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
process.
|
|
629
|
+
if (!opts.json)
|
|
630
|
+
console.error(`\n${r.failures} query/queries failed to prepare`);
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
return;
|
|
391
633
|
}
|
|
392
|
-
|
|
634
|
+
if (!opts.json)
|
|
635
|
+
console.log(`\nprepared ${r.entries} unique query/queries, ${r.functions} function(s) → ${opts.dtsPath}`);
|
|
393
636
|
}
|
|
394
637
|
finally {
|
|
395
638
|
await session.client.end();
|
|
396
639
|
}
|
|
397
640
|
}
|
|
641
|
+
export async function verifyPrepareArtifacts(opts, log = console.log, err = console.error) {
|
|
642
|
+
const tmp = mkdtempSync(join(tmpdir(), "sqlx-js-verify-"));
|
|
643
|
+
const cacheDir = join(tmp, "cache");
|
|
644
|
+
const dtsPath = join(tmp, "sqlx-js-env.d.ts");
|
|
645
|
+
const verifyOpts = {
|
|
646
|
+
...opts,
|
|
647
|
+
cacheDir,
|
|
648
|
+
dtsPath,
|
|
649
|
+
check: false,
|
|
650
|
+
verify: false,
|
|
651
|
+
prune: true,
|
|
652
|
+
};
|
|
653
|
+
let session;
|
|
654
|
+
try {
|
|
655
|
+
session = await openSession(verifyOpts);
|
|
656
|
+
const result = await prepareOnce(verifyOpts, session, log, err);
|
|
657
|
+
if (result.failures > 0) {
|
|
658
|
+
err(`\n${result.failures} query/queries failed to prepare`);
|
|
659
|
+
return { ok: false, result, changed: [] };
|
|
660
|
+
}
|
|
661
|
+
let comparison;
|
|
662
|
+
try {
|
|
663
|
+
comparison = compareArtifacts({ cacheDir: opts.cacheDir, dtsPath: opts.dtsPath }, { cacheDir, dtsPath });
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
throw fatal("verify", error);
|
|
667
|
+
}
|
|
668
|
+
if (!comparison.ok) {
|
|
669
|
+
err("sqlx-js prepare --verify: generated artifacts are stale:");
|
|
670
|
+
for (const file of comparison.changed)
|
|
671
|
+
err(` ${file}`);
|
|
672
|
+
err("Run `sqlx-js prepare` and commit the regenerated artifacts.");
|
|
673
|
+
return { ok: false, result, changed: comparison.changed };
|
|
674
|
+
}
|
|
675
|
+
log(`verified ${result.entries} query/queries and ${result.functions} function(s); generated artifacts are current`);
|
|
676
|
+
return { ok: true, result, changed: [] };
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
if (session)
|
|
680
|
+
await session.client.end();
|
|
681
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { watch as fsWatch } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { openSession, prepareOnce } from "./prepare.js";
|
|
4
|
+
import { configHash, loadConfig } from "../config.js";
|
|
4
5
|
const EXT_RE = /\.(ts|tsx|mts|cts|sql)$/;
|
|
5
6
|
const SKIP_DIRS = ["node_modules", ".git", ".sqlx-js", "dist", "build", ".next"];
|
|
6
7
|
const DEBOUNCE_MS = 150;
|
|
8
|
+
const CONFIG_FILES = new Set([
|
|
9
|
+
"sqlx-js.config.ts",
|
|
10
|
+
"sqlx-js.config.mts",
|
|
11
|
+
"sqlx-js.config.js",
|
|
12
|
+
"sqlx-js.config.mjs",
|
|
13
|
+
]);
|
|
14
|
+
export function shouldWatchFile(filename) {
|
|
15
|
+
const file = filename.replace(/\\/g, "/");
|
|
16
|
+
if (SKIP_DIRS.some((dir) => file === dir || file.startsWith(`${dir}/`) || file.includes(`/${dir}/`))) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const base = basename(file);
|
|
20
|
+
if (base === "sqlx-js-env.d.ts" || base === "sqlx-js.d.ts")
|
|
21
|
+
return false;
|
|
22
|
+
if (CONFIG_FILES.has(base) || /^tsconfig(?:\.[^.]+)*\.json$/.test(base))
|
|
23
|
+
return true;
|
|
24
|
+
return EXT_RE.test(file);
|
|
25
|
+
}
|
|
7
26
|
const DEFAULT_DEPS = { openSession, prepareOnce };
|
|
8
27
|
async function closeSession(session) {
|
|
9
28
|
if (!session)
|
|
@@ -15,7 +34,9 @@ async function closeSession(session) {
|
|
|
15
34
|
}
|
|
16
35
|
export async function prepareWatchedOnce(opts, state, log, err, deps = DEFAULT_DEPS) {
|
|
17
36
|
const hookResult = await opts.beforePrepare?.();
|
|
18
|
-
|
|
37
|
+
const currentConfig = await loadConfig(opts.root);
|
|
38
|
+
if (hookResult?.resetSession === true ||
|
|
39
|
+
(state.session !== null && configHash(state.session.userCfg) !== configHash(currentConfig))) {
|
|
19
40
|
await closeSession(state.session);
|
|
20
41
|
state.session = null;
|
|
21
42
|
}
|
|
@@ -70,14 +91,7 @@ export async function runWatch(opts) {
|
|
|
70
91
|
const watcher = fsWatch(opts.root, { recursive: true }, (_event, filename) => {
|
|
71
92
|
if (!filename)
|
|
72
93
|
return;
|
|
73
|
-
|
|
74
|
-
const f = raw.replace(/\\/g, "/");
|
|
75
|
-
if (SKIP_DIRS.some((d) => f === d || f.startsWith(`${d}/`) || f.includes(`/${d}/`)))
|
|
76
|
-
return;
|
|
77
|
-
const base = basename(f);
|
|
78
|
-
if (base === "sqlx-js-env.d.ts" || base === "sqlx-js.d.ts")
|
|
79
|
-
return;
|
|
80
|
-
if (!EXT_RE.test(f))
|
|
94
|
+
if (!shouldWatchFile(filename.toString()))
|
|
81
95
|
return;
|
|
82
96
|
trigger();
|
|
83
97
|
});
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
|
+
export type ScanConfig = {
|
|
2
|
+
include?: string[];
|
|
3
|
+
exclude?: string[];
|
|
4
|
+
};
|
|
1
5
|
export type SqlxJsConfig = {
|
|
2
6
|
jsonbTypes?: Record<string, string>;
|
|
3
7
|
customTypes?: Record<string, string>;
|
|
8
|
+
scan?: ScanConfig;
|
|
9
|
+
schema?: {
|
|
10
|
+
provider?: "builtin" | "pgschema";
|
|
11
|
+
file?: string;
|
|
12
|
+
schemas?: string[];
|
|
13
|
+
command?: string;
|
|
14
|
+
};
|
|
4
15
|
};
|
|
16
|
+
export declare function defineConfig<T extends SqlxJsConfig>(config: T): T;
|
|
17
|
+
export declare function loadRootEnv(root: string): string | undefined;
|
|
18
|
+
export declare function configPath(root: string): string | undefined;
|
|
5
19
|
export declare function loadConfig(root: string): Promise<SqlxJsConfig>;
|
|
20
|
+
export declare function prepareConfigHash(cfg: SqlxJsConfig): string;
|
|
21
|
+
export declare function configHash(cfg: SqlxJsConfig): string;
|
|
22
|
+
export declare function loadConfigInfo(root: string): Promise<{
|
|
23
|
+
config: SqlxJsConfig;
|
|
24
|
+
path?: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare function assertSupportedRuntime(): void;
|
|
27
|
+
export declare function runtimeVersion(): {
|
|
28
|
+
runtime: "node" | "bun";
|
|
29
|
+
version: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function nativeTypeScriptEnabled(): boolean | string;
|
|
32
|
+
export declare function envFilePath(root: string): string;
|
|
6
33
|
export declare function lookupJsonbType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
|