tina4-nodejs 3.13.94 → 3.13.96

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 (123) hide show
  1. package/CLAUDE.md +158 -30
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/packages/cli/dist/bin.js +30911 -28444
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +30810 -28261
  8. package/packages/core/public/css/tina4.min.css +1 -1
  9. package/packages/core/src/ai.ts +7 -1
  10. package/packages/core/src/auth.ts +191 -39
  11. package/packages/core/src/background.ts +19 -19
  12. package/packages/core/src/cache.ts +492 -49
  13. package/packages/core/src/devAdmin.ts +79 -32
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +6 -7
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +294 -106
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +34 -16
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +886 -421
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  34. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  35. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
  36. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  37. package/packages/core/src/sessionHandlers/respClient.ts +16 -147
  38. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  39. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  40. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  41. package/packages/core/src/testClient.ts +18 -5
  42. package/packages/core/src/trustedProxy.ts +249 -0
  43. package/packages/core/src/types.ts +29 -5
  44. package/packages/core/src/websocket.ts +66 -0
  45. package/packages/orm/dist/index.js +22717 -20168
  46. package/packages/orm/src/adapters/firebird.ts +183 -56
  47. package/packages/orm/src/adapters/mongodb.ts +25 -4
  48. package/packages/orm/src/adapters/mssql.ts +114 -29
  49. package/packages/orm/src/adapters/mysql.ts +103 -40
  50. package/packages/orm/src/adapters/odbc.ts +44 -21
  51. package/packages/orm/src/adapters/postgres.ts +118 -26
  52. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  53. package/packages/orm/src/adapters/sqlite.ts +60 -24
  54. package/packages/orm/src/autoCrud.ts +12 -10
  55. package/packages/orm/src/baseModel.ts +135 -40
  56. package/packages/orm/src/cachedDatabase.ts +43 -19
  57. package/packages/orm/src/connectTimeout.ts +265 -0
  58. package/packages/orm/src/database.ts +241 -197
  59. package/packages/orm/src/databaseResult.ts +51 -28
  60. package/packages/orm/src/databaseUrl.ts +484 -0
  61. package/packages/orm/src/docstore.ts +386 -145
  62. package/packages/orm/src/index.ts +13 -6
  63. package/packages/orm/src/migration.ts +44 -11
  64. package/packages/orm/src/model.ts +4 -0
  65. package/packages/orm/src/queryBuilder.ts +47 -6
  66. package/packages/orm/src/sqlTranslator.ts +310 -4
  67. package/packages/orm/src/types.ts +21 -77
  68. package/packages/swagger/dist/index.js +78 -20
  69. package/packages/swagger/src/generator.ts +172 -29
  70. package/types/core/src/ai.d.ts +1 -1
  71. package/types/core/src/auth.d.ts +28 -5
  72. package/types/core/src/background.d.ts +3 -3
  73. package/types/core/src/cache.d.ts +15 -12
  74. package/types/core/src/dispatchPipeline.d.ts +117 -0
  75. package/types/core/src/dotenv.d.ts +38 -16
  76. package/types/core/src/index.d.ts +6 -9
  77. package/types/core/src/logger.d.ts +93 -16
  78. package/types/core/src/messenger.d.ts +47 -6
  79. package/types/core/src/metrics.d.ts +25 -61
  80. package/types/core/src/middleware.d.ts +134 -11
  81. package/types/core/src/queue.d.ts +54 -5
  82. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
  83. package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
  84. package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
  85. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
  86. package/types/core/src/router.d.ts +14 -3
  87. package/types/core/src/server.d.ts +15 -4
  88. package/types/core/src/session.d.ts +87 -2
  89. package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
  90. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  91. package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
  92. package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
  93. package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
  94. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  95. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  96. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  97. package/types/core/src/trustedProxy.d.ts +44 -0
  98. package/types/core/src/types.d.ts +28 -5
  99. package/types/core/src/websocket.d.ts +26 -0
  100. package/types/orm/src/adapters/firebird.d.ts +55 -10
  101. package/types/orm/src/adapters/mongodb.d.ts +2 -2
  102. package/types/orm/src/adapters/mssql.d.ts +18 -11
  103. package/types/orm/src/adapters/mysql.d.ts +11 -10
  104. package/types/orm/src/adapters/odbc.d.ts +9 -12
  105. package/types/orm/src/adapters/postgres.d.ts +11 -10
  106. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  107. package/types/orm/src/adapters/sqlite.d.ts +15 -3
  108. package/types/orm/src/baseModel.d.ts +45 -9
  109. package/types/orm/src/cachedDatabase.d.ts +18 -5
  110. package/types/orm/src/connectTimeout.d.ts +100 -0
  111. package/types/orm/src/database.d.ts +78 -28
  112. package/types/orm/src/databaseResult.d.ts +29 -15
  113. package/types/orm/src/databaseUrl.d.ts +125 -0
  114. package/types/orm/src/docstore.d.ts +102 -43
  115. package/types/orm/src/index.d.ts +6 -4
  116. package/types/orm/src/migration.d.ts +4 -3
  117. package/types/orm/src/queryBuilder.d.ts +23 -3
  118. package/types/orm/src/sqlTranslator.d.ts +126 -2
  119. package/types/orm/src/types.d.ts +21 -38
  120. package/packages/core/src/scss.ts +0 -623
  121. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
  122. package/types/core/src/scss.d.ts +0 -19
  123. package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
@@ -1,5 +1,6 @@
1
1
  import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js";
2
2
  import { DatabaseResult } from "./databaseResult.js";
3
+ import { DatabaseUrl } from "./databaseUrl.js";
3
4
  import { type CachedAdapterOptions } from "./cachedDatabase.js";
4
5
  /**
5
6
  * v3.13.12 — strip trailing `;` and whitespace from user-supplied SQL
@@ -37,6 +38,40 @@ export declare function adapterTableExists(adapter: DatabaseAdapter, name: strin
37
38
  export declare function adapterTables(adapter: DatabaseAdapter): Promise<string[]>;
38
39
  export declare function adapterColumns(adapter: DatabaseAdapter, table: string): Promise<ColumnInfo[]>;
39
40
  export declare function adapterCreateTable(adapter: DatabaseAdapter, name: string, columns: Record<string, FieldDefinition>): Promise<void>;
41
+ /**
42
+ * The true row count for `sql`, ignoring the pagination the caller applied.
43
+ *
44
+ * `count` on a DatabaseResult is the TRUE TOTAL for the filter, not the number
45
+ * of rows the page returned. Node and Ruby used to populate it with
46
+ * `records.length` while Python and PHP populated it from a probe, so
47
+ * `db.fetch(sql).count` answered 20 here and 250 there for one query against one
48
+ * table, and every `toPaginate()` envelope built on it under-reported (ADR-0043).
49
+ * MEASURED 2026-08-05 on a 250-row table read with limit=20: Node reported total
50
+ * 20 over 2 pages against Python's 250 over 13.
51
+ *
52
+ * This is the single source of truth for that probe. Both read paths that build a
53
+ * DatabaseResult — `Database.fetch()` and `QueryBuilder.get()` — call it, so the
54
+ * two can never drift (QueryBuilder.get used to leave `count` at rows-returned,
55
+ * diverging from db.fetch AND from Python/Ruby, whose get() routes through fetch).
56
+ *
57
+ * Only probed when a limit was actually applied. With no limit the rows returned
58
+ * ARE the whole answer for this SQL, so `records.length` is already the true total
59
+ * and a second round-trip would buy nothing — which is also what keeps
60
+ * `fetchAll()` at one query.
61
+ *
62
+ * BEST EFFORT, and it can never mask a real failure: it runs AFTER the main query
63
+ * (which has already thrown on bad SQL) and returns `undefined` on any error.
64
+ * `undefined` — not 0 — is the miss value, so DatabaseResult falls back to
65
+ * records.length, a true lower bound. Reporting 0 next to 100 real records would
66
+ * be the same "states a wrong number authoritatively" defect this exists to remove.
67
+ *
68
+ * The closing paren goes on its OWN LINE: appended inline, a trailing
69
+ * `-- comment` in the caller's SQL comments it out and the probe dies with
70
+ * "incomplete input". Postgres, MySQL and MSSQL additionally require a name for
71
+ * the derived table; SQLite and Firebird do not, and Firebird rejects `AS` there —
72
+ * so the alias comes from the adapter, not an assumption.
73
+ */
74
+ export declare function probeTotal(adapter: DatabaseAdapter, sql: string, params: unknown[] | undefined, limit: number | undefined): Promise<number | undefined>;
40
75
  /**
41
76
  * Extract the engine-assigned auto-increment id from an `execute()` result.
42
77
  *
@@ -46,6 +81,15 @@ export declare function adapterCreateTable(adapter: DatabaseAdapter, name: strin
46
81
  * so callers fall back to `adapter.lastInsertId()` when the result has neither.
47
82
  */
48
83
  export declare function extractLastInsertId(result: unknown): number | bigint | null;
84
+ /**
85
+ * The default row cap on every read path that advertises a `limit`.
86
+ *
87
+ * One number for the whole family (Python, PHP, Ruby and Node all default to
88
+ * this). Pagination is a default principle: an un-paginated read of a table
89
+ * that grew to a million rows is a production incident waiting to happen. A
90
+ * caller who wants more passes a bigger limit.
91
+ */
92
+ export declare const DEFAULT_ROW_CAP = 100;
49
93
  /**
50
94
  * Wrap a raw adapter with the query cache so BOTH `db.fetch()` (via the
51
95
  * Database wrapper) AND ORM reads (via `getAdapter()` / `getNamedAdapter()`)
@@ -137,34 +181,19 @@ export interface DatabaseConfig {
137
181
  /**
138
182
  * Parsed result from a TINA4_DATABASE_URL connection string.
139
183
  */
140
- export interface ParsedDatabaseUrl {
141
- type: "sqlite" | "postgres" | "mysql" | "mssql" | "firebird" | "mongodb" | "odbc";
142
- path?: string;
143
- host?: string;
144
- port?: number;
145
- user?: string;
146
- password?: string;
147
- database?: string;
148
- /** ODBC-specific: raw connection string passed to odbc.connect() */
149
- connectionString?: string;
150
- }
151
184
  /**
152
- * Parse a TINA4_DATABASE_URL connection string into its components.
153
- *
154
- * Supported formats:
155
- * sqlite:///path/to/db.sqlite
156
- * sqlite://./relative/path.db
157
- * postgresql://user:pass@host:port/dbname
158
- * postgres://user:pass@host:port/dbname
159
- * mysql://user:pass@host:port/dbname
160
- *
161
- * @param url - The connection URL string.
162
- * @param username - Optional username to merge when the URL has no credentials.
163
- * @param password - Optional password to merge when the URL has no credentials.
164
- * @returns Parsed database configuration.
165
- * @throws Error if the URL scheme is not supported.
185
+ * Parse a connection URL into a `DatabaseUrl` value.
186
+ *
187
+ * Breaking (feature 5): this returned a `ParsedDatabaseUrl` struct whose fields
188
+ * were `type`, `user` and `path`. It now returns a `DatabaseUrl`, whose fields
189
+ * are `engine`, `username` and `database` - the same names PHP, Python and Ruby
190
+ * use, and the same names as the TINA4_DATABASE_USERNAME env var they come from.
191
+ * `ParsedDatabaseUrl` is gone rather than kept as an alias.
192
+ *
193
+ * The 43-CC body that used to live here - the worst function measured anywhere
194
+ * in the audit - is now one small parser per engine inside the value type.
166
195
  */
167
- export declare function parseDatabaseUrl(url: string, username?: string, password?: string): ParsedDatabaseUrl;
196
+ export declare function parseDatabaseUrl(url: string, username?: string, password?: string): DatabaseUrl;
168
197
  /**
169
198
  * A wrapper class around a DatabaseAdapter that provides a clean, high-level API.
170
199
  *
@@ -295,9 +324,30 @@ export declare class Database {
295
324
  * the fallback resolves instantly). This is the breaking change that makes
296
325
  * the wrapper work uniformly across every engine.
297
326
  */
327
+ /**
328
+ * Fetch rows with pagination, capped at DEFAULT_ROW_CAP (100) when the
329
+ * caller does not pass a limit.
330
+ *
331
+ * The cap is the one row-cap number the whole family shares (Python, PHP and
332
+ * Ruby all default `fetch` to 100). Node was the outlier: `limit` was
333
+ * optional with NO default, so a bare `db.fetch("select * from big_table")`
334
+ * returned every row.
335
+ *
336
+ * `fetchAll` deliberately does NOT inherit the cap — see below.
337
+ */
298
338
  fetch(sql: string, params?: unknown[], limit?: number, offset?: number, opts?: {
299
339
  noCache?: boolean;
300
340
  }): Promise<DatabaseResult>;
341
+ /**
342
+ * The shared read body. `limit` is passed through VERBATIM: `undefined`
343
+ * means "no LIMIT clause at all", which is how `fetchAll` stays uncapped.
344
+ *
345
+ * This exists because Node's adapters treat `limit: 0` as `LIMIT 0` (zero
346
+ * rows), not as the "no truncation" sentinel Python and PHP use — so the cap
347
+ * cannot live on the parameter default, or `fetchAll()` would silently
348
+ * inherit it and stop returning every row.
349
+ */
350
+ private _fetchWithLimit;
301
351
  /**
302
352
  * Fetch a single row or null.
303
353
  *
@@ -370,9 +420,9 @@ export declare class Database {
370
420
  * the WHERE clause. With neither a filter nor a primary key in `data` this
371
421
  * throws rather than silently changing nothing (audit feature 4, P1).
372
422
  */
373
- update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>, params?: unknown[]): Promise<DatabaseWriteResult>;
423
+ update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseWriteResult>;
374
424
  /** Delete rows. A filterless delete throws; use truncate() to empty a table. */
375
- delete(table: string, filter?: Record<string, unknown>, params?: unknown[]): Promise<DatabaseWriteResult>;
425
+ delete(table: string, filter?: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): Promise<DatabaseWriteResult>;
376
426
  /** Remove every row. The explicit spelling of a whole-table delete. */
377
427
  truncate(table: string): Promise<DatabaseWriteResult>;
378
428
  /** Close all database connections (pool or single). */
@@ -31,29 +31,43 @@ export declare class DatabaseResult implements Iterable<Record<string, unknown>>
31
31
  toCsv(): string;
32
32
  /** Same as records — plain array of row objects. */
33
33
  toArray(): Record<string, unknown>[];
34
- /** Pagination envelope — accepts either (page, perPage) or (offset, limit) style.
34
+ /**
35
+ * Describe the page this result IS — the canonical pagination envelope.
36
+ *
37
+ * Takes NO arguments and derives every field from the query that produced this
38
+ * result (ADR-0043). Passing ANY argument RAISES: a DatabaseResult holds no
39
+ * connection, so an argument could only re-slice the rows already in memory and
40
+ * then report total_pages for pages it can never reach. To read page N, FETCH
41
+ * page N (limit + offset) and call this with no arguments.
42
+ *
43
+ * The envelope is EXACTLY seven snake_case keys, identical across all four
44
+ * frameworks: `records, total, page, per_page, total_pages, limit, offset`.
45
+ *
46
+ * per_page = the query's limit
47
+ * page = floor(offset / limit) + 1
48
+ * total = the TRUE total for the filter — Database.fetch (and
49
+ * QueryBuilder.get) run a COUNT probe whenever a limit was
50
+ * applied — NEVER the number of rows returned
51
+ * total_pages = ceil(total / per_page)
52
+ * records = the rows the query returned, VERBATIM (never re-sliced)
53
+ * limit = the SQL limit actually applied
54
+ * offset = the SQL offset actually applied
35
55
  *
36
- * When called with two arguments both >= 0 and the first >= the second
37
- * (i.e. offset-style), pass `{ offset, limit }` as the first argument.
38
- * The simplest way is to always use the default (page, perPage) form and
39
- * let the autoCRUD layer supply offset/limit from the query string.
56
+ * The JSON payload is snake_case even though the method name is camelCase a
57
+ * JSON key is data, not a language surface (ADR-0043). The old duplicate and
58
+ * camelCase keys (`data`, `count`, `perPage`, `totalPages`, `has_next`,
59
+ * `has_prev`) are removed: Node emitted 13 keys, the worst offender of the four.
40
60
  *
41
- * Returns a superset of keys for backwards-compatibility across all clients.
61
+ * @throws {TypeError} if called with any argument.
42
62
  */
43
- toPaginate(page?: number, perPage?: number): {
63
+ toPaginate(): {
44
64
  records: Record<string, unknown>[];
45
- data: Record<string, unknown>[];
46
- count: number;
47
65
  total: number;
48
- limit: number;
49
- offset: number;
50
66
  page: number;
51
67
  per_page: number;
52
- perPage: number;
53
- totalPages: number;
54
68
  total_pages: number;
55
- has_next: boolean;
56
- has_prev: boolean;
69
+ limit: number;
70
+ offset: number;
57
71
  };
58
72
  /** Iterable — for (const row of result) */
59
73
  [Symbol.iterator](): Iterator<Record<string, unknown>>;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * A parsed database connection URL, as a VALUE.
3
+ *
4
+ * Feature 5 of the feature audit. This used to be `parseDatabaseUrl()`, a single
5
+ * function with a cyclomatic complexity of 43 - the worst function measured
6
+ * anywhere in the audit - whose entire job is string-to-struct. It is now one
7
+ * small parser per engine, each well under the threshold, behind a value type
8
+ * with the same surface as PHP's `DatabaseUrl` (the reference for this row).
9
+ *
10
+ * Core Principle 6 says a connection string must mean literally the same thing
11
+ * in every framework. `test/fixtures/database_url_corpus.json` is the answer
12
+ * key, byte-identical in all four.
13
+ */
14
+ import { inspect } from "node:util";
15
+ /** The canonical engine names. Aliases resolve to these ONCE, at parse. */
16
+ export type DatabaseEngine = "sqlite" | "postgres" | "mysql" | "mssql" | "firebird" | "mongodb" | "odbc";
17
+ /**
18
+ * Remove every credential from an arbitrary connection string.
19
+ *
20
+ * THE single redaction primitive. It works on a RAW string - valid or
21
+ * malformed, a URL or an ODBC DSN - so the error paths can use it too, and it
22
+ * is what `toSafeString()` calls for the odbc form rather than hand-rolling a
23
+ * second, weaker rule.
24
+ *
25
+ * It cannot be complete on a string with no recognisable credential structure
26
+ * (`notaurl-with-hunter2` has nothing to key off), which is exactly why the
27
+ * invalid-URL error reports the scheme and host instead of any form of the
28
+ * input. Redaction is for strings we can parse enough to redact.
29
+ */
30
+ export declare function redactCredentials(raw: string): string;
31
+ /**
32
+ * DISPLAY REDACTS, FIDELITY DOES NOT. JSON.stringify, util.inspect, String() and
33
+ * toSafeString() replace the password with the redaction marker, so a log line, a
34
+ * stack or a status payload is safe. structuredClone deliberately does not: its
35
+ * contract is a faithful structural copy, and a masked clone would produce an
36
+ * object whose password is the literal "***".
37
+ *
38
+ * The consequence: DO NOT PERSIST THIS OBJECT. A DatabaseUrl structured-cloned
39
+ * onto a worker thread, into a cache or into a queue payload carries a cleartext
40
+ * credential across that boundary. Use toSafeString() instead.
41
+ * test/databaseUrlRedaction.test.ts fails the build if framework code ever does.
42
+ */
43
+ export declare class DatabaseUrl {
44
+ readonly engine: DatabaseEngine;
45
+ /** Null for sqlite and odbc - a file or a DSN string has no host. */
46
+ readonly host: string | null;
47
+ /** Null for sqlite and odbc. Otherwise always set: the engine default applies. */
48
+ readonly port: number | null;
49
+ readonly database: string;
50
+ /** Null when absent, never an empty string - absent and blank differ. */
51
+ readonly username: string | null;
52
+ readonly password: string | null;
53
+ /** ODBC only: the raw connection string handed to odbc.connect(). */
54
+ readonly connectionString: string | null;
55
+ constructor(url: string, username?: string, password?: string);
56
+ static fromEnv(key?: string): DatabaseUrl | null;
57
+ /**
58
+ * Connection target for the adapter. sqlite and odbc are the whole value.
59
+ *
60
+ * NOT SAFE TO LOG. For every network engine this is credential-free
61
+ * (host:port/database), which makes it look loggable - but the odbc branch
62
+ * returns the connection string VERBATIM, `PWD=` included, because that is
63
+ * what the driver has to receive. Log `toSafeString()`; never this.
64
+ */
65
+ dsn(): string;
66
+ /**
67
+ * The URL with the password replaced by ***.
68
+ *
69
+ * The ONLY form allowed in a log line or an error message: a connection URL in
70
+ * a log is a credential leak. Node had no such method at all before this,
71
+ * which meant every call site that wanted to log a connection target had to
72
+ * redact it by hand. It round-trips, so it stays readable as well as safe.
73
+ */
74
+ toSafeString(): string;
75
+ /**
76
+ * What `JSON.stringify(url)` emits.
77
+ *
78
+ * Without it, stringifying the value - directly, or as one field of a config
79
+ * object being logged - emitted `"password":"<the real password>"`, measured
80
+ * on this class. Python guards the same exposure with `__repr__` and Ruby
81
+ * with `#inspect`; JSON is the shape Node actually serialises into a log
82
+ * line, so it needs the guard too.
83
+ *
84
+ * Structure is preserved so the dump is still worth having: only the secret
85
+ * is masked. `null` stays `null` - an ABSENT password and a masked one are
86
+ * different facts, and flattening them would hide exactly the confusion C7
87
+ * is about.
88
+ */
89
+ toJSON(): Record<string, unknown>;
90
+ /**
91
+ * What `console.log(url)` / `util.inspect(url)` print.
92
+ *
93
+ * Node's equivalent of Python's `__repr__` and Ruby's `#inspect`, and the
94
+ * same rendering they produce - `DatabaseUrl('postgres://user:***@h:5432/db')`
95
+ * (tina4-python/tina4_python/database/database_url.py:153). Without it,
96
+ * `console.log(url)` printed the default field dump, password included.
97
+ */
98
+ [inspect.custom](): string;
99
+ private static parse;
100
+ /**
101
+ * sqlite is parsed on the RAW string. The URL class collapses `sqlite:/x` and
102
+ * `sqlite:///x`, losing the difference between a one-slash ABSOLUTE path and
103
+ * the documented three-slash RELATIVE form.
104
+ *
105
+ * sqlite:///app.db -> app.db (three slashes = relative to cwd)
106
+ * sqlite:////abs/app.db -> /abs/app.db (four slashes = absolute)
107
+ * sqlite:/abs/app.db -> /abs/app.db (one slash = a real absolute path)
108
+ * sqlite:app.db -> app.db
109
+ */
110
+ private static parseSqlite;
111
+ /**
112
+ * mssql and firebird: the URL class does not know these schemes, so they are
113
+ * matched directly.
114
+ *
115
+ * The captured path keeps its own leading slash when the URL had two, which is
116
+ * how the documented absolute Firebird form survives. The old code did
117
+ * `"/" + match[5]`, ADDING a slash - so an absolute path came back with two
118
+ * and a relative path was silently made absolute. Verified against live
119
+ * Firebird 5.0.4: the driver takes one or two leading slashes and rejects a
120
+ * relative path outright.
121
+ */
122
+ private static parseRegexForm;
123
+ /** postgres / mysql / mongodb, via the URL class. */
124
+ private static parseStandard;
125
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * import { getCollection, ObjectId } from "@tina4/orm";
8
8
  *
9
- * const orders = getCollection("orders"); // SqliteCollection when no Mongo configured
9
+ * const orders = await getCollection("orders"); // SqliteCollection when no Mongo configured
10
10
  * const { insertedId } = await orders.insertOne({ customer_id: 1, total: 9.99 });
11
11
  * for (const o of await orders.find({ customer_id: { $in: [1, 2] } }).sort("created_at", -1).limit(10).toArray()) {
12
12
  * // ...
@@ -21,6 +21,11 @@
21
21
  * production runs serverless in local dev with no code change - only the backend
22
22
  * differs.
23
23
  *
24
+ * A configured URI with NO driver installed throws `DocStoreDriverMissing`
25
+ * (ADR-0033). It does NOT quietly use the local SQLite store, and it no longer
26
+ * surfaces a bare ERR_MODULE_NOT_FOUND that names an npm package rather than
27
+ * the framework decision that led there.
28
+ *
24
29
  * Design (the SQLite backend):
25
30
  * - Each collection is a table `(_id TEXT PRIMARY KEY, doc TEXT)`; `doc` is JSON.
26
31
  * - Query filters are pushed down to SQL over `json_extract(doc, '$.field')`
@@ -101,57 +106,87 @@ export interface DeleteResult {
101
106
  deletedCount: number;
102
107
  }
103
108
  /** Lazy result cursor. Builds and runs SQL only when materialised (toArray). */
109
+ /** The three sort spellings a real FindCursor accepts. */
110
+ export type SortSpec = string | [string, number][] | Record<string, number> | Map<string, number>;
111
+ /**
112
+ * Normalise the driver's three sort spellings to a list of [key, direction].
113
+ *
114
+ * ADR-0036. A real `FindCursor.sort()` accepts a key plus a direction, a list
115
+ * of `[key, direction]` pairs, OR an object/Map - and the driver is the shape
116
+ * this fallback imitates (ADR-0025). The object form used to throw
117
+ * `TypeError: keyOrList is not iterable` here. Measured 2026-08-04 against a
118
+ * real MongoDB: the object spelling worked on the driver and threw on the
119
+ * fallback, in three of the four frameworks.
120
+ */
121
+ export declare function sortSpec(keyOrList: SortSpec, direction?: number): [string, number][];
104
122
  export declare class Cursor {
105
- private readonly collection;
106
- private readonly where;
107
- private readonly params;
108
- private readonly projection?;
109
- private _sort;
110
- private _limit;
111
- private _skip;
112
- constructor(collection: SqliteCollection, where: string, params: unknown[], projection?: (Record<string, unknown> | null) | undefined);
113
- sort(keyOrList: string | [string, number][], direction?: number): this;
123
+ #private;
124
+ /**
125
+ * The cursor receives WHAT IT NEEDS, not the collection it came from.
126
+ *
127
+ * It used to hold the collection and reach back for `connection`, `quoted` and
128
+ * `load` - which is the only reason those three were public. ADR-0025
129
+ * corollary 1: anything the fallback needs internally is private, and a real
130
+ * FindCursor exposes none of them. Handing over the two values and calling the
131
+ * module-level loader removes the back-reference AND the public surface.
132
+ */
133
+ constructor(conn: DatabaseSync, quoted: string, where: string, params: unknown[], projection?: Record<string, unknown> | null);
134
+ sort(keyOrList: SortSpec, direction?: number): this;
114
135
  limit(n: number): this;
115
136
  skip(n: number): this;
116
- private buildSql;
117
- /** Materialise the cursor into an array of decoded documents. */
118
- toArray(): Record<string, unknown>[];
119
- /** Alias for toArray() (pymongo's to_list / driver's toArray). */
120
- toList(length?: number): Record<string, unknown>[];
121
- [Symbol.iterator](): Iterator<Record<string, unknown>>;
137
+ /**
138
+ * Materialise the cursor into an array of decoded documents.
139
+ *
140
+ * ASYNC because the driver's FindCursor.toArray() is async (ADR-0025 clause
141
+ * 3). The work underneath is synchronous - node:sqlite has no async API - but
142
+ * the SHAPE is what a call site sees, and a shape that changes with the
143
+ * provider is the defect this fixes.
144
+ */
145
+ toArray(): Promise<Record<string, unknown>[]>;
146
+ /**
147
+ * Async iteration, matching the driver.
148
+ *
149
+ * NOTE: there is deliberately no [Symbol.iterator] here. A real FindCursor
150
+ * has ONLY Symbol.asyncIterator, so `for (const doc of cursor)` is a
151
+ * fallback-only spelling - it works locally and throws "is not iterable" the
152
+ * moment TINA4_MONGO_URI is set. Use `for await (const doc of cursor)`.
153
+ *
154
+ * toList() is gone for the same reason: the driver's FindCursor has no such
155
+ * method.
156
+ *
157
+ * ADR-0035 restored the uniform spellings in ruby and php through a
158
+ * delegator, and deliberately did NOT do so here. A delegator can only supply
159
+ * a method that is POSSIBLE on the real provider, and a synchronous iterator
160
+ * is not: a FindCursor is async-only. Adding one back on the fallback alone
161
+ * would recreate ADR-0025's worst measured defect - identical source changing
162
+ * TYPE, with a truthy Promise passing `if (doc)` for a document that does not
163
+ * exist. That is ADR-0025 corollary 3, which ADR-0035 keeps.
164
+ */
165
+ [Symbol.asyncIterator](): AsyncIterator<Record<string, unknown>>;
122
166
  }
123
167
  /** A SQLite-backed collection exposing the everyday MongoDB driver API. */
124
168
  export declare class SqliteCollection {
125
- readonly connection: DatabaseSync;
126
- private readonly name;
127
- readonly quoted: string;
128
- constructor(connection: DatabaseSync, name: string);
129
- private dump;
130
- /** Decode a stored JSON document (rehydrating ObjectId/Date), with optional projection. */
131
- load(docText: string, projection?: Record<string, unknown> | null): Record<string, unknown>;
132
- insertOne(document: Record<string, unknown>): InsertOneResult;
133
- insertMany(documents: Record<string, unknown>[]): InsertManyResult;
169
+ #private;
170
+ constructor(conn: DatabaseSync, name: string);
171
+ insertOne(document: Record<string, unknown>): Promise<InsertOneResult>;
172
+ insertMany(documents: Record<string, unknown>[]): Promise<InsertManyResult>;
134
173
  find(filter?: Record<string, unknown> | null, projection?: Record<string, unknown> | null): Cursor;
135
- findOne(filter?: Record<string, unknown> | null, projection?: Record<string, unknown> | null): Record<string, unknown> | null;
136
- countDocuments(filter?: Record<string, unknown> | null): number;
137
- estimatedDocumentCount(): number;
138
- distinct(key: string, filter?: Record<string, unknown> | null): unknown[];
139
- private matchingRows;
140
- private firstMatch;
141
- private writeBack;
142
- private doUpsert;
174
+ findOne(filter?: Record<string, unknown> | null, projection?: Record<string, unknown> | null): Promise<Record<string, unknown> | null>;
175
+ countDocuments(filter?: Record<string, unknown> | null): Promise<number>;
176
+ estimatedDocumentCount(): Promise<number>;
177
+ distinct(key: string, filter?: Record<string, unknown> | null): Promise<unknown[]>;
143
178
  updateOne(filter: Record<string, unknown> | null | undefined, update: Record<string, unknown>, options?: {
144
179
  upsert?: boolean;
145
- }): UpdateResult;
180
+ }): Promise<UpdateResult>;
146
181
  updateMany(filter: Record<string, unknown> | null | undefined, update: Record<string, unknown>, options?: {
147
182
  upsert?: boolean;
148
- }): UpdateResult;
183
+ }): Promise<UpdateResult>;
149
184
  replaceOne(filter: Record<string, unknown> | null | undefined, replacement: Record<string, unknown>, options?: {
150
185
  upsert?: boolean;
151
- }): UpdateResult;
152
- deleteOne(filter?: Record<string, unknown> | null): DeleteResult;
153
- deleteMany(filter?: Record<string, unknown> | null): DeleteResult;
154
- drop(): void;
186
+ }): Promise<UpdateResult>;
187
+ deleteOne(filter?: Record<string, unknown> | null): Promise<DeleteResult>;
188
+ deleteMany(filter?: Record<string, unknown> | null): Promise<DeleteResult>;
189
+ drop(): Promise<void>;
155
190
  }
156
191
  /** A SQLite-backed document database (a file of collection tables). */
157
192
  export declare class SqliteDatabase {
@@ -163,6 +198,18 @@ export declare class SqliteDatabase {
163
198
  listCollectionNames(): string[];
164
199
  close(): void;
165
200
  }
201
+ /**
202
+ * A Mongo URI is configured but the MongoDB driver is not installed.
203
+ *
204
+ * ADR-0024 rule 3, settled for DocStore by ADR-0033: a provider that cannot
205
+ * honour an operation must RAISE, naming the provider and what is missing.
206
+ * Node already threw here, but with a bare ERR_MODULE_NOT_FOUND that named an
207
+ * npm package and not the framework decision that led there - so the outcome
208
+ * was loud but undocumented, and different from the other three frameworks.
209
+ */
210
+ export declare class DocStoreDriverMissing extends Error {
211
+ constructor(message: string);
212
+ }
166
213
  /** True when no Mongo is configured, so the SQLite fallback is in effect. */
167
214
  export declare function isServerless(): boolean;
168
215
  /**
@@ -172,11 +219,23 @@ export declare function isServerless(): boolean;
172
219
  * `mongodb` driver is installed); otherwise a `SqliteCollection` backed by the
173
220
  * local SQLite file. Same call sites either way - only the backend differs.
174
221
  *
175
- * The real-Mongo path is async (the driver connects lazily), so this returns a
176
- * Promise when Mongo is configured. In serverless mode it returns a
177
- * SqliteCollection synchronously (the common local-dev case).
222
+ * ALWAYS async, on BOTH providers (ADR-0025 clause 3).
223
+ *
224
+ * It used to return a SqliteCollection SYNCHRONOUSLY in serverless mode and a
225
+ * Promise on the real-Mongo path. That made identical source change TYPE when
226
+ * TINA4_MONGO_URI was set, and a Promise is always truthy - so un-awaited code
227
+ * read a real document locally and a thenable in production, and `if (doc)`
228
+ * succeeded for a document that did not exist. The driver cannot become sync,
229
+ * so the fallback becomes async.
230
+ */
231
+ export declare function getCollection(name: string): Promise<SqliteCollection | unknown>;
232
+ /**
233
+ * Close every DocStore connection: the SQLite store and all Mongo clients.
234
+ *
235
+ * A pooled client keeps the event loop alive, so a script or test that touches
236
+ * the real provider needs a way to let the process end on its own.
178
237
  */
179
- export declare function getCollection(name: string): SqliteCollection | Promise<unknown>;
238
+ export declare function closeDocStore(): Promise<void>;
180
239
  /** Drop the cached default SQLite store (test helper). */
181
240
  export declare function resetDefaultStore(): void;
182
241
  export {};
@@ -1,10 +1,11 @@
1
- export type { FieldType, FieldDefinition, ModelDefinition, DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, QueryOptions, RelationshipDefinition, PaginatedResult, } from "./types.js";
2
- export { FetchResult } from "./types.js";
1
+ export type { FieldType, FieldDefinition, ModelDefinition, DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, QueryOptions, RelationshipDefinition, } from "./types.js";
3
2
  export { DatabaseResult } from "./databaseResult.js";
4
3
  export type { ColumnInfoResult } from "./databaseResult.js";
5
4
  export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons, wrapWithCache, resetRequestCaches } from "./database.js";
6
5
  export { adapterFetch, adapterQuery, adapterFetchOne, adapterExecute, adapterStartTransaction, adapterCommit, adapterRollback, adapterTableExists, adapterTables, adapterColumns, adapterCreateTable, extractLastInsertId, } from "./database.js";
7
- export type { DatabaseConfig, ParsedDatabaseUrl } from "./database.js";
6
+ export type { DatabaseConfig } from "./database.js";
7
+ export { DatabaseUrl, redactCredentials } from "./databaseUrl.js";
8
+ export type { DatabaseEngine } from "./databaseUrl.js";
8
9
  export { discoverModels } from "./model.js";
9
10
  export type { DiscoveredModel } from "./model.js";
10
11
  export { syncModels, ensureMigrationTable, getNextBatch, isMigrationApplied, recordMigration, applyMigration, rollback, getAppliedMigrations, getLastBatchMigrations, removeMigrationRecord, migrate, createMigration, status, Migration, splitStatements, parseSetTerm, normalizeQuotes, sortMigrationFiles, shouldSkipCreateTable, } from "./migration.js";
@@ -17,12 +18,13 @@ export type { ValidationError } from "./validation.js";
17
18
  export { BaseModel, snakeToCamel, camelToSnake } from "./baseModel.js";
18
19
  export { QueryBuilder } from "./queryBuilder.js";
19
20
  export { SQLTranslator, QueryCache } from "./sqlTranslator.js";
21
+ export { DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS, CONNECT_TIMEOUT_TOLERANCE_MS, connectTimeoutMillis, driverConnectTimeoutMillis, connectTarget, withConnectTimeout, } from "./connectTimeout.js";
20
22
  export { CachedDatabaseAdapter } from "./cachedDatabase.js";
21
23
  export type { CachedAdapterOptions } from "./cachedDatabase.js";
22
24
  export { FakeData } from "./fakeData.js";
23
25
  export { seedTable, seedOrm, seedModels, autoFieldMap } from "./seeder.js";
24
26
  export type { SeedSummary, SeedOptions } from "./seeder.js";
25
- export { ObjectId, InvalidId, SqliteDatabase, SqliteCollection, Cursor, getCollection, isServerless, resetDefaultStore, encodeValue, decodeValue, compileFilter, } from "./docstore.js";
27
+ export { ObjectId, InvalidId, DocStoreDriverMissing, SqliteDatabase, SqliteCollection, Cursor, getCollection, isServerless, resetDefaultStore, closeDocStore, encodeValue, decodeValue, compileFilter, } from "./docstore.js";
26
28
  export type { InsertOneResult, InsertManyResult, UpdateResult, DeleteResult, } from "./docstore.js";
27
29
  export { SQLiteAdapter } from "./adapters/sqlite.js";
28
30
  export { PostgresAdapter } from "./adapters/postgres.js";
@@ -206,7 +206,7 @@ export declare function status(adapter?: DatabaseAdapter, options?: {
206
206
  */
207
207
  export declare function createMigration(description: string, options?: {
208
208
  migrationsDir?: string;
209
- kind?: "sql" | "class";
209
+ kind?: "sql" | "code" | "class";
210
210
  }): Promise<string | {
211
211
  upPath: string;
212
212
  downPath: string;
@@ -258,11 +258,12 @@ export declare class Migration {
258
258
  * Scaffold a new migration file.
259
259
  *
260
260
  * kind="sql" — creates {timestamp}_{description}.sql + .down.sql (default)
261
- * kind="class" — creates {timestamp}_{description}.ts with a TypeScript class template
261
+ * kind="code" — creates {timestamp}_{description}.ts with a TypeScript class
262
+ * template. "class" is accepted as a legacy alias.
262
263
  *
263
264
  * Returns the path to the created up file (or class file).
264
265
  */
265
- create(description: string, kind?: "sql" | "class"): Promise<string | {
266
+ create(description: string, kind?: "sql" | "code" | "class"): Promise<string | {
266
267
  upPath: string;
267
268
  downPath: string;
268
269
  }>;
@@ -17,6 +17,7 @@
17
17
  * .get();
18
18
  */
19
19
  import type { DatabaseAdapter } from "./types.js";
20
+ import { DatabaseResult } from "./databaseResult.js";
20
21
  export declare class QueryBuilder {
21
22
  private table;
22
23
  private db;
@@ -118,11 +119,30 @@ export declare class QueryBuilder {
118
119
  */
119
120
  toSql(): string;
120
121
  /**
121
- * Execute the query and return all matching rows.
122
+ * Execute the query and return a DatabaseResult.
122
123
  *
123
- * @returns Array of row objects.
124
+ * BREAKING (3.13.95, parity): this returned a bare array of rows. The other
125
+ * three frameworks all return the DatabaseResult that `db.fetch()` produces:
126
+ * Python get() -> DatabaseResult (orm/query_builder/__init__.py)
127
+ * PHP get(): mixed -> $this->db->fetch(...)
128
+ * Ruby get -> @db.fetch(...)
129
+ * Node was the odd one out, so the same builder chain returned a different
130
+ * TYPE per language and portable code could not read `.records`, `.count`,
131
+ * `.limit` or `.offset` off it.
132
+ *
133
+ * MIGRATION: read `.records` for the rows.
134
+ * before: const rows = await qb.get(); rows.length
135
+ * after: const result = await qb.get(); result.records.length
136
+ * DatabaseResult is iterable, so `for (const row of result)` and
137
+ * `[...result]` work unchanged, and `response()`/`res.json()` already
138
+ * auto-serialize it to a JSON array.
139
+ *
140
+ * No default LIMIT is applied when `.limit()` was never called (v3.13.39) --
141
+ * a silent cap here was a data-loss-on-read footgun. That is unchanged.
142
+ *
143
+ * @returns DatabaseResult carrying `.records`, `.count`, `.limit`, `.offset`.
124
144
  */
125
- get<T = Record<string, unknown>>(): Promise<T[]>;
145
+ get(): Promise<DatabaseResult>;
126
146
  /**
127
147
  * Execute the query and return a single row.
128
148
  *