@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.
Files changed (39) hide show
  1. package/README.md +129 -36
  2. package/ROADMAP.md +3 -4
  3. package/dist/bin/sqlx-js.js +137 -26
  4. package/dist/src/artifacts.d.ts +9 -0
  5. package/dist/src/artifacts.js +26 -0
  6. package/dist/src/cache.d.ts +17 -0
  7. package/dist/src/cache.js +83 -1
  8. package/dist/src/codegen.d.ts +2 -1
  9. package/dist/src/codegen.js +34 -5
  10. package/dist/src/commands/doctor.d.ts +16 -0
  11. package/dist/src/commands/doctor.js +196 -0
  12. package/dist/src/commands/init.d.ts +1 -0
  13. package/dist/src/commands/init.js +36 -9
  14. package/dist/src/commands/migrate.js +7 -22
  15. package/dist/src/commands/pgschema.d.ts +31 -0
  16. package/dist/src/commands/pgschema.js +208 -0
  17. package/dist/src/commands/prepare.d.ts +40 -5
  18. package/dist/src/commands/prepare.js +344 -58
  19. package/dist/src/commands/watch.d.ts +1 -0
  20. package/dist/src/commands/watch.js +23 -9
  21. package/dist/src/config.d.ts +27 -0
  22. package/dist/src/config.js +141 -11
  23. package/dist/src/function-cache.d.ts +18 -0
  24. package/dist/src/function-cache.js +38 -0
  25. package/dist/src/index.d.ts +6 -2
  26. package/dist/src/index.js +2 -1
  27. package/dist/src/pg/functions.d.ts +4 -0
  28. package/dist/src/pg/functions.js +150 -0
  29. package/dist/src/pg/oids.js +2 -0
  30. package/dist/src/pg/schema.d.ts +1 -0
  31. package/dist/src/pg/schema.js +3 -0
  32. package/dist/src/postgres-runtime.d.ts +3 -0
  33. package/dist/src/postgres-runtime.js +66 -29
  34. package/dist/src/runtime.d.ts +36 -3
  35. package/dist/src/runtime.js +91 -22
  36. package/dist/src/scan/scanner.d.ts +9 -2
  37. package/dist/src/scan/scanner.js +79 -28
  38. package/dist/src/typed.d.ts +10 -0
  39. 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 JSON_INPUT = 'import("@onreza/sqlx-js").JsonInput';
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) || JSON_ARRAY_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 JSON_ARRAY_OIDS.has(paramOid) ? `(${decl})[]` : decl;
85
+ return arrayParameter(jsonParameter(decl));
64
86
  }
65
- return JSON_ARRAY_OIDS.has(paramOid) ? `(${JSON_INPUT})[]` : JSON_INPUT;
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
- const userCfg = await loadConfig(opts.root);
133
- const cfg = parseDatabaseUrl(opts.databaseUrl);
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
- await client.connect();
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
- const sites = scanProject(opts.root);
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
- await schema.loadAttributes(allAttrRefs);
247
- await schema.loadTableNamesByOid(allTableOids);
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 Set();
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
- dmlTablesToLoad.add(`${t.schema ?? "public"}.${t.table}`);
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
- if (dmlTablesToLoad.size > 0) {
282
- const names = [...dmlTablesToLoad].map((k) => {
283
- const [schema, name] = k.split(".");
284
- return { schema, name: name };
285
- });
286
- await schema.loadTableNames(names);
287
- const oids = [];
288
- for (const n of names) {
289
- const oid = schema.resolveTable(n.schema, n.name);
290
- if (oid !== undefined)
291
- oids.push(oid);
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
- await schema.loadColumnsForTables(oids);
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
- await schema.loadCustomTypes([...unknownOids]);
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
- let pruned = 0;
342
- if (opts.prune !== false) {
343
- pruned = cache.prune(unique.keys()).length;
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
- emitDts(opts.dtsPath, entries);
348
- return { entries: entries.length, failures, pruned };
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
- const sites = scanProject(opts.root);
353
- console.log(`scanned: found ${sites.length} sql() call site(s)`);
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
- let stale = 0;
552
+ const diagnostics = [];
365
553
  for (const { fp, query, sites: ss } of unique.values()) {
366
554
  if (!cache.has(fp)) {
367
- stale++;
368
- console.error(`stale: ${formatSite(ss[0])} — query not in cache`);
369
- console.error(` query: ${snippet(query)}`);
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
- if (stale > 0) {
373
- console.error(`\nsqlx-js prepare --check: ${stale} stale/missing entries. Run \`sqlx-js prepare\` against a live DB.`);
374
- process.exit(1);
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
- console.error(`\n${r.failures} query/queries failed to prepare`);
389
- await session.client.end();
390
- process.exit(1);
629
+ if (!opts.json)
630
+ console.error(`\n${r.failures} query/queries failed to prepare`);
631
+ process.exitCode = 1;
632
+ return;
391
633
  }
392
- console.log(`\nprepared ${r.entries} unique query/queries → ${opts.dtsPath}`);
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,4 +1,5 @@
1
1
  import { openSession, prepareOnce, type PrepareOptions, type PrepareSession } from "./prepare.js";
2
+ export declare function shouldWatchFile(filename: string): boolean;
2
3
  export type WatchPrepareHookResult = {
3
4
  resetSession?: boolean;
4
5
  };
@@ -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
- if (hookResult?.resetSession === true) {
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
- const raw = filename.toString();
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
  });
@@ -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;