@onreza/sqlx-js 0.4.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 +85 -34
- package/ROADMAP.md +1 -3
- package/dist/bin/sqlx-js.js +91 -20
- 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.js +14 -2
- package/dist/src/commands/doctor.d.ts +16 -0
- package/dist/src/commands/doctor.js +196 -0
- package/dist/src/commands/init.js +6 -10
- package/dist/src/commands/migrate.js +7 -22
- package/dist/src/commands/pgschema.d.ts +6 -0
- package/dist/src/commands/pgschema.js +30 -0
- package/dist/src/commands/prepare.d.ts +40 -6
- package/dist/src/commands/prepare.js +341 -63
- package/dist/src/commands/watch.d.ts +1 -0
- package/dist/src/commands/watch.js +23 -9
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +141 -11
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- 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,18 +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";
|
|
9
12
|
import { readFunctionCache, writeFunctionCache } from "../function-cache.js";
|
|
10
13
|
import { introspectFunctions } from "../pg/functions.js";
|
|
11
14
|
import { buildParamMap } from "../pg/param-map.js";
|
|
12
15
|
import { mergeExtensionTypes } from "../pg/extensions.js";
|
|
16
|
+
import { compareArtifacts } from "../artifacts.js";
|
|
13
17
|
const JSON_OIDS = new Set([114, 3802]);
|
|
14
18
|
const JSON_ARRAY_OIDS = new Set([199, 3807]);
|
|
15
|
-
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
|
+
}
|
|
16
26
|
function enumUnion(values) {
|
|
17
27
|
if (values.length === 0)
|
|
18
28
|
return "never";
|
|
@@ -56,15 +66,39 @@ function resolveColumnTs(f, schema, cfg) {
|
|
|
56
66
|
return resolveTs(f.typeOid, (oid) => schema.customType(oid));
|
|
57
67
|
}
|
|
58
68
|
function resolveParamTs(paramIndex, paramOid, paramMap, schema, cfg) {
|
|
59
|
-
if (JSON_OIDS.has(paramOid)
|
|
69
|
+
if (JSON_OIDS.has(paramOid)) {
|
|
60
70
|
const target = paramMap.get(paramIndex);
|
|
61
71
|
if (target) {
|
|
62
72
|
const column = resolveTargetColumn(target, schema);
|
|
63
73
|
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
64
74
|
if (decl)
|
|
65
|
-
return
|
|
75
|
+
return jsonParameter(decl);
|
|
66
76
|
}
|
|
67
|
-
return
|
|
77
|
+
return jsonParameter(JSON_INPUT_VALUE);
|
|
78
|
+
}
|
|
79
|
+
if (JSON_ARRAY_OIDS.has(paramOid)) {
|
|
80
|
+
const target = paramMap.get(paramIndex);
|
|
81
|
+
if (target) {
|
|
82
|
+
const column = resolveTargetColumn(target, schema);
|
|
83
|
+
const decl = column ? lookupJsonbType(cfg, target.schema ?? "public", target.table, column) : undefined;
|
|
84
|
+
if (decl)
|
|
85
|
+
return arrayParameter(jsonParameter(decl));
|
|
86
|
+
}
|
|
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);
|
|
68
102
|
}
|
|
69
103
|
return resolveTs(paramOid, (oid) => schema.customType(oid));
|
|
70
104
|
}
|
|
@@ -114,6 +148,29 @@ function isAliasOrExpression(f, schema) {
|
|
|
114
148
|
const real = schema.columnNameByAttno(f.tableOid, f.columnAttr);
|
|
115
149
|
return real !== undefined && real !== f.name;
|
|
116
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
|
+
}
|
|
117
174
|
function formatSite(s) {
|
|
118
175
|
return `${s.file}:${s.line}:${s.column}`;
|
|
119
176
|
}
|
|
@@ -131,10 +188,28 @@ function siteUsage(sites) {
|
|
|
131
188
|
};
|
|
132
189
|
}
|
|
133
190
|
export async function openSession(opts) {
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|
|
136
205
|
const client = new PgClient(cfg);
|
|
137
|
-
|
|
206
|
+
try {
|
|
207
|
+
await client.connect();
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
await client.end().catch(() => { });
|
|
211
|
+
throw fatal("connect", error);
|
|
212
|
+
}
|
|
138
213
|
const schema = new SchemaCache(client);
|
|
139
214
|
schema.setTypeRegistry(mergeExtensionTypes(userCfg.customTypes));
|
|
140
215
|
return { client, schema, userCfg };
|
|
@@ -192,8 +267,15 @@ export async function describeAll(cfg, sessionClient, queries, concurrency) {
|
|
|
192
267
|
return results;
|
|
193
268
|
}
|
|
194
269
|
export async function prepareOnce(opts, session, log = console.log, err = console.error, concurrency = defaultPrepareConcurrency()) {
|
|
195
|
-
|
|
270
|
+
let sites;
|
|
271
|
+
try {
|
|
272
|
+
sites = scanProject(opts.root, session.userCfg.scan);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
throw fatal("scan", error);
|
|
276
|
+
}
|
|
196
277
|
log(`scanned: found ${sites.length} sql() call site(s)`);
|
|
278
|
+
const diagnostics = [];
|
|
197
279
|
const cache = new Cache(opts.cacheDir);
|
|
198
280
|
const unique = new Map();
|
|
199
281
|
for (const s of sites) {
|
|
@@ -219,6 +301,18 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
219
301
|
failures++;
|
|
220
302
|
const e = outcome.error;
|
|
221
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
|
+
});
|
|
222
316
|
const extras = [];
|
|
223
317
|
if (e.position)
|
|
224
318
|
extras.push(`pos ${e.position}`);
|
|
@@ -231,6 +325,15 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
231
325
|
err(` query: ${snippet(query)}`);
|
|
232
326
|
}
|
|
233
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
|
+
});
|
|
234
337
|
err(` ✗ ${formatSite(site)} — describe failed: ${e.message}`);
|
|
235
338
|
err(` query: ${snippet(query)}`);
|
|
236
339
|
}
|
|
@@ -245,8 +348,13 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
245
348
|
}
|
|
246
349
|
}
|
|
247
350
|
}
|
|
248
|
-
|
|
249
|
-
|
|
351
|
+
try {
|
|
352
|
+
await schema.loadAttributes(allAttrRefs);
|
|
353
|
+
await schema.loadTableNamesByOid(allTableOids);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
throw fatal("introspect", error);
|
|
357
|
+
}
|
|
250
358
|
const analyses = new Map();
|
|
251
359
|
const paramMaps = new Map();
|
|
252
360
|
const failedFps = new Set();
|
|
@@ -258,6 +366,15 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
258
366
|
catch (e) {
|
|
259
367
|
failures++;
|
|
260
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
|
+
});
|
|
261
378
|
err(` ✗ ${formatSite(site)} — analyze failed: ${e.message}`);
|
|
262
379
|
err(` query: ${snippet(r.query)}`);
|
|
263
380
|
continue;
|
|
@@ -268,31 +385,44 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
268
385
|
catch (e) {
|
|
269
386
|
failures++;
|
|
270
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
|
+
});
|
|
271
397
|
err(` ✗ ${formatSite(site)} — paramMap failed: ${e.message}`);
|
|
272
398
|
err(` query: ${snippet(r.query)}`);
|
|
273
399
|
}
|
|
274
400
|
}
|
|
275
|
-
const dmlTablesToLoad = new
|
|
401
|
+
const dmlTablesToLoad = new Map();
|
|
276
402
|
for (const pm of paramMaps.values()) {
|
|
277
403
|
for (const idx of pm.dmlBound) {
|
|
278
404
|
const t = pm.targets.get(idx);
|
|
279
|
-
if (t)
|
|
280
|
-
|
|
405
|
+
if (t) {
|
|
406
|
+
const schema = t.schema ?? "public";
|
|
407
|
+
dmlTablesToLoad.set(JSON.stringify([schema, t.table]), { schema, name: t.table });
|
|
408
|
+
}
|
|
281
409
|
}
|
|
282
410
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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);
|
|
294
422
|
}
|
|
295
|
-
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
throw fatal("introspect", error);
|
|
296
426
|
}
|
|
297
427
|
const unknownOids = new Set();
|
|
298
428
|
for (const r of raw) {
|
|
@@ -303,8 +433,14 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
303
433
|
if (!isBuiltinOid(f.typeOid))
|
|
304
434
|
unknownOids.add(f.typeOid);
|
|
305
435
|
}
|
|
306
|
-
|
|
436
|
+
try {
|
|
437
|
+
await schema.loadCustomTypes([...unknownOids]);
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
throw fatal("introspect", error);
|
|
441
|
+
}
|
|
307
442
|
const entries = [];
|
|
443
|
+
const generated = [];
|
|
308
444
|
for (const r of raw) {
|
|
309
445
|
if (failedFps.has(r.fp))
|
|
310
446
|
continue;
|
|
@@ -317,7 +453,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
317
453
|
const entry = {
|
|
318
454
|
query: r.query,
|
|
319
455
|
...siteUsage(r.sites),
|
|
320
|
-
paramOids: r.paramOids,
|
|
456
|
+
paramOids: r.paramOids.map(portableCacheOid),
|
|
321
457
|
paramTsTypes: r.paramOids.map((o, idx) => resolveParamTs(idx + 1, o, pm.targets, schema, userCfg)),
|
|
322
458
|
paramNullable: r.paramOids.map((_o, idx) => resolveParamNullable(idx + 1, pm, schema)),
|
|
323
459
|
columns: r.fields.map((f, i) => {
|
|
@@ -325,7 +461,7 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
325
461
|
const treatAsOverride = parsed.override !== undefined && isAliasOrExpression(f, schema);
|
|
326
462
|
return {
|
|
327
463
|
name: parsed.name,
|
|
328
|
-
typeOid: f.typeOid,
|
|
464
|
+
typeOid: portableCacheOid(f.typeOid),
|
|
329
465
|
tsType: resolveColumnTs(f, schema, userCfg),
|
|
330
466
|
nullable: analysis.perColumnNullable[i] ?? true,
|
|
331
467
|
...(treatAsOverride ? { override: parsed.override } : {}),
|
|
@@ -334,31 +470,76 @@ export async function prepareOnce(opts, session, log = console.log, err = consol
|
|
|
334
470
|
hasResultSet: r.fields.length > 0,
|
|
335
471
|
...(analysis.degraded ? { degraded: analysis.degraded } : {}),
|
|
336
472
|
};
|
|
337
|
-
cache.write(r.fp, entry);
|
|
338
473
|
entries.push(entry);
|
|
474
|
+
generated.push({ fp: r.fp, entry });
|
|
339
475
|
const nn = entry.columns.filter((c) => !effectiveNullable(c)).length;
|
|
340
476
|
const tag = entry.degraded ? ` [degraded: ${entry.degraded.reason}]` : "";
|
|
341
477
|
log(` ✓ ${formatSite(r.sites[0])} → ${r.paramOids.length} param(s), ${r.fields.length} col(s) [${nn} non-null]${tag}`);
|
|
342
478
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
pruned = cache.prune(unique.keys()).length;
|
|
346
|
-
if (pruned > 0)
|
|
347
|
-
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
479
|
+
if (failures > 0) {
|
|
480
|
+
return { sites: sites.length, entries: entries.length, failures, pruned: 0, functions: 0, diagnostics };
|
|
348
481
|
}
|
|
349
|
-
let functions
|
|
350
|
-
|
|
482
|
+
let functions;
|
|
483
|
+
try {
|
|
351
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;
|
|
492
|
+
if (pruned > 0)
|
|
493
|
+
log(`pruned ${pruned} orphaned cache entry/entries`);
|
|
352
494
|
writeFunctionCache(opts.cacheDir, functions);
|
|
495
|
+
writeCacheManifest(opts.cacheDir, prepareConfigHash(userCfg));
|
|
496
|
+
emitDts(opts.dtsPath, entries, functions);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
throw fatal("cache", error);
|
|
353
500
|
}
|
|
354
|
-
|
|
355
|
-
return { entries: entries.length, failures, pruned, functions: functions.length };
|
|
501
|
+
return { sites: sites.length, entries: entries.length, failures, pruned, functions: functions.length, diagnostics };
|
|
356
502
|
}
|
|
357
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
|
+
}
|
|
358
519
|
if (opts.check) {
|
|
359
|
-
|
|
360
|
-
|
|
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)`);
|
|
361
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
|
+
}
|
|
362
543
|
const unique = new Map();
|
|
363
544
|
for (const s of sites) {
|
|
364
545
|
const fp = fingerprint(s.query);
|
|
@@ -368,38 +549,135 @@ export async function runPrepare(opts) {
|
|
|
368
549
|
else
|
|
369
550
|
unique.set(fp, { fp, query: s.query, sites: [s] });
|
|
370
551
|
}
|
|
371
|
-
|
|
552
|
+
const diagnostics = [];
|
|
372
553
|
for (const { fp, query, sites: ss } of unique.values()) {
|
|
373
554
|
if (!cache.has(fp)) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
+
}
|
|
377
569
|
}
|
|
378
570
|
}
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
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.`);
|
|
587
|
+
}
|
|
588
|
+
process.exitCode = 1;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
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`);
|
|
382
619
|
}
|
|
383
|
-
const entries = [...unique.values()].map((u) => {
|
|
384
|
-
const entry = cache.read(u.fp);
|
|
385
|
-
return entry ? { ...entry, ...siteUsage(u.sites) } : null;
|
|
386
|
-
}).filter((e) => e !== null);
|
|
387
|
-
const functions = readFunctionCache(opts.cacheDir);
|
|
388
|
-
emitDts(opts.dtsPath, entries, functions);
|
|
389
|
-
console.log(`ok — ${entries.length} unique queries, ${functions.length} function(s), types regenerated`);
|
|
390
620
|
return;
|
|
391
621
|
}
|
|
392
622
|
const session = await openSession(opts);
|
|
393
623
|
try {
|
|
394
|
-
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
|
+
}
|
|
395
628
|
if (r.failures > 0) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
process.
|
|
629
|
+
if (!opts.json)
|
|
630
|
+
console.error(`\n${r.failures} query/queries failed to prepare`);
|
|
631
|
+
process.exitCode = 1;
|
|
632
|
+
return;
|
|
399
633
|
}
|
|
400
|
-
|
|
634
|
+
if (!opts.json)
|
|
635
|
+
console.log(`\nprepared ${r.entries} unique query/queries, ${r.functions} function(s) → ${opts.dtsPath}`);
|
|
401
636
|
}
|
|
402
637
|
finally {
|
|
403
638
|
await session.client.end();
|
|
404
639
|
}
|
|
405
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,11 @@
|
|
|
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;
|
|
4
9
|
schema?: {
|
|
5
10
|
provider?: "builtin" | "pgschema";
|
|
6
11
|
file?: string;
|
|
@@ -8,5 +13,21 @@ export type SqlxJsConfig = {
|
|
|
8
13
|
command?: string;
|
|
9
14
|
};
|
|
10
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;
|
|
11
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;
|
|
12
33
|
export declare function lookupJsonbType(cfg: SqlxJsConfig, schema: string, table: string, column: string): string | undefined;
|