@onreza/sqlx-js 0.5.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.
@@ -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;
@@ -1,7 +1,7 @@
1
1
  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
- import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./commands/migrate.js";
4
+ import { applyPending, acquireMigrateLock, releaseMigrateLock, DEFAULT_MIGRATE_LOCK_KEY } from "./migration-core.js";
5
5
  const PARAMETER_KIND = Symbol("sqlx-js.parameter");
6
6
  export function json(value) {
7
7
  return { [PARAMETER_KIND]: "json", value };
@@ -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) => {
@@ -15,5 +15,5 @@ export declare class ScanError extends Error {
15
15
  constructor(file: string, line: number, column: number, message: string);
16
16
  }
17
17
  export declare function findSourceFiles(root: string, scan?: ScanConfig): string[];
18
- export declare function scanFile(absPath: string, root: string): QueryCallSite[];
18
+ export declare function scanFile(absPath: string, root: string, modules?: readonly string[]): QueryCallSite[];
19
19
  export declare function scanProject(root: string, scan?: ScanConfig): QueryCallSite[];
@@ -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;
@@ -21,7 +21,7 @@ const DEFAULT_EXCLUDES = [
21
21
  "**/build/**",
22
22
  "**/.next/**",
23
23
  ];
24
- const SQLX_MODULES = new Set(["@onreza/sqlx-js"]);
24
+ const DEFAULT_SQLX_MODULES = ["@onreza/sqlx-js"];
25
25
  const EXT = /\.(ts|tsx|mts|cts)$/;
26
26
  const TS_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
27
27
  function formatConfigError(error) {
@@ -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")) {
@@ -131,18 +132,27 @@ function classifyCallee(callee, scope) {
131
132
  }
132
133
  return null;
133
134
  }
134
- export function scanFile(absPath, root) {
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;
142
152
  const mod = stmt.moduleSpecifier;
143
153
  if (!ts.isStringLiteral(mod))
144
154
  continue;
145
- if (!SQLX_MODULES.has(mod.text))
155
+ if (!modules.includes(mod.text))
146
156
  continue;
147
157
  const ic = stmt.importClause;
148
158
  if (!ic)
@@ -158,17 +168,19 @@ export function scanFile(absPath, root) {
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) {
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) {
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) {
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) {
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,14 +374,27 @@ export function scanFile(absPath, root) {
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 = [];
329
396
  for (const f of files) {
330
- for (const site of scanFile(f, root))
397
+ for (const site of scanFile(f, root, scan.modules ?? DEFAULT_SQLX_MODULES))
331
398
  out.push(site);
332
399
  }
333
400
  return out;
@@ -0,0 +1 @@
1
+ export declare function containsUnknownType(type: string): boolean;
@@ -0,0 +1,20 @@
1
+ import ts from "typescript";
2
+ const unknownTypeCache = new Map();
3
+ export function containsUnknownType(type) {
4
+ const cached = unknownTypeCache.get(type);
5
+ if (cached !== undefined)
6
+ return cached;
7
+ const source = ts.createSourceFile("sqlx-js-type.ts", `type SqlxJsType = ${type};`, ts.ScriptTarget.Latest, true);
8
+ let found = false;
9
+ const visit = (node) => {
10
+ if (node.kind === ts.SyntaxKind.UnknownKeyword) {
11
+ found = true;
12
+ return;
13
+ }
14
+ if (!found)
15
+ ts.forEachChild(node, visit);
16
+ };
17
+ visit(source);
18
+ unknownTypeCache.set(type, found);
19
+ return found;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onreza/sqlx-js",
3
- "version": "0.5.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",
@@ -19,7 +19,8 @@
19
19
  }
20
20
  },
21
21
  "bin": {
22
- "sqlx-js": "dist/bin/sqlx-js.js"
22
+ "sqlx-js": "dist/bin/sqlx-js.js",
23
+ "sqlx-js-diagnostics": "dist/bin/sqlx-js-diagnostics.js"
23
24
  },
24
25
  "files": [
25
26
  "dist",
@@ -54,6 +55,9 @@
54
55
  "test": "bun test tests --timeout 120000",
55
56
  "test:typecheck": "tsc --noEmit",
56
57
  "test:example": "bunx tsc -p example --noEmit",
58
+ "test:runtime-boundary": "bun run build && node scripts/check-runtime-boundary.mjs",
59
+ "test:node-package": "node scripts/node-package-smoke.mjs",
60
+ "test:corpus": "bun scripts/check-production-corpus.ts",
57
61
  "sqlx:prepare": "bun bin/sqlx-js.ts prepare",
58
62
  "sqlx:migrate": "bun bin/sqlx-js.ts migrate",
59
63
  "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json && bun scripts/fix-dist-imports.ts",