@onreza/sqlx-js 0.6.0 → 0.7.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.
@@ -49,34 +49,38 @@ export class SchemaCache {
49
49
  }
50
50
  }
51
51
  async loadTableNames(names) {
52
- const need = [];
52
+ const explicit = [];
53
+ const visible = [];
53
54
  for (const n of names) {
54
- const schema = n.schema ?? "public";
55
- const key = `${schema}.${n.name}`;
56
- if (!this.nameToOid.has(key))
57
- need.push({ schema, name: n.name });
55
+ if (n.schema) {
56
+ if (!this.nameToOid.has(`${n.schema}.${n.name}`))
57
+ explicit.push({ schema: n.schema, name: n.name });
58
+ }
59
+ else if (!this.nameToOid.has(unqualifiedKey(n.name))) {
60
+ visible.push(n.name);
61
+ }
58
62
  }
59
- if (need.length === 0)
60
- return;
61
- const where = need.map((n) => `(n.nspname = ${quote(n.schema)} AND c.relname = ${quote(n.name)})`).join(" OR ");
62
- const sql = `SELECT c.oid::int8, n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE ${where}`;
63
- const r = await this.client.simpleQueryAll(sql);
64
- for (const row of r.rows) {
65
- const oid = Number(decodeText(row[0]));
66
- const schema = decodeText(row[1]);
67
- const name = decodeText(row[2]);
68
- const k = `${schema}.${name}`;
69
- const arr = this.nameToOid.get(k) ?? [];
70
- arr.push(oid);
71
- this.nameToOid.set(k, arr);
72
- this.oidToName.set(oid, { schema, name });
63
+ if (explicit.length > 0) {
64
+ const where = explicit.map((n) => `(n.nspname = ${quote(n.schema)} AND c.relname = ${quote(n.name)})`).join(" OR ");
65
+ const sql = `SELECT c.oid::int8, n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE ${where}`;
66
+ const result = await this.client.simpleQueryAll(sql);
67
+ for (const row of result.rows)
68
+ this.recordTable(row);
69
+ }
70
+ if (visible.length > 0) {
71
+ const requested = [...new Set(visible)].map(quote).join(",");
72
+ const sql = `SELECT c.oid::int8, n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relname IN (${requested}) AND pg_table_is_visible(c.oid)`;
73
+ const result = await this.client.simpleQueryAll(sql);
74
+ for (const row of result.rows) {
75
+ this.recordTable(row, true);
76
+ }
73
77
  }
74
78
  }
75
79
  isNotNull(tableOid, attno) {
76
80
  return this.byOidNum.get(key(tableOid, attno))?.notNull;
77
81
  }
78
82
  resolveTable(schema, name) {
79
- const k = `${schema ?? "public"}.${name}`;
83
+ const k = schema ? `${schema}.${name}` : unqualifiedKey(name);
80
84
  const arr = this.nameToOid.get(k);
81
85
  return arr?.[0];
82
86
  }
@@ -89,17 +93,21 @@ export class SchemaCache {
89
93
  return;
90
94
  const sql = `SELECT c.oid::int8, n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid IN (${need.join(",")})`;
91
95
  const r = await this.client.simpleQueryAll(sql);
92
- for (const row of r.rows) {
93
- const oid = Number(decodeText(row[0]));
94
- const schema = decodeText(row[1]);
95
- const name = decodeText(row[2]);
96
- const k = `${schema}.${name}`;
97
- const arr = this.nameToOid.get(k) ?? [];
98
- if (!arr.includes(oid))
99
- arr.push(oid);
100
- this.nameToOid.set(k, arr);
101
- this.oidToName.set(oid, { schema, name });
102
- }
96
+ for (const row of r.rows)
97
+ this.recordTable(row);
98
+ }
99
+ recordTable(row, visible = false) {
100
+ const oid = Number(decodeText(row[0]));
101
+ const schema = decodeText(row[1]);
102
+ const name = decodeText(row[2]);
103
+ const key = `${schema}.${name}`;
104
+ const arr = this.nameToOid.get(key) ?? [];
105
+ if (!arr.includes(oid))
106
+ arr.push(oid);
107
+ this.nameToOid.set(key, arr);
108
+ if (visible)
109
+ this.nameToOid.set(unqualifiedKey(name), [oid]);
110
+ this.oidToName.set(oid, { schema, name });
103
111
  }
104
112
  columnNameByAttno(tableOid, attno) {
105
113
  return this.byOidNum.get(key(tableOid, attno))?.name;
@@ -269,6 +277,9 @@ export class SchemaCache {
269
277
  function key(oid, attno) {
270
278
  return `${oid}/${attno}`;
271
279
  }
280
+ function unqualifiedKey(name) {
281
+ return `\0${name}`;
282
+ }
272
283
  function quote(s) {
273
284
  return `'${s.replace(/'/g, "''")}'`;
274
285
  }
@@ -8,6 +8,7 @@ export type ConnConfig = {
8
8
  database: string;
9
9
  sslmode?: SslMode;
10
10
  applicationName?: string;
11
+ startupOptions?: string;
11
12
  connectTimeoutMs?: number;
12
13
  statementTimeoutMs?: number;
13
14
  sslRootCert?: string;
@@ -26,6 +26,9 @@ export function parseDatabaseUrl(url) {
26
26
  const appName = params.get("application_name");
27
27
  if (appName)
28
28
  cfg.applicationName = appName;
29
+ const startupOptions = params.get("options");
30
+ if (startupOptions)
31
+ cfg.startupOptions = startupOptions;
29
32
  const ct = params.get("connect_timeout");
30
33
  if (ct) {
31
34
  const n = Number(ct);
@@ -446,6 +449,9 @@ export class PgClient {
446
449
  if (this.cfg.applicationName) {
447
450
  pairs.push(cstr("application_name"), cstr(this.cfg.applicationName));
448
451
  }
452
+ if (this.cfg.startupOptions) {
453
+ pairs.push(cstr("options"), cstr(this.cfg.startupOptions));
454
+ }
449
455
  if (this.cfg.statementTimeoutMs !== undefined) {
450
456
  pairs.push(cstr("statement_timeout"), cstr(String(this.cfg.statementTimeoutMs)));
451
457
  }
@@ -1,21 +1,31 @@
1
1
  import postgres from "postgres";
2
- import { type OnQueryHook } from "./runtime.js";
2
+ import { type OnQueryHook, type OnQueryHookError } from "./runtime.js";
3
3
  export type PostgresClient = postgres.Sql<{
4
4
  bigint: bigint;
5
5
  }>;
6
6
  export type PostgresOptions = postgres.Options<Record<string, postgres.PostgresType>>;
7
7
  export type CreateClientOptions = PostgresOptions & {
8
8
  onQuery?: OnQueryHook;
9
+ onQueryHookError?: OnQueryHookError;
9
10
  statementTimeoutMs?: number;
10
11
  fileRoot?: string;
12
+ reloadSqlFiles?: boolean;
11
13
  };
12
14
  export declare function createClient(url?: string | undefined, options?: CreateClientOptions): PostgresClient;
13
15
  export declare function getClient(): PostgresClient;
14
16
  export declare function setClient(client: PostgresClient, options?: {
15
17
  onQuery?: OnQueryHook;
18
+ onQueryHookError?: OnQueryHookError;
16
19
  prepare?: boolean;
17
20
  fileRoot?: string;
21
+ reloadSqlFiles?: boolean;
18
22
  }): void;
19
23
  export declare function close(): Promise<void>;
24
+ export declare function createSqlClient(url?: string | undefined, options?: CreateClientOptions): {
25
+ client: PostgresClient;
26
+ close: () => Promise<void>;
27
+ sql: import("./runtime.js").SqlRoot;
28
+ unsafe: (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
29
+ };
20
30
  export declare const sql: import("./runtime.js").SqlRoot;
21
31
  export declare const unsafe: (query: string, ...params: unknown[]) => Promise<Record<string, unknown>[]>;
@@ -9,13 +9,17 @@ function resolvedFileRoot(value) {
9
9
  class PostgresRuntimeClient {
10
10
  client;
11
11
  onQuery;
12
+ onQueryHookError;
12
13
  prepare;
13
14
  fileRoot;
14
- constructor(client, onQuery, prepare = true, fileRoot = resolvedFileRoot()) {
15
+ reloadSqlFiles;
16
+ constructor(client, onQuery, onQueryHookError, prepare = true, fileRoot = resolvedFileRoot(), reloadSqlFiles = false) {
15
17
  this.client = client;
16
18
  this.onQuery = onQuery;
19
+ this.onQueryHookError = onQueryHookError;
17
20
  this.prepare = prepare;
18
21
  this.fileRoot = fileRoot;
22
+ this.reloadSqlFiles = reloadSqlFiles;
19
23
  }
20
24
  async query(query, params) {
21
25
  return await this.client.unsafe(query, params, { prepare: this.prepare });
@@ -39,7 +43,7 @@ class PostgresRuntimeClient {
39
43
  if (!("begin" in this.client))
40
44
  throw new Error("sqlx-js.transaction: nested transactions are not supported");
41
45
  return await this.client.begin(async (tx) => {
42
- return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.prepare, this.fileRoot));
46
+ return await fn(new PostgresRuntimeClient(tx, this.onQuery, this.onQueryHookError, this.prepare, this.fileRoot, this.reloadSqlFiles));
43
47
  });
44
48
  }
45
49
  async close() {
@@ -158,7 +162,7 @@ function installJsonArrayCodecs(client) {
158
162
  export function createClient(url = process.env.DATABASE_URL, options = {}) {
159
163
  if (!url)
160
164
  throw new Error("sqlx-js: DATABASE_URL is not set");
161
- const { onQuery, statementTimeoutMs, fileRoot, ...pgOptions } = options;
165
+ const { onQuery, onQueryHookError, statementTimeoutMs, fileRoot, reloadSqlFiles, ...pgOptions } = options;
162
166
  const connection = statementTimeoutMs !== undefined
163
167
  ? { ...(pgOptions.connection ?? {}), statement_timeout: statementTimeoutMs }
164
168
  : pgOptions.connection;
@@ -169,15 +173,17 @@ export function createClient(url = process.env.DATABASE_URL, options = {}) {
169
173
  });
170
174
  client[HOOKS] = {
171
175
  onQuery,
176
+ onQueryHookError,
172
177
  prepare: pgOptions.prepare ?? true,
173
178
  fileRoot: resolvedFileRoot(fileRoot),
179
+ reloadSqlFiles: reloadSqlFiles ?? false,
174
180
  };
175
181
  return client;
176
182
  }
177
183
  function createDefaultClient() {
178
184
  const client = createClient();
179
185
  const attached = client[HOOKS];
180
- return new PostgresRuntimeClient(client, attached.onQuery, attached.prepare, attached.fileRoot);
186
+ return new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles);
181
187
  }
182
188
  function getRuntimeClient() {
183
189
  defaultClient ??= createDefaultClient();
@@ -189,7 +195,7 @@ export function getClient() {
189
195
  export function setClient(client, options) {
190
196
  installJsonArrayCodecs(client);
191
197
  const attached = client[HOOKS];
192
- defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot));
198
+ defaultClient = new PostgresRuntimeClient(client, options?.onQuery ?? attached?.onQuery, options?.onQueryHookError ?? attached?.onQueryHookError, options?.prepare ?? attached?.prepare ?? client.options?.prepare ?? true, resolvedFileRoot(options?.fileRoot ?? attached?.fileRoot), options?.reloadSqlFiles ?? attached?.reloadSqlFiles ?? false);
193
199
  }
194
200
  export async function close() {
195
201
  if (defaultClient) {
@@ -197,6 +203,17 @@ export async function close() {
197
203
  defaultClient = null;
198
204
  }
199
205
  }
206
+ export function createSqlClient(url = process.env.DATABASE_URL, options = {}) {
207
+ const client = createClient(url, options);
208
+ const attached = client[HOOKS];
209
+ const runtimeClient = new PostgresRuntimeClient(client, attached.onQuery, attached.onQueryHookError, attached.prepare, attached.fileRoot, attached.reloadSqlFiles);
210
+ const runtime = createSqlRuntime(() => runtimeClient);
211
+ return {
212
+ ...runtime,
213
+ client,
214
+ close: async () => runtimeClient.close(),
215
+ };
216
+ }
200
217
  const runtime = createSqlRuntime(getRuntimeClient);
201
218
  export const sql = runtime.sql;
202
219
  export const unsafe = runtime.unsafe;
@@ -6,7 +6,8 @@ export type OnQueryEvent = {
6
6
  rowCount?: number;
7
7
  error?: unknown;
8
8
  };
9
- export type OnQueryHook = (event: OnQueryEvent) => void;
9
+ export type OnQueryHook = (event: OnQueryEvent) => void | Promise<void>;
10
+ export type OnQueryHookError = (error: unknown, event: OnQueryEvent) => void | Promise<void>;
10
11
  export type RuntimeQueryResult = unknown[] & {
11
12
  count?: number | null;
12
13
  command?: string | null;
@@ -17,7 +18,9 @@ export type RuntimeClient = {
17
18
  transaction: <R>(fn: (client: RuntimeClient) => Promise<R>) => Promise<R>;
18
19
  close: () => Promise<void>;
19
20
  onQuery?: OnQueryHook;
21
+ onQueryHookError?: OnQueryHookError;
20
22
  fileRoot?: string;
23
+ reloadSqlFiles?: boolean;
21
24
  };
22
25
  type AnyFn = (...args: unknown[]) => Promise<unknown[]>;
23
26
  type AnyOneFn = (...args: unknown[]) => Promise<unknown>;
@@ -71,7 +74,7 @@ export declare const _internal: {
71
74
  parameterKind: typeof parameterKind;
72
75
  toPgError: typeof toPgError;
73
76
  };
74
- declare function loadSqlFile(path: string, fileRoot?: string): string;
77
+ declare function loadSqlFile(path: string, fileRoot?: string, reload?: boolean): string;
75
78
  export declare function clearSqlFileCache(): void;
76
79
  declare function clearIdentifierCache(): void;
77
80
  export declare function id(...parts: string[]): string;
@@ -257,7 +257,7 @@ async function runRawQuery(client, query, params) {
257
257
  const start = performance.now();
258
258
  try {
259
259
  const result = await client.query(query, encoded);
260
- onQuery({
260
+ notifyQuery(client, {
261
261
  query,
262
262
  params,
263
263
  durationMs: performance.now() - start,
@@ -267,10 +267,29 @@ async function runRawQuery(client, query, params) {
267
267
  }
268
268
  catch (e) {
269
269
  const error = toPgError(e) ?? e;
270
- onQuery({ query, params, durationMs: performance.now() - start, error });
270
+ notifyQuery(client, { query, params, durationMs: performance.now() - start, error });
271
271
  throw error;
272
272
  }
273
273
  }
274
+ function notifyQuery(client, event) {
275
+ try {
276
+ const pending = client.onQuery?.(event);
277
+ if (pending)
278
+ void pending.catch((error) => notifyQueryHookError(client, error, event));
279
+ }
280
+ catch (error) {
281
+ notifyQueryHookError(client, error, event);
282
+ }
283
+ }
284
+ function notifyQueryHookError(client, error, event) {
285
+ try {
286
+ const pending = client.onQueryHookError?.(error, event);
287
+ if (pending)
288
+ void pending.catch(() => { });
289
+ }
290
+ catch {
291
+ }
292
+ }
274
293
  async function runQuery(client, query, params) {
275
294
  return renameRows(await runRawQuery(client, query, params));
276
295
  }
@@ -298,7 +317,7 @@ async function runOptional(client, query, params) {
298
317
  throw new TooManyRowsError(rows.length, "0 or 1");
299
318
  }
300
319
  const sqlFileCache = new Map();
301
- function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd()) {
320
+ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.cwd(), reload = false) {
302
321
  const root = resolve(fileRoot);
303
322
  if (isAbsolute(path)) {
304
323
  throw new Error(`sqlx-js.sql.file: path must be relative to fileRoot: ${path}`);
@@ -309,8 +328,10 @@ function loadSqlFile(path, fileRoot = process.env.SQLX_JS_FILE_ROOT ?? process.c
309
328
  throw new Error(`sqlx-js.sql.file: path escapes fileRoot: ${path}`);
310
329
  }
311
330
  try {
312
- const st = statSync(full);
313
331
  const cached = sqlFileCache.get(full);
332
+ if (cached && !reload)
333
+ return cached.content;
334
+ const st = statSync(full);
314
335
  if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
315
336
  return cached.content;
316
337
  }
@@ -442,16 +463,16 @@ function makeBoundCallable(client) {
442
463
  return runQuery(client, query, params);
443
464
  });
444
465
  const file = (async (path, ...params) => {
445
- return runQuery(client, loadSqlFile(path, client.fileRoot), params);
466
+ return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
446
467
  });
447
468
  file.one = (async (path, ...params) => {
448
- return runOne(client, loadSqlFile(path, client.fileRoot), params);
469
+ return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
449
470
  });
450
471
  file.optional = (async (path, ...params) => {
451
- return runOptional(client, loadSqlFile(path, client.fileRoot), params);
472
+ return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
452
473
  });
453
474
  file.execute = (async (path, ...params) => {
454
- return runExecute(client, loadSqlFile(path, client.fileRoot), params);
475
+ return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
455
476
  });
456
477
  fn.file = file;
457
478
  fn.one = (async (query, ...params) => {
@@ -486,19 +507,19 @@ export function createSqlRuntime(getClient) {
486
507
  });
487
508
  const rootFile = (async (path, ...params) => {
488
509
  const client = getClient();
489
- return runQuery(client, loadSqlFile(path, client.fileRoot), params);
510
+ return runQuery(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
490
511
  });
491
512
  rootFile.one = (async (path, ...params) => {
492
513
  const client = getClient();
493
- return runOne(client, loadSqlFile(path, client.fileRoot), params);
514
+ return runOne(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
494
515
  });
495
516
  rootFile.optional = (async (path, ...params) => {
496
517
  const client = getClient();
497
- return runOptional(client, loadSqlFile(path, client.fileRoot), params);
518
+ return runOptional(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
498
519
  });
499
520
  rootFile.execute = (async (path, ...params) => {
500
521
  const client = getClient();
501
- return runExecute(client, loadSqlFile(path, client.fileRoot), params);
522
+ return runExecute(client, loadSqlFile(path, client.fileRoot, client.reloadSqlFiles), params);
502
523
  });
503
524
  root.file = rootFile;
504
525
  root.one = (async (query, ...params) => {
@@ -1,6 +1,6 @@
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
4
  export class ScanError extends Error {
5
5
  file;
6
6
  line;
@@ -76,7 +76,7 @@ function classifyCallee(callee, scope) {
76
76
  const methodName = callee.name.text;
77
77
  if (ts.isIdentifier(callee.expression)) {
78
78
  const id = callee.expression.text;
79
- if (scope.namespaces.has(id)) {
79
+ if (scope.namespaces.has(id) || scope.clients.has(id)) {
80
80
  if (methodName === "sql")
81
81
  return { kind: "inline" };
82
82
  return null;
@@ -95,8 +95,9 @@ function classifyCallee(callee, scope) {
95
95
  const mid = callee.expression;
96
96
  if (!ts.isIdentifier(mid.name))
97
97
  return null;
98
- // ns.sql.X(...) chains
99
- if (ts.isIdentifier(mid.expression) && scope.namespaces.has(mid.expression.text)) {
98
+ // ns.sql.X(...) and client.sql.X(...) chains
99
+ if (ts.isIdentifier(mid.expression) &&
100
+ (scope.namespaces.has(mid.expression.text) || scope.clients.has(mid.expression.text))) {
100
101
  if (mid.name.text !== "sql")
101
102
  return null;
102
103
  if (methodName === "one" || methodName === "optional" || methodName === "execute")
@@ -118,11 +119,11 @@ function classifyCallee(callee, scope) {
118
119
  return { kind: "file" };
119
120
  return null;
120
121
  }
121
- // ns.sql.file.X(...) chains
122
+ // ns.sql.file.X(...) and client.sql.file.X(...) chains
122
123
  if (ts.isPropertyAccessExpression(mid.expression) &&
123
124
  ts.isIdentifier(mid.expression.expression) &&
124
125
  ts.isIdentifier(mid.expression.name) &&
125
- scope.namespaces.has(mid.expression.expression.text) &&
126
+ (scope.namespaces.has(mid.expression.expression.text) || scope.clients.has(mid.expression.expression.text)) &&
126
127
  mid.expression.name.text === "sql" &&
127
128
  mid.name.text === "file" &&
128
129
  (methodName === "one" || methodName === "optional" || methodName === "execute")) {
@@ -133,9 +134,18 @@ function classifyCallee(callee, scope) {
133
134
  }
134
135
  export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
135
136
  const text = readFileSync(absPath, "utf8");
136
- const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TSX);
137
+ const source = ts.createSourceFile(absPath, text, ts.ScriptTarget.ESNext, false, scriptKind(absPath));
138
+ const parseDiagnostics = source.parseDiagnostics ?? [];
139
+ const parseError = parseDiagnostics.find((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
140
+ if (parseError) {
141
+ const start = parseError.start ?? 0;
142
+ const { line, character } = source.getLineAndCharacterOfPosition(start);
143
+ const file = relative(root, absPath).replace(/\\/g, "/");
144
+ throw new ScanError(file, line + 1, character + 1, ts.flattenDiagnosticMessageText(parseError.messageText, "\n"));
145
+ }
137
146
  const importedAliases = new Set();
138
147
  const importedNamespaces = new Set();
148
+ const importedClientFactories = new Set();
139
149
  for (const stmt of source.statements) {
140
150
  if (!ts.isImportDeclaration(stmt))
141
151
  continue;
@@ -158,17 +168,19 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
158
168
  const orig = (elem.propertyName ?? elem.name).text;
159
169
  if (orig === "sql")
160
170
  importedAliases.add(elem.name.text);
171
+ if (orig === "createSqlClient")
172
+ importedClientFactories.add(elem.name.text);
161
173
  }
162
174
  }
163
175
  }
164
- if (importedAliases.size === 0 && importedNamespaces.size === 0)
176
+ if (importedAliases.size === 0 && importedNamespaces.size === 0 && importedClientFactories.size === 0)
165
177
  return [];
166
178
  const out = [];
167
179
  const here = (node) => {
168
180
  const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
169
181
  return { line: line + 1, column: character + 1 };
170
182
  };
171
- const fileRel = relative(root, absPath);
183
+ const fileRel = relative(root, absPath).replace(/\\/g, "/");
172
184
  const recordInline = (first, args) => {
173
185
  if (!ts.isStringLiteralLike(first)) {
174
186
  const pos = here(first);
@@ -233,6 +245,8 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
233
245
  let changed = false;
234
246
  const nextSql = new Set(scope.sqlAliases);
235
247
  const nextNs = new Set(scope.namespaces);
248
+ const nextFactories = new Set(scope.clientFactories);
249
+ const nextClients = new Set(scope.clients);
236
250
  for (const binding of bindings) {
237
251
  for (const a of scope.sqlAliases) {
238
252
  if (bindingDeclares(binding, a)) {
@@ -246,8 +260,44 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
246
260
  changed = true;
247
261
  }
248
262
  }
263
+ for (const a of scope.clientFactories) {
264
+ if (bindingDeclares(binding, a)) {
265
+ nextFactories.delete(a);
266
+ changed = true;
267
+ }
268
+ }
269
+ for (const a of scope.clients) {
270
+ if (bindingDeclares(binding, a)) {
271
+ nextClients.delete(a);
272
+ changed = true;
273
+ }
274
+ }
275
+ }
276
+ return changed
277
+ ? { sqlAliases: nextSql, namespaces: nextNs, clientFactories: nextFactories, clients: nextClients }
278
+ : scope;
279
+ };
280
+ const isClientFactoryCall = (initializer, scope) => {
281
+ if (!initializer || !ts.isCallExpression(initializer))
282
+ return false;
283
+ const callee = initializer.expression;
284
+ if (ts.isIdentifier(callee))
285
+ return scope.clientFactories.has(callee.text);
286
+ return ts.isPropertyAccessExpression(callee) &&
287
+ ts.isIdentifier(callee.expression) &&
288
+ scope.namespaces.has(callee.expression.text) &&
289
+ callee.name.text === "createSqlClient";
290
+ };
291
+ const scopeWithClientDeclarations = (scope, declarations) => {
292
+ const nextClients = new Set(scope.clients);
293
+ let changed = false;
294
+ for (const declaration of declarations) {
295
+ if (!ts.isIdentifier(declaration.name) || !isClientFactoryCall(declaration.initializer, scope))
296
+ continue;
297
+ nextClients.add(declaration.name.text);
298
+ changed = true;
249
299
  }
250
- return changed ? { sqlAliases: nextSql, namespaces: nextNs } : scope;
300
+ return changed ? { ...scope, clients: nextClients } : scope;
251
301
  };
252
302
  const visit = (node, scope) => {
253
303
  if (ts.isCallExpression(node)) {
@@ -262,7 +312,7 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
262
312
  if (param && ts.isIdentifier(param.name)) {
263
313
  innerSql.add(param.name.text);
264
314
  }
265
- visit(fn.body, { sqlAliases: innerSql, namespaces: shadowed.namespaces });
315
+ visit(fn.body, { ...shadowed, sqlAliases: innerSql });
266
316
  return;
267
317
  }
268
318
  }
@@ -283,7 +333,11 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
283
333
  const stmts = node.statements;
284
334
  for (const stmt of stmts) {
285
335
  if (ts.isVariableStatement(stmt)) {
286
- current = scopeWithoutBindingShadows(current, stmt.declarationList.declarations.map((d) => d.name));
336
+ const declarations = stmt.declarationList.declarations;
337
+ current = scopeWithoutBindingShadows(current, declarations.map((d) => d.name));
338
+ visit(stmt, current);
339
+ current = scopeWithClientDeclarations(current, declarations);
340
+ continue;
287
341
  }
288
342
  else if (ts.isFunctionDeclaration(stmt) && stmt.name) {
289
343
  current = scopeWithoutBindingShadows(current, [stmt.name]);
@@ -320,9 +374,22 @@ export function scanFile(absPath, root, modules = DEFAULT_SQLX_MODULES) {
320
374
  }
321
375
  ts.forEachChild(node, (child) => visit(child, scope));
322
376
  };
323
- visit(source, { sqlAliases: importedAliases, namespaces: importedNamespaces });
377
+ visit(source, {
378
+ sqlAliases: importedAliases,
379
+ namespaces: importedNamespaces,
380
+ clientFactories: importedClientFactories,
381
+ clients: new Set(),
382
+ });
324
383
  return out;
325
384
  }
385
+ function scriptKind(path) {
386
+ switch (extname(path).toLowerCase()) {
387
+ case ".tsx": return ts.ScriptKind.TSX;
388
+ case ".mts":
389
+ case ".cts":
390
+ default: return ts.ScriptKind.TS;
391
+ }
392
+ }
326
393
  export function scanProject(root, scan = {}) {
327
394
  const files = findSourceFiles(root, scan);
328
395
  const out = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onreza/sqlx-js",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Compile-time-checked raw SQL for TypeScript + PostgreSQL. Inspired by sqlx.",
5
5
  "license": "MIT",
6
6
  "type": "module",