@onreza/sqlx-js 0.6.0 → 0.8.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.
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { PgClient, parseDatabaseUrl, PgError } from "./pg/wire.js";
4
4
  import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./migration-core.js";
5
+ import { bindNamedParameters, rewriteNamedParameters } from "./sql-params.js";
5
6
  const PARAMETER_KIND = Symbol("sqlx-js.parameter");
6
7
  export function json(value) {
7
8
  return { [PARAMETER_KIND]: "json", value };
@@ -242,6 +243,11 @@ export const _internal = {
242
243
  toPgError,
243
244
  };
244
245
  async function runRawQuery(client, query, params) {
246
+ const observedQuery = query;
247
+ const observedParams = params;
248
+ const bound = bindNamedParameters(rewriteNamedParameters(query), params);
249
+ query = bound.query;
250
+ params = bound.params;
245
251
  const encoded = params.length === 0
246
252
  ? params
247
253
  : params.map((p) => client.transformParam ? client.transformParam(p) : encodeParam(p));
@@ -257,9 +263,9 @@ async function runRawQuery(client, query, params) {
257
263
  const start = performance.now();
258
264
  try {
259
265
  const result = await client.query(query, encoded);
260
- onQuery({
261
- query,
262
- params,
266
+ notifyQuery(client, {
267
+ query: observedQuery,
268
+ params: observedParams,
263
269
  durationMs: performance.now() - start,
264
270
  rowCount: result.count ?? result.length,
265
271
  });
@@ -267,10 +273,29 @@ async function runRawQuery(client, query, params) {
267
273
  }
268
274
  catch (e) {
269
275
  const error = toPgError(e) ?? e;
270
- onQuery({ query, params, durationMs: performance.now() - start, error });
276
+ notifyQuery(client, { query: observedQuery, params: observedParams, durationMs: performance.now() - start, error });
271
277
  throw error;
272
278
  }
273
279
  }
280
+ function notifyQuery(client, event) {
281
+ try {
282
+ const pending = client.onQuery?.(event);
283
+ if (pending)
284
+ void pending.catch((error) => notifyQueryHookError(client, error, event));
285
+ }
286
+ catch (error) {
287
+ notifyQueryHookError(client, error, event);
288
+ }
289
+ }
290
+ function notifyQueryHookError(client, error, event) {
291
+ try {
292
+ const pending = client.onQueryHookError?.(error, event);
293
+ if (pending)
294
+ void pending.catch(() => { });
295
+ }
296
+ catch {
297
+ }
298
+ }
274
299
  async function runQuery(client, query, params) {
275
300
  return renameRows(await runRawQuery(client, query, params));
276
301
  }
@@ -298,7 +323,7 @@ async function runOptional(client, query, params) {
298
323
  throw new TooManyRowsError(rows.length, "0 or 1");
299
324
  }
300
325
  const sqlFileCache = new Map();
301
- function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd()) {
326
+ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd(), reload = false) {
302
327
  const root = resolve(fileRoot);
303
328
  if (isAbsolute(path)) {
304
329
  throw new Error(`sqlx-js.sql.file: path must be relative to fileRoot: ${path}`);
@@ -309,8 +334,10 @@ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.c
309
334
  throw new Error(`sqlx-js.sql.file: path escapes fileRoot: ${path}`);
310
335
  }
311
336
  try {
312
- const st = statSync(full);
313
337
  const cached = sqlFileCache.get(full);
338
+ if (cached && !reload)
339
+ return cached.content;
340
+ const st = statSync(full);
314
341
  if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
315
342
  return cached.content;
316
343
  }
@@ -442,16 +469,16 @@ function makeBoundCallable(client) {
442
469
  return runQuery(client, query, params);
443
470
  });
444
471
  const file = (async (path, ...params) => {
445
- return runQuery(client, loadSqlFile(path, client.fileRoot), params);
472
+ return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
446
473
  });
447
474
  file.one = (async (path, ...params) => {
448
- return runOne(client, loadSqlFile(path, client.fileRoot), params);
475
+ return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
449
476
  });
450
477
  file.optional = (async (path, ...params) => {
451
- return runOptional(client, loadSqlFile(path, client.fileRoot), params);
478
+ return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
452
479
  });
453
480
  file.execute = (async (path, ...params) => {
454
- return runExecute(client, loadSqlFile(path, client.fileRoot), params);
481
+ return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
455
482
  });
456
483
  fn.file = file;
457
484
  fn.one = (async (query, ...params) => {
@@ -486,19 +513,19 @@ export function createSqlRuntime(getClient) {
486
513
  });
487
514
  const rootFile = (async (path, ...params) => {
488
515
  const client = getClient();
489
- return runQuery(client, loadSqlFile(path, client.fileRoot), params);
516
+ return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
490
517
  });
491
518
  rootFile.one = (async (path, ...params) => {
492
519
  const client = getClient();
493
- return runOne(client, loadSqlFile(path, client.fileRoot), params);
520
+ return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
494
521
  });
495
522
  rootFile.optional = (async (path, ...params) => {
496
523
  const client = getClient();
497
- return runOptional(client, loadSqlFile(path, client.fileRoot), params);
524
+ return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
498
525
  });
499
526
  rootFile.execute = (async (path, ...params) => {
500
527
  const client = getClient();
501
- return runExecute(client, loadSqlFile(path, client.fileRoot), params);
528
+ return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
502
529
  });
503
530
  root.file = rootFile;
504
531
  root.one = (async (query, ...params) => {
@@ -1,6 +1,7 @@
1
1
  import ts from "typescript";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { rewriteNamedParameters } from "../sql-params.js";
4
5
  export class ScanError extends Error {
5
6
  file;
6
7
  line;
@@ -76,7 +77,7 @@ function classifyCallee(callee, scope) {
76
77
  const methodName = callee.name.text;
77
78
  if (ts.isIdentifier(callee.expression)) {
78
79
  const id = callee.expression.text;
79
- if (scope.namespaces.has(id)) {
80
+ if (scope.namespaces.has(id) || scope.clients.has(id)) {
80
81
  if (methodName === "sql")
81
82
  return { kind: "inline" };
82
83
  return null;
@@ -95,8 +96,9 @@ function classifyCallee(callee, scope) {
95
96
  const mid = callee.expression;
96
97
  if (!ts.isIdentifier(mid.name))
97
98
  return null;
98
- // ns.sql.X(...) chains
99
- if (ts.isIdentifier(mid.expression) && scope.namespaces.has(mid.expression.text)) {
99
+ // ns.sql.X(...) and client.sql.X(...) chains
100
+ if (ts.isIdentifier(mid.expression) &&
101
+ (scope.namespaces.has(mid.expression.text) || scope.clients.has(mid.expression.text))) {
100
102
  if (mid.name.text !== "sql")
101
103
  return null;
102
104
  if (methodName === "one" || methodName === "optional" || methodName === "execute")
@@ -118,11 +120,11 @@ function classifyCallee(callee, scope) {
118
120
  return { kind: "file" };
119
121
  return null;
120
122
  }
121
- // ns.sql.file.X(...) chains
123
+ // ns.sql.file.X(...) and client.sql.file.X(...) chains
122
124
  if (ts.isPropertyAccessExpression(mid.expression) &&
123
125
  ts.isIdentifier(mid.expression.expression) &&
124
126
  ts.isIdentifier(mid.expression.name) &&
125
- scope.namespaces.has(mid.expression.expression.text) &&
127
+ (scope.namespaces.has(mid.expression.expression.text) || scope.clients.has(mid.expression.expression.text)) &&
126
128
  mid.expression.name.text === "sql" &&
127
129
  mid.name.text === "file" &&
128
130
  (methodName === "one" || methodName === "optional" || methodName === "execute")) {
@@ -133,9 +135,18 @@ function classifyCallee(callee, scope) {
133
135
  }
134
136
  export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
135
137
  const text = readFileSync(absPath, "utf8");
136
- const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TSX);
138
+ const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, scriptKind(absPath));
139
+ const parseDiagnostics = source.parseDiagnostics ?? [];
140
+ const parseError = parseDiagnostics.find((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
141
+ if (parseError) {
142
+ const start = parseError.start ?? 0;
143
+ const { line, character } = source.getLineAndCharacterOfPosition(start);
144
+ const file = relative(root, absPath).replace(/\\/g, "/");
145
+ throw new ScanError(file, line + 1, character + 1, ts.flattenDiagnosticMessageText(parseError.messageText, "\n"));
146
+ }
137
147
  const importedAliases = new Set();
138
148
  const importedNamespaces = new Set();
149
+ const importedClientFactories = new Set();
139
150
  for (const stmt of source.statements) {
140
151
  if (!ts.isImportDeclaration(stmt))
141
152
  continue;
@@ -158,23 +169,35 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
158
169
  const orig = (elem.propertyName ?? elem.name).text;
159
170
  if (orig === "sql")
160
171
  importedAliases.add(elem.name.text);
172
+ if (orig === "createSqlClient")
173
+ importedClientFactories.add(elem.name.text);
161
174
  }
162
175
  }
163
176
  }
164
- if (importedAliases.size === 0 && importedNamespaces.size === 0)
177
+ if (importedAliases.size === 0 && importedNamespaces.size === 0 && importedClientFactories.size === 0)
165
178
  return [];
166
179
  const out = [];
167
180
  const here = (node) => {
168
181
  const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
169
182
  return { line: line + 1, column: character + 1 };
170
183
  };
171
- const fileRel = relative(root, absPath);
184
+ const fileRel = relative(root, absPath).replace(/\\/g, "/");
172
185
  const recordInline = (first, args) => {
173
186
  if (!ts.isStringLiteralLike(first)) {
174
187
  const pos = here(first);
175
188
  throw new ScanError(fileRel, pos.line, pos.column, "sql() requires a string literal as first argument");
176
189
  }
177
190
  const pos = here(first);
191
+ let named;
192
+ try {
193
+ named = rewriteNamedParameters(first.text).names;
194
+ }
195
+ catch (error) {
196
+ throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
197
+ }
198
+ if (named.length > 0 && args.length !== 2) {
199
+ throw new ScanError(fileRel, pos.line, pos.column, "a query with named parameters requires exactly one parameter object");
200
+ }
178
201
  out.push({
179
202
  file: fileRel,
180
203
  line: pos.line,
@@ -207,6 +230,16 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
207
230
  }
208
231
  const query = readFileSync(abs, "utf8");
209
232
  const pos = here(first);
233
+ let named;
234
+ try {
235
+ named = rewriteNamedParameters(query).names;
236
+ }
237
+ catch (error) {
238
+ throw new ScanError(fileRel, pos.line, pos.column, error.message.replace(/^sqlx-js: /, ""));
239
+ }
240
+ if (named.length > 0 && args.length !== 2) {
241
+ throw new ScanError(fileRel, pos.line, pos.column, "a SQL file with named parameters requires exactly one parameter object");
242
+ }
210
243
  out.push({
211
244
  file: fileRel,
212
245
  line: pos.line,
@@ -233,6 +266,8 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
233
266
  let changed = false;
234
267
  const nextSql = new Set(scope.sqlAliases);
235
268
  const nextNs = new Set(scope.namespaces);
269
+ const nextFactories = new Set(scope.clientFactories);
270
+ const nextClients = new Set(scope.clients);
236
271
  for (const binding of bindings) {
237
272
  for (const a of scope.sqlAliases) {
238
273
  if (bindingDeclares(binding, a)) {
@@ -246,8 +281,44 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
246
281
  changed = true;
247
282
  }
248
283
  }
284
+ for (const a of scope.clientFactories) {
285
+ if (bindingDeclares(binding, a)) {
286
+ nextFactories.delete(a);
287
+ changed = true;
288
+ }
289
+ }
290
+ for (const a of scope.clients) {
291
+ if (bindingDeclares(binding, a)) {
292
+ nextClients.delete(a);
293
+ changed = true;
294
+ }
295
+ }
249
296
  }
250
- return changed ? { sqlAliases: nextSql, namespaces: nextNs } : scope;
297
+ return changed
298
+ ? { sqlAliases: nextSql, namespaces: nextNs, clientFactories: nextFactories, clients: nextClients }
299
+ : scope;
300
+ };
301
+ const isClientFactoryCall = (initializer, scope) => {
302
+ if (!initializer || !ts.isCallExpression(initializer))
303
+ return false;
304
+ const callee = initializer.expression;
305
+ if (ts.isIdentifier(callee))
306
+ return scope.clientFactories.has(callee.text);
307
+ return ts.isPropertyAccessExpression(callee) &&
308
+ ts.isIdentifier(callee.expression) &&
309
+ scope.namespaces.has(callee.expression.text) &&
310
+ callee.name.text === "createSqlClient";
311
+ };
312
+ const scopeWithClientDeclarations = (scope, declarations) => {
313
+ const nextClients = new Set(scope.clients);
314
+ let changed = false;
315
+ for (const declaration of declarations) {
316
+ if (!ts.isIdentifier(declaration.name) || !isClientFactoryCall(declaration.initializer, scope))
317
+ continue;
318
+ nextClients.add(declaration.name.text);
319
+ changed = true;
320
+ }
321
+ return changed ? { ...scope, clients: nextClients } : scope;
251
322
  };
252
323
  const visit = (node, scope) => {
253
324
  if (ts.isCallExpression(node)) {
@@ -262,7 +333,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
262
333
  if (param && ts.isIdentifier(param.name)) {
263
334
  innerSql.add(param.name.text);
264
335
  }
265
- visit(fn.body, { sqlAliases: innerSql, namespaces: shadowed.namespaces });
336
+ visit(fn.body, { ...shadowed, sqlAliases: innerSql });
266
337
  return;
267
338
  }
268
339
  }
@@ -283,7 +354,11 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
283
354
  const stmts = node.statements;
284
355
  for (const stmt of stmts) {
285
356
  if (ts.isVariableStatement(stmt)) {
286
- current = scopeWithoutBindingShadows(current, stmt.declarationList.declarations.map((d) => d.name));
357
+ const declarations = stmt.declarationList.declarations;
358
+ current = scopeWithoutBindingShadows(current, declarations.map((d) => d.name));
359
+ visit(stmt, current);
360
+ current = scopeWithClientDeclarations(current, declarations);
361
+ continue;
287
362
  }
288
363
  else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
289
364
  current = scopeWithoutBindingShadows(current, [stmt.name]);
@@ -320,9 +395,22 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
320
395
  }
321
396
  ts.forEachChild(node, (child) => visit(child, scope));
322
397
  };
323
- visit(source, { sqlAliases: importedAliases, namespaces: importedNamespaces });
398
+ visit(source, {
399
+ sqlAliases: importedAliases,
400
+ namespaces: importedNamespaces,
401
+ clientFactories: importedClientFactories,
402
+ clients: new Set(),
403
+ });
324
404
  return out;
325
405
  }
406
+ function scriptKind(path) {
407
+ switch (extname(path).toLowerCase()) {
408
+ case ".tsx": return ts.ScriptKind.TSX;
409
+ case ".mts":
410
+ case ".cts":
411
+ default: return ts.ScriptKind.TS;
412
+ }
413
+ }
326
414
  export function scanProject(root, scan = {}) {
327
415
  const files = findSourceFiles(root, scan);
328
416
  const out = [];
@@ -0,0 +1,7 @@
1
+ export declare function isIdentifierContinuation(value: string): boolean;
2
+ export declare function isEscapeStringPrefix(query: string, quote: number): boolean;
3
+ export declare function readSingleQuoted(query: string, start: number, escapeBackslash: boolean): number;
4
+ export declare function readQuotedIdentifier(query: string, start: number): number;
5
+ export declare function readDollarQuoted(query: string, start: number): number | null;
6
+ export declare function readLineComment(query: string, start: number): number;
7
+ export declare function readBlockComment(query: string, start: number): number;
@@ -0,0 +1,76 @@
1
+ export function isIdentifierContinuation(value) {
2
+ return /[A-Za-z0-9_$\u0080-\uFFFF]/.test(value);
3
+ }
4
+ export function isEscapeStringPrefix(query, quote) {
5
+ const prefix = query[quote - 1];
6
+ if (prefix !== "e" && prefix !== "E")
7
+ return false;
8
+ const before = query[quote - 2];
9
+ return before === undefined || !isIdentifierContinuation(before);
10
+ }
11
+ export function readSingleQuoted(query, start, escapeBackslash) {
12
+ let i = start + 1;
13
+ while (i < query.length) {
14
+ if (escapeBackslash && query[i] === "\\") {
15
+ i += 2;
16
+ continue;
17
+ }
18
+ if (query[i] === "'") {
19
+ if (query[i + 1] === "'") {
20
+ i += 2;
21
+ continue;
22
+ }
23
+ return i + 1;
24
+ }
25
+ i++;
26
+ }
27
+ return query.length;
28
+ }
29
+ export function readQuotedIdentifier(query, start) {
30
+ let i = start + 1;
31
+ while (i < query.length) {
32
+ if (query[i] === '"') {
33
+ if (query[i + 1] === '"') {
34
+ i += 2;
35
+ continue;
36
+ }
37
+ return i + 1;
38
+ }
39
+ i++;
40
+ }
41
+ return query.length;
42
+ }
43
+ export function readDollarQuoted(query, start) {
44
+ let end = start + 1;
45
+ if (query[end] !== "$" && !/[A-Za-z_\u0080-\uFFFF]/.test(query[end]))
46
+ return null;
47
+ while (end < query.length && /[A-Za-z0-9_\u0080-\uFFFF]/.test(query[end]))
48
+ end++;
49
+ if (query[end] !== "$")
50
+ return null;
51
+ const tag = query.slice(start, end + 1);
52
+ const close = query.indexOf(tag, end + 1);
53
+ return close === -1 ? query.length : close + tag.length;
54
+ }
55
+ export function readLineComment(query, start) {
56
+ const end = query.indexOf("\n", start + 2);
57
+ return end === -1 ? query.length : end + 1;
58
+ }
59
+ export function readBlockComment(query, start) {
60
+ let depth = 1;
61
+ let i = start + 2;
62
+ while (i < query.length && depth > 0) {
63
+ if (query[i] === "/" && query[i + 1] === "*") {
64
+ depth++;
65
+ i += 2;
66
+ continue;
67
+ }
68
+ if (query[i] === "*" && query[i + 1] === "/") {
69
+ depth--;
70
+ i += 2;
71
+ continue;
72
+ }
73
+ i++;
74
+ }
75
+ return i;
76
+ }
@@ -0,0 +1,11 @@
1
+ export type RewrittenSql = {
2
+ query: string;
3
+ names: string[];
4
+ positionMap: number[];
5
+ };
6
+ export declare function rewriteNamedParameters(query: string): RewrittenSql;
7
+ export declare function bindNamedParameters(rewritten: RewrittenSql, args: readonly unknown[]): {
8
+ query: string;
9
+ params: unknown[];
10
+ };
11
+ export declare function originalPosition(rewritten: RewrittenSql, oneBasedPosition: number): number;
@@ -0,0 +1,101 @@
1
+ import { isEscapeStringPrefix, isIdentifierContinuation, readBlockComment, readDollarQuoted, readLineComment, readQuotedIdentifier, readSingleQuoted, } from "./sql-lex.js";
2
+ export function rewriteNamedParameters(query) {
3
+ let out = "";
4
+ let i = 0;
5
+ let positional = false;
6
+ const names = [];
7
+ const indexes = new Map();
8
+ const positionMap = [];
9
+ const append = (text, sourceStart) => {
10
+ out += text;
11
+ for (let offset = 0; offset < text.length; offset++)
12
+ positionMap.push(sourceStart + offset);
13
+ };
14
+ while (i < query.length) {
15
+ const start = i;
16
+ const ch = query[i];
17
+ if (ch === "-" && query[i + 1] === "-") {
18
+ i = readLineComment(query, i);
19
+ append(query.slice(start, i), start);
20
+ continue;
21
+ }
22
+ if (ch === "/" && query[i + 1] === "*") {
23
+ i = readBlockComment(query, i);
24
+ append(query.slice(start, i), start);
25
+ continue;
26
+ }
27
+ if (ch === "'") {
28
+ i = readSingleQuoted(query, i, isEscapeStringPrefix(query, i));
29
+ append(query.slice(start, i), start);
30
+ continue;
31
+ }
32
+ if (ch === '"') {
33
+ i = readQuotedIdentifier(query, i);
34
+ append(query.slice(start, i), start);
35
+ continue;
36
+ }
37
+ if (ch === "$") {
38
+ if (i > 0 && isIdentifierContinuation(query[i - 1])) {
39
+ append(ch, i);
40
+ i++;
41
+ continue;
42
+ }
43
+ const quotedEnd = readDollarQuoted(query, i);
44
+ if (quotedEnd !== null) {
45
+ i = quotedEnd;
46
+ append(query.slice(start, i), start);
47
+ continue;
48
+ }
49
+ const positionalMatch = /^\$[1-9][0-9]*/.exec(query.slice(i));
50
+ if (positionalMatch) {
51
+ positional = true;
52
+ i += positionalMatch[0].length;
53
+ append(positionalMatch[0], start);
54
+ continue;
55
+ }
56
+ const namedMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)/.exec(query.slice(i));
57
+ if (namedMatch) {
58
+ const name = namedMatch[1];
59
+ let index = indexes.get(name);
60
+ if (index === undefined) {
61
+ names.push(name);
62
+ index = names.length;
63
+ indexes.set(name, index);
64
+ }
65
+ const replacement = `$${index}`;
66
+ append(replacement, start);
67
+ i += namedMatch[0].length;
68
+ continue;
69
+ }
70
+ }
71
+ append(ch, i);
72
+ i++;
73
+ }
74
+ if (positional && names.length > 0) {
75
+ throw new Error("sqlx-js: named and positional parameters cannot be mixed in one query");
76
+ }
77
+ positionMap.push(query.length);
78
+ return { query: out, names, positionMap };
79
+ }
80
+ export function bindNamedParameters(rewritten, args) {
81
+ if (rewritten.names.length === 0)
82
+ return { query: rewritten.query, params: [...args] };
83
+ if (args.length !== 1 || args[0] === null || typeof args[0] !== "object" || Array.isArray(args[0])) {
84
+ throw new Error("sqlx-js: a query with named parameters requires exactly one parameter object");
85
+ }
86
+ const values = args[0];
87
+ const missing = rewritten.names.filter((name) => !Object.hasOwn(values, name));
88
+ const expected = new Set(rewritten.names);
89
+ const extra = Object.keys(values).filter((name) => !expected.has(name));
90
+ if (missing.length > 0)
91
+ throw new Error(`sqlx-js: missing named parameter(s): ${missing.join(", ")}`);
92
+ if (extra.length > 0)
93
+ throw new Error(`sqlx-js: unknown named parameter(s): ${extra.join(", ")}`);
94
+ return { query: rewritten.query, params: rewritten.names.map((name) => values[name]) };
95
+ }
96
+ export function originalPosition(rewritten, oneBasedPosition) {
97
+ const position = Number(oneBasedPosition);
98
+ if (!Number.isInteger(position) || position < 1)
99
+ return oneBasedPosition;
100
+ return (rewritten.positionMap[position - 1] ?? position - 1) + 1;
101
+ }
@@ -1,6 +1,6 @@
1
1
  type ParamsOf<T> = T extends {
2
- params: infer P extends readonly unknown[];
3
- } ? P : never[];
2
+ params: infer P;
3
+ } ? P extends readonly unknown[] ? P : [P] : never[];
4
4
  type RowOf<T> = T extends {
5
5
  row: infer R;
6
6
  } ? R : never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onreza/sqlx-js",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by sqlx.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,6 +66,11 @@
66
66
  "peerDependencies": {
67
67
  "typescript": ">=5.4"
68
68
  },
69
+ "peerDependenciesMeta": {
70
+ "typescript": {
71
+ "optional": true
72
+ }
73
+ },
69
74
  "dependencies": {
70
75
  "libpg-query": "^17.7.3",
71
76
  "postgres": "^3.4.9"