tina4-nodejs 3.13.97 → 3.13.99

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 (96) hide show
  1. package/CLAUDE.md +60 -25
  2. package/package.json +1 -2
  3. package/packages/cli/dist/bin.js +20620 -18995
  4. package/packages/cli/src/bin.ts +28 -71
  5. package/packages/cli/src/commands/migrate.ts +36 -75
  6. package/packages/cli/src/commands/migrateRollback.ts +10 -1
  7. package/packages/cli/src/commands/test.ts +92 -21
  8. package/packages/core/dist/index.js +20459 -18815
  9. package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
  10. package/packages/core/src/ai.ts +28 -12
  11. package/packages/core/src/api.ts +13 -5
  12. package/packages/core/src/background.ts +9 -3
  13. package/packages/core/src/devAdmin.ts +135 -20
  14. package/packages/core/src/dispatchPipeline.ts +185 -1
  15. package/packages/core/src/docs.ts +33 -5
  16. package/packages/core/src/env.ts +1 -1
  17. package/packages/core/src/errorOverlay.ts +39 -48
  18. package/packages/core/src/fakeData.ts +15 -0
  19. package/packages/core/src/index.ts +17 -6
  20. package/packages/core/src/logger.ts +892 -572
  21. package/packages/core/src/mcp.ts +9 -1
  22. package/packages/core/src/messenger.ts +31 -4
  23. package/packages/core/src/middleware.ts +169 -43
  24. package/packages/core/src/portTakeover.ts +232 -0
  25. package/packages/core/src/request.ts +57 -8
  26. package/packages/core/src/response.ts +67 -0
  27. package/packages/core/src/router.ts +35 -7
  28. package/packages/core/src/server.ts +450 -190
  29. package/packages/core/src/static.ts +81 -12
  30. package/packages/core/src/testClient.ts +126 -137
  31. package/packages/core/src/testing.ts +16 -12
  32. package/packages/core/src/types.ts +21 -9
  33. package/packages/core/src/version.ts +66 -0
  34. package/packages/core/src/websocket.ts +2 -2
  35. package/packages/core/src/websocketBackplane.ts +2 -2
  36. package/packages/frond/dist/index.js +31 -13
  37. package/packages/frond/src/engine.ts +39 -7
  38. package/packages/orm/dist/index.js +10879 -9258
  39. package/packages/orm/src/adapters/firebird.ts +200 -27
  40. package/packages/orm/src/adapters/mongodb.ts +160 -10
  41. package/packages/orm/src/adapters/mssql.ts +38 -11
  42. package/packages/orm/src/adapters/mysql.ts +24 -1
  43. package/packages/orm/src/adapters/odbc.ts +127 -29
  44. package/packages/orm/src/adapters/postgres.ts +18 -0
  45. package/packages/orm/src/adapters/sqlite.ts +93 -14
  46. package/packages/orm/src/autoCrud.ts +72 -8
  47. package/packages/orm/src/baseModel.ts +323 -71
  48. package/packages/orm/src/cachedDatabase.ts +48 -1
  49. package/packages/orm/src/database.ts +162 -59
  50. package/packages/orm/src/fakeData.ts +6 -2
  51. package/packages/orm/src/index.ts +4 -1
  52. package/packages/orm/src/migration.ts +95 -52
  53. package/packages/orm/src/query.ts +16 -4
  54. package/packages/orm/src/seeder.ts +43 -25
  55. package/packages/orm/src/sqlTranslator.ts +104 -19
  56. package/packages/orm/src/types.ts +97 -21
  57. package/packages/orm/src/validation.ts +5 -1
  58. package/packages/swagger/dist/index.js +3 -2
  59. package/packages/swagger/src/generator.ts +19 -4
  60. package/packages/swagger/src/ui.ts +6 -4
  61. package/types/cli/src/bin.d.ts +0 -22
  62. package/types/core/src/api.d.ts +11 -4
  63. package/types/core/src/background.d.ts +5 -2
  64. package/types/core/src/devAdmin.d.ts +35 -0
  65. package/types/core/src/dispatchPipeline.d.ts +41 -1
  66. package/types/core/src/errorOverlay.d.ts +13 -13
  67. package/types/core/src/index.d.ts +9 -6
  68. package/types/core/src/logger.d.ts +111 -185
  69. package/types/core/src/middleware.d.ts +40 -5
  70. package/types/core/src/portTakeover.d.ts +50 -0
  71. package/types/core/src/request.d.ts +15 -0
  72. package/types/core/src/response.d.ts +29 -0
  73. package/types/core/src/server.d.ts +92 -0
  74. package/types/core/src/testClient.d.ts +29 -3
  75. package/types/core/src/testing.d.ts +16 -12
  76. package/types/core/src/types.d.ts +21 -9
  77. package/types/core/src/version.d.ts +11 -0
  78. package/types/core/src/websocketBackplane.d.ts +1 -1
  79. package/types/frond/src/engine.d.ts +10 -0
  80. package/types/orm/src/adapters/firebird.d.ts +61 -2
  81. package/types/orm/src/adapters/mongodb.d.ts +20 -0
  82. package/types/orm/src/adapters/mssql.d.ts +11 -0
  83. package/types/orm/src/adapters/mysql.d.ts +11 -0
  84. package/types/orm/src/adapters/odbc.d.ts +35 -4
  85. package/types/orm/src/adapters/postgres.d.ts +11 -0
  86. package/types/orm/src/adapters/sqlite.d.ts +23 -4
  87. package/types/orm/src/baseModel.d.ts +45 -25
  88. package/types/orm/src/cachedDatabase.d.ts +27 -1
  89. package/types/orm/src/database.d.ts +56 -6
  90. package/types/orm/src/index.d.ts +3 -2
  91. package/types/orm/src/migration.d.ts +23 -5
  92. package/types/orm/src/query.d.ts +3 -0
  93. package/types/orm/src/seeder.d.ts +15 -2
  94. package/types/orm/src/sqlTranslator.d.ts +17 -4
  95. package/types/orm/src/types.d.ts +75 -16
  96. package/packages/core/src/errorOverlay.test.ts +0 -122
@@ -4,7 +4,18 @@ export declare class TestResponse {
4
4
  readonly body: string;
5
5
  readonly headers: Record<string, string>;
6
6
  readonly contentType: string;
7
- constructor(statusCode: number, headers: Record<string, string>, body: string);
7
+ /** Every value sent per header name (lowercased), in emission order. */
8
+ private readonly headerList;
9
+ constructor(statusCode: number, headerList: Record<string, string[]>, body: string);
10
+ /**
11
+ * Every value sent for `name` (case-insensitive), in emission order.
12
+ *
13
+ * A header sent once returns a one-item array; a header never sent returns
14
+ * an empty array. This is the one place a duplicate response header (two
15
+ * `Set-Cookie`) is visible — `headers[name]` always collapses to the LAST
16
+ * value, same as before (TC-HEADER-COLLAPSE, TC-DEC-02).
17
+ */
18
+ getHeaderList(name: string): string[];
8
19
  /** Parse body as JSON. */
9
20
  json(): unknown;
10
21
  /** Return body as a string. */
@@ -17,8 +28,23 @@ export interface RequestOptions {
17
28
  headers?: Record<string, string>;
18
29
  }
19
30
  export declare class TestClient {
20
- private router;
31
+ /** An explicitly-injected router (test isolation); undefined means "use the live server's router, or defaultRouter". */
32
+ private readonly explicitRouter;
33
+ private ctxPromise;
21
34
  constructor(router?: Router);
35
+ /**
36
+ * Resolve (and memoise) the DispatchContext this client dispatches
37
+ * through.
38
+ *
39
+ * An explicitly-injected router always gets its OWN standalone context
40
+ * (buildDispatchContext) — the test-isolation contract an injected router
41
+ * has always had: a dedicated Router never races with whatever else is
42
+ * registered on defaultRouter or a live server. With no injected router,
43
+ * the LIVE server's context wins when one is running in this process
44
+ * (getLiveDispatchContext — maximum fidelity, mirrors Ruby's
45
+ * `RackApp.current`), else a standalone context bound to defaultRouter.
46
+ */
47
+ private context;
22
48
  /** Send a GET request. */
23
49
  get(path: string, options?: RequestOptions): Promise<TestResponse>;
24
50
  /** Send a POST request. */
@@ -29,7 +55,7 @@ export declare class TestClient {
29
55
  patch(path: string, options?: RequestOptions): Promise<TestResponse>;
30
56
  /** Send a DELETE request. */
31
57
  delete(path: string, options?: RequestOptions): Promise<TestResponse>;
32
- /** Build a mock request, match the route, execute the handler. */
58
+ /** Build a mock request/response pair and dispatch it through the REAL pipeline (runDispatch). */
33
59
  private _request;
34
60
  /** Gather the captured status/headers/body into a TestResponse and free the socket. */
35
61
  private _collect;
@@ -1,19 +1,23 @@
1
1
  /**
2
2
  * Tina4 Node.js — Inline testing framework.
3
3
  *
4
- * Attach test assertions to functions and run them all at once.
4
+ * Attach test expectations to functions and run them all at once.
5
5
  *
6
- * import { tests, assertEqual, assertRaises, runAll } from "./testing.js";
6
+ * import { tests, expectEqual, expectRaises, runAll } from "./testing.js";
7
7
  *
8
8
  * const add = tests(
9
- * assertEqual([5, 3], 8),
10
- * assertRaises(Error, [null]),
9
+ * expectEqual([5, 3], 8),
10
+ * expectRaises(Error, [null]),
11
11
  * )(function add(a: number, b: number | null = null): number {
12
12
  * if (b === null) throw new Error("b required");
13
13
  * return a + b;
14
14
  * });
15
15
  *
16
16
  * runAll();
17
+ *
18
+ * The builders are named expect* — DESCRIPTORS that record an expectation for the
19
+ * runner — deliberately distinct from the xUnit assert* on Tina4Test (test.ts),
20
+ * so importing the wrong surface can never silently change call semantics.
17
21
  */
18
22
  interface Assertion {
19
23
  type: "equal" | "raises" | "true" | "false";
@@ -31,14 +35,14 @@ interface TestResults {
31
35
  message?: string;
32
36
  }>;
33
37
  }
34
- /** Assert that calling the function with `args` returns `expected`. */
35
- export declare function assertEqual(args: unknown[], expected: unknown): Assertion;
36
- /** Assert that calling the function with `args` throws an instance of `errorClass`. */
37
- export declare function assertRaises(errorClass: new (...a: unknown[]) => Error, args: unknown[]): Assertion;
38
- /** Assert that calling the function with `args` returns a truthy value. */
39
- export declare function assertTrue(args: unknown[]): Assertion;
40
- /** Assert that calling the function with `args` returns a falsy value. */
41
- export declare function assertFalse(args: unknown[]): Assertion;
38
+ /** Expect that calling the function with `args` returns `expected`. */
39
+ export declare function expectEqual(args: unknown[], expected: unknown): Assertion;
40
+ /** Expect that calling the function with `args` throws an instance of `errorClass`. */
41
+ export declare function expectRaises(errorClass: new (...a: unknown[]) => Error, args: unknown[]): Assertion;
42
+ /** Expect that calling the function with `args` returns a truthy value. */
43
+ export declare function expectTrue(args: unknown[]): Assertion;
44
+ /** Expect that calling the function with `args` returns a falsy value. */
45
+ export declare function expectFalse(args: unknown[]): Assertion;
42
46
  /**
43
47
  * Attach inline test assertions to a function.
44
48
  *
@@ -18,20 +18,32 @@ export interface Tina4Request extends IncomingMessage {
18
18
  /**
19
19
  * Path params. Typed params arrive coerced: `{id:int}`/`{id:integer}` and
20
20
  * `{p:float}`/`{p:number}` are JS `number`s; every other type and untyped
21
- * `{id}` stay `string` (parity with Python/PHP/Ruby).
21
+ * `{id}` stay `string` (parity with Python/PHP/Ruby). ROUTE-ONLY — never
22
+ * the query string or body (REQ-PARAM-POLLUTION, 3.13.99). Writable: the
23
+ * router assigns it AFTER createRequest() builds the wire-derived fields
24
+ * below, once a route has matched.
22
25
  */
23
26
  params: Record<string, string | number>;
24
- query: Record<string, string>;
27
+ /**
28
+ * Core wire-derived fields below are `readonly` (REQ-IMMUTABILITY-DIVERGE,
29
+ * 3.13.99) — set once in createRequest() and never reassigned afterward,
30
+ * matching PHP's `readonly` properties and Ruby's writer-less attr_reader
31
+ * (the two languages already at this posture; this is TS-compile-time
32
+ * only, like PHP/Ruby's enforcement is at their own language boundary).
33
+ * `params`/`body`/`files`/`session`/`user` stay mutable: the router and
34
+ * middleware legitimately set them after construction.
35
+ */
36
+ readonly query: Record<string, string>;
25
37
  /**
26
38
  * Request path only — no query string. Matches `request.path` in
27
39
  * Python/PHP/Ruby. Example: `/users/42`.
28
40
  */
29
- path: string;
41
+ readonly path: string;
30
42
  /**
31
43
  * Raw query string with no leading "?". Matches `request.query_string`
32
44
  * (Python/Ruby) and `request.queryString` (PHP). Example: `"page=2"`.
33
45
  */
34
- queryString: string;
46
+ readonly queryString: string;
35
47
  /**
36
48
  * Full absolute URL — `scheme://host[:port]/path[?query]`.
37
49
  * Honours X-Forwarded-Proto / X-Forwarded-Host. Matches PHP/Ruby/Python parity.
@@ -39,19 +51,19 @@ export interface Tina4Request extends IncomingMessage {
39
51
  * Note: this overrides Node's native `IncomingMessage.url` (which contains
40
52
  * only path+query). Inside Tina4 handlers, `req.url` is always the full URL.
41
53
  */
42
- url: string;
54
+ readonly url: string;
43
55
  body: unknown;
44
- ip: string;
56
+ readonly ip: string;
45
57
  /**
46
58
  * Raw socket peer address - NEVER honours X-Forwarded-For (which any
47
59
  * caller can spoof), so it can be trusted for security decisions.
48
60
  * Empty for in-process / synthetic requests. Parity with Python's
49
61
  * request.remote_ip and PHP's Request::$remoteIp.
50
62
  */
51
- remoteIp: string;
63
+ readonly remoteIp: string;
52
64
  files: Record<string, UploadedFile | UploadedFile[]>;
53
- cookies: Record<string, string>;
54
- contentType: string;
65
+ readonly cookies: Record<string, string>;
66
+ readonly contentType: string;
55
67
  /**
56
68
  * NULL when the session backend was unusable for this request (ADR-0021).
57
69
  * The request path logs the failure and degrades rather than 500-ing, so a
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Walk up from this file to the nearest package.json carrying a non-empty
3
+ * `version` field. Stops at the first hit (nearest wins), so a published
4
+ * `@tina4/core` install resolves its OWN package.json, and the monorepo dev
5
+ * tree resolves the workspace root's -- both the real, current version.
6
+ * Falls back to "0.0.0" only if none is found within the walk (a layout with
7
+ * no package.json anywhere in its ancestry at all).
8
+ */
9
+ export declare function resolveFrameworkVersion(): string;
10
+ /** Resolved once at module load -- every @tina4/core surface imports this. */
11
+ export declare const TINA4_VERSION: string;
@@ -155,7 +155,7 @@ export declare class WsBackplaneManager {
155
155
  /** Minimal logger shape so the manager doesn't import the logger module. */
156
156
  export interface WsBackplaneLogger {
157
157
  info(message: string): void;
158
- warn(message: string): void;
158
+ warning(message: string): void;
159
159
  error(message: string): void;
160
160
  }
161
161
  /** Build the cross-framework envelope. Exported for tests. */
@@ -126,6 +126,16 @@ export declare class Frond {
126
126
  /** Render a debug dump of a value as HTML — parity with PHP/Ruby/Python.
127
127
  * Gated on TINA4_DEBUG=true. Returns empty string in production. */
128
128
  renderDump(value: unknown): string;
129
+ /**
130
+ * Load a template's source, CONFINED under the templates directory.
131
+ *
132
+ * Every path-taking tag ({% include %}, {% extends %}, {% import %},
133
+ * {% from ... import %}) funnels through this one loader, so this single guard
134
+ * confines them all (TAG-DEC-01): a name that is absolute, climbs out with a
135
+ * `..` up-level segment, or resolves through a symlink to a location OUTSIDE
136
+ * the templates root is REFUSED -- the outside file is never read. Template
137
+ * -side analogue of the static-asset confinement (feature 41 / ADR-0050).
138
+ */
129
139
  private load;
130
140
  /** Execute pre-tokenized template against context. */
131
141
  private executeCached;
@@ -89,9 +89,42 @@ export declare class FirebirdAdapter implements DatabaseAdapter {
89
89
  private db;
90
90
  private transaction;
91
91
  private _lastInsertId;
92
+ /** Resolved node-firebird config, kept so a dead connection can re-attach. */
93
+ private fbConfig;
94
+ private static readonly DEAD_CONN_MARKERS;
95
+ /** Is this a dead-socket error worth a transparent reconnect (not a logical SQL error)? */
96
+ static isDeadConnection(err: unknown): boolean;
92
97
  constructor(config: FirebirdConfig | string);
93
98
  /** Connect to Firebird. Must be called before using the adapter. */
99
+ /** ADR-0044 required adapter capability. */
100
+ getDatabaseType(): string;
101
+ /** ADR-0044: readable/writable native boolean. */
102
+ autocommit: boolean;
103
+ /**
104
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
105
+ * multi-row batch by default. A test-only deployment representing one
106
+ * that cannot sets this false so executeMany rejects BEFORE the first
107
+ * write rather than risking partial durability.
108
+ */
109
+ supportsAtomicBatch: boolean;
94
110
  connect(): Promise<void>;
111
+ private attachOnce;
112
+ /**
113
+ * Attach with a BOUNDED retry (FB-DEC-03). node-firebird's SRP login over
114
+ * WireCrypt is intermittently flaky (~12% measured historically), and a flake
115
+ * surfaces as an auth/handshake error indistinguishable from a real one, so a
116
+ * bounded retry-all is the robust, honest handling: a transient handshake
117
+ * failure recovers, while a genuine bad credential still fails after the bound
118
+ * -- never skipped, never papered over.
119
+ */
120
+ private attachWithRetry;
121
+ /**
122
+ * Run a node-firebird op; on a DEAD-connection error (outside an explicit
123
+ * transaction) re-attach once and retry (FB-DEC-01). Inside a transaction the
124
+ * error surfaces -- atomicity beats resilience, and the caller rolls back.
125
+ */
126
+ private withReconnect;
127
+ private reconnectFirebird;
95
128
  private parseUrl;
96
129
  private ensureConnected;
97
130
  /** Translate SQL for Firebird dialect. */
@@ -121,6 +154,28 @@ export declare class FirebirdAdapter implements DatabaseAdapter {
121
154
  private statementHandle;
122
155
  private queryPromise;
123
156
  private executePromise;
157
+ /**
158
+ * The real affected-row count. node-firebird gives NO DML count of its own
159
+ * (the callback result is undefined -- MEASURED), but Firebird 5 multi-row
160
+ * RETURNING surfaces one row per affected row, so `... RETURNING 1` + the row
161
+ * count IS the real count (FB-AFFECTED-FAB replaces the hardcoded 1). RETURNING
162
+ * a constant, not `*`, so a large update/delete does not materialise full rows.
163
+ */
164
+ private executeReturningCount;
165
+ /**
166
+ * Firebird has no generic last_insert_id -- read the GEN_<TABLE>_ID generator
167
+ * the row's BEFORE INSERT trigger drew from (FB-LASTID-GAP). Column-name-
168
+ * independent, so correct for a non-`id` PK too. null when the table has no
169
+ * such generator (GEN_ID then throws -> caught).
170
+ */
171
+ private readGeneratorId;
172
+ /**
173
+ * Read a node-firebird BLOB column into a Buffer. A BLOB arrives as a STREAMING
174
+ * FUNCTION (fn((err, name, emitter) => emitter.on('data'|'end'))), NOT a Buffer
175
+ * -- MEASURED -- so the old decodeBlobs no-op leaked the function to the caller
176
+ * and no bytes round-tripped (FB-BLOB-SRP-UNVERIFIED).
177
+ */
178
+ private readBlob;
124
179
  execute(sql: string, params?: unknown[]): unknown;
125
180
  executeMany(sql: string, paramsList: unknown[][]): {
126
181
  totalAffected: number;
@@ -133,8 +188,12 @@ export declare class FirebirdAdapter implements DatabaseAdapter {
133
188
  executeAsync(sql: string, params?: unknown[]): Promise<unknown>;
134
189
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
135
190
  queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
136
- /** Ensure BLOB columns are readable — node-firebird may return callback-based
137
- * blob readers. Convert to Buffer. Regular buffers pass through unchanged. */
191
+ /**
192
+ * Read out any BLOB columns to Buffers. node-firebird returns a BLOB as a
193
+ * STREAMING FUNCTION, not a Buffer (MEASURED), so a column whose value is a
194
+ * function is read via readBlob(); everything else passes through unchanged
195
+ * (FB-BLOB-SRP-UNVERIFIED -- the old no-op leaked the function to the caller).
196
+ */
138
197
  private decodeBlobs;
139
198
  fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[];
140
199
  fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]>;
@@ -24,6 +24,17 @@ export declare class MongodbAdapter implements DatabaseAdapter {
24
24
  private _dbName;
25
25
  constructor(config: MongoConfig | string);
26
26
  /** Connect to MongoDB. Must be called before using the adapter. */
27
+ /** ADR-0044 required adapter capability. */
28
+ getDatabaseType(): string;
29
+ /** ADR-0044: readable/writable native boolean. */
30
+ autocommit: boolean;
31
+ /**
32
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
33
+ * multi-row batch by default. A test-only deployment representing one
34
+ * that cannot sets this false so executeMany rejects BEFORE the first
35
+ * write rather than risking partial durability.
36
+ */
37
+ supportsAtomicBatch: boolean;
27
38
  connect(): Promise<void>;
28
39
  private ensureConnected;
29
40
  /** Execute a SQL-like statement translated to a MongoDB operation. */
@@ -43,6 +54,15 @@ export declare class MongodbAdapter implements DatabaseAdapter {
43
54
  fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]>;
44
55
  fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
45
56
  fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
57
+ /**
58
+ * Atomic, monotonic, concurrency-safe next id — feature 16. A
59
+ * findOneAndUpdate($inc) on the tina4_sequences collection, keyed by _id (its
60
+ * built-in unique index makes concurrent first-use upserts race-safe: two
61
+ * callers can never create two counters for one table). Seeds from
62
+ * MAX(pkColumn) the FIRST time only ($setOnInsert). Throws on an impossible
63
+ * empty result rather than returning a fixed id that could collide with a row.
64
+ */
65
+ getNextId(table: string, pkColumn?: string): Promise<number>;
46
66
  insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
47
67
  insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult>;
48
68
  update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): DatabaseResult;
@@ -22,6 +22,17 @@ export declare class MssqlAdapter implements DatabaseAdapter {
22
22
  private _inTransaction;
23
23
  constructor(config: MssqlConfig | string);
24
24
  /** Connect to MSSQL. Must be called before using the adapter. */
25
+ /** ADR-0044 required adapter capability. */
26
+ getDatabaseType(): string;
27
+ /** ADR-0044: readable/writable native boolean. */
28
+ autocommit: boolean;
29
+ /**
30
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
31
+ * multi-row batch by default. A test-only deployment representing one
32
+ * that cannot sets this false so executeMany rejects BEFORE the first
33
+ * write rather than risking partial durability.
34
+ */
35
+ supportsAtomicBatch: boolean;
25
36
  connect(): Promise<void>;
26
37
  private parseUrl;
27
38
  private ensureConnected;
@@ -21,6 +21,17 @@ export declare class MysqlAdapter implements DatabaseAdapter {
21
21
  private _inTransaction;
22
22
  constructor(config: MysqlConfig | string);
23
23
  /** Connect to MySQL. Must be called before using the adapter. */
24
+ /** ADR-0044 required adapter capability. */
25
+ getDatabaseType(): string;
26
+ /** ADR-0044: readable/writable native boolean. */
27
+ autocommit: boolean;
28
+ /**
29
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
30
+ * multi-row batch by default. A test-only deployment representing one
31
+ * that cannot sets this false so executeMany rejects BEFORE the first
32
+ * write rather than risking partial durability.
33
+ */
34
+ supportsAtomicBatch: boolean;
24
35
  connect(): Promise<void>;
25
36
  private ensureConnected;
26
37
  private queryPromise;
@@ -2,6 +2,10 @@ import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } fro
2
2
  export interface OdbcConfig {
3
3
  /** Full ODBC connection string, e.g. "DSN=MyDSN" or "DRIVER={SQL Server};SERVER=host;DATABASE=db" */
4
4
  connectionString: string;
5
+ /** Optional username; appended as UID when not already in the connection string. */
6
+ username?: string;
7
+ /** Optional password; appended as PWD when not already in the connection string. */
8
+ password?: string;
5
9
  }
6
10
  export declare class OdbcAdapter implements DatabaseAdapter {
7
11
  private config;
@@ -16,6 +20,14 @@ export declare class OdbcAdapter implements DatabaseAdapter {
16
20
  constructor(config: OdbcConfig | string);
17
21
  /** Extract the raw ODBC connection string from config. */
18
22
  private getConnectionString;
23
+ /**
24
+ * The connection string with credentials applied. ODBC has no separate-
25
+ * credentials API (odbc.connect() reads only the string), so a username/
26
+ * password passed to Database.create() must be folded in as UID/PWD - the
27
+ * adapter used to drop them. Never used for diagnostics (describeTarget reads
28
+ * the raw string), so the password never reaches an error message.
29
+ */
30
+ private effectiveConnectionString;
19
31
  /**
20
32
  * The address for a diagnostic message. ODBC hides it inside an opaque
21
33
  * driver keyword string, so this reads the standard keywords and falls back to
@@ -24,6 +36,17 @@ export declare class OdbcAdapter implements DatabaseAdapter {
24
36
  */
25
37
  private describeTarget;
26
38
  /** Connect to the ODBC data source. Must be called before using the adapter. */
39
+ /** ADR-0044 required adapter capability. */
40
+ getDatabaseType(): string;
41
+ /** ADR-0044: readable/writable native boolean. */
42
+ autocommit: boolean;
43
+ /**
44
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
45
+ * multi-row batch by default. A test-only deployment representing one
46
+ * that cannot sets this false so executeMany rejects BEFORE the first
47
+ * write rather than risking partial durability.
48
+ */
49
+ supportsAtomicBatch: boolean;
27
50
  connect(): Promise<void>;
28
51
  private ensureConnected;
29
52
  execute(sql: string, params?: unknown[]): unknown;
@@ -56,18 +79,20 @@ export declare class OdbcAdapter implements DatabaseAdapter {
56
79
  totalAffected: number;
57
80
  lastId?: number | bigint;
58
81
  }>;
82
+ /** The real affected-row count from an odbc result, when the driver reports it. */
83
+ private affectedCount;
59
84
  /** Run a SELECT and return all matching rows. */
60
85
  queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
61
86
  /** Run a SELECT with optional LIMIT/OFFSET pagination. */
62
87
  fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]>;
63
88
  /** Run a SELECT and return the first row or null. */
64
89
  fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null>;
65
- /** Insert a single row into a table. */
66
- insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult>;
90
+ /** Insert a single row, or a list of rows as a batch. */
91
+ insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult>;
67
92
  /** Update rows in a table matching filter. */
68
- updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult>;
93
+ updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult>;
69
94
  /** Delete rows from a table. */
70
- deleteAsync(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[]): Promise<DatabaseResult>;
95
+ deleteAsync(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): Promise<DatabaseResult>;
71
96
  /** Begin a transaction. */
72
97
  startTransactionAsync(): Promise<void>;
73
98
  /** Commit the current transaction. */
@@ -78,6 +103,12 @@ export declare class OdbcAdapter implements DatabaseAdapter {
78
103
  tablesAsync(): Promise<string[]>;
79
104
  /** Get column metadata for a table using ODBC catalog functions. */
80
105
  columnsAsync(table: string): Promise<ColumnInfo[]>;
106
+ /**
107
+ * The table's primary-key columns from the ODBC catalog (SQLPrimaryKeys),
108
+ * lower-cased for case-insensitive matching. Empty on any target that does not
109
+ * report them - the write-guard then requires an explicit filter.
110
+ */
111
+ private primaryKeyColumns;
81
112
  /** Check whether a table exists. */
82
113
  tableExistsAsync(name: string): Promise<boolean>;
83
114
  /** Create a table from a FieldDefinition map. Uses generic SQL — works with most ODBC sources. */
@@ -21,6 +21,17 @@ export declare class PostgresAdapter implements DatabaseAdapter {
21
21
  private _inTransaction;
22
22
  constructor(config: PostgresConfig | string);
23
23
  /** Connect to PostgreSQL. Must be called before using the adapter. */
24
+ /** ADR-0044 required adapter capability. */
25
+ getDatabaseType(): string;
26
+ /** ADR-0044: readable/writable native boolean. */
27
+ autocommit: boolean;
28
+ /**
29
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
30
+ * multi-row batch by default. A test-only deployment representing one
31
+ * that cannot sets this false so executeMany rejects BEFORE the first
32
+ * write rather than risking partial durability.
33
+ */
34
+ supportsAtomicBatch: boolean;
24
35
  connect(): Promise<void>;
25
36
  private ensureConnected;
26
37
  /** Convert ? placeholders to $1, $2, ... for pg. */
@@ -2,6 +2,24 @@ import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } fro
2
2
  export declare class SQLiteAdapter implements DatabaseAdapter {
3
3
  private db;
4
4
  private _lastInsertId;
5
+ /** ADR-0044: readable/writable native boolean. */
6
+ autocommit: boolean;
7
+ /**
8
+ * ADR-0044 / DBA-P02: every built-in adapter can guarantee an atomic
9
+ * multi-row batch by default. A test-only deployment representing one that
10
+ * cannot (a standalone MongoDB without a replica set is the motivating real
11
+ * case) sets this false so executeMany rejects BEFORE the first write.
12
+ */
13
+ supportsAtomicBatch: boolean;
14
+ /** ADR-0044 required adapter capability. */
15
+ getDatabaseType(): string;
16
+ /**
17
+ * ADR-0044 canonical lifecycle name. A genuine no-op: `node:sqlite` opens
18
+ * the file synchronously in the constructor (see the timeout note below),
19
+ * so by the time a caller could reach connect() the adapter is already
20
+ * connected — repeated calls open no additional physical connection.
21
+ */
22
+ connect(): void;
5
23
  /**
6
24
  * TINA4_DATABASE_CONNECT_TIMEOUT DOES NOT APPLY HERE, deliberately.
7
25
  *
@@ -16,10 +34,7 @@ export declare class SQLiteAdapter implements DatabaseAdapter {
16
34
  */
17
35
  constructor(dbPath: string);
18
36
  execute(sql: string, params?: unknown[]): unknown;
19
- executeMany(sql: string, paramsList: unknown[][]): {
20
- totalAffected: number;
21
- lastId?: number | bigint;
22
- };
37
+ executeMany(sql: string, paramsList: unknown[][]): DatabaseResult;
23
38
  query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
24
39
  fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[];
25
40
  fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
@@ -33,6 +48,10 @@ export declare class SQLiteAdapter implements DatabaseAdapter {
33
48
  getTables(): string[];
34
49
  getColumns(table: string): ColumnInfo[];
35
50
  lastInsertId(): number | bigint | null;
51
+ private _closed;
52
+ /** ADR-0044 (DBA-L02): idempotent — node:sqlite's DatabaseSync.close()
53
+ * throws when called on an already-closed database, so a second close()
54
+ * must not reach it. */
36
55
  close(): void;
37
56
  /**
38
57
  * Atomically increment and return the next value of a tina4_sequences row.
@@ -1,5 +1,4 @@
1
1
  import { QueryBuilder } from "./queryBuilder.js";
2
- import { QueryCache } from "./sqlTranslator.js";
3
2
  import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
4
3
  /**
5
4
  * Convert a snake_case name to camelCase.
@@ -29,23 +28,6 @@ export declare function toDbFieldValue(def: FieldDefinition | undefined, value:
29
28
  * null stays null; a non-decodable string keeps its raw form.
30
29
  */
31
30
  export declare function fromDbFieldValue(def: FieldDefinition | undefined, value: unknown): unknown;
32
- /**
33
- * BaseModel provides instance methods for ORM models.
34
- * Models extend this class and define static properties.
35
- *
36
- * Usage:
37
- * class User extends BaseModel {
38
- * static tableName = "users";
39
- * static fields = { id: { type: "integer", primaryKey: true, autoIncrement: true }, ... };
40
- * static softDelete = true;
41
- * static tableFilter = "active = 1";
42
- * static hasOne = [{ model: "Profile", foreignKey: "user_id" }];
43
- * static hasMany = [{ model: "Post", foreignKey: "author_id" }];
44
- * static _db = "secondary";
45
- * static fieldMapping = { firstName: "first_name", lastName: "last_name" };
46
- * static autoMap = true; // auto-generate fieldMapping from camelCase → snake_case
47
- * }
48
- */
49
31
  export declare class BaseModel {
50
32
  static tableName: string;
51
33
  static fields: Record<string, FieldDefinition>;
@@ -55,7 +37,6 @@ export declare class BaseModel {
55
37
  static hasMany?: RelationshipDefinition[];
56
38
  static belongsTo?: RelationshipDefinition[];
57
39
  static _db?: string;
58
- static _queryCache?: QueryCache;
59
40
  /**
60
41
  * When true, auto-generates fieldMapping entries from camelCase field names
61
42
  * to snake_case DB column names. Explicit fieldMapping entries always win.
@@ -315,7 +296,7 @@ export declare class BaseModel {
315
296
  * Validate this instance's values against the model's field definitions.
316
297
  * Returns an array of error strings (empty array means valid).
317
298
  */
318
- validate(): string[];
299
+ validate(isUpdate?: boolean): string[];
319
300
  /**
320
301
  * Generate and execute CREATE TABLE DDL from the model's field definitions.
321
302
  * Uses the adapter's createTable method if available, otherwise builds SQL directly.
@@ -330,18 +311,36 @@ export declare class BaseModel {
330
311
  */
331
312
  static exists(pkValue: unknown): Promise<boolean>;
332
313
  /**
333
- * Run a raw SQL query with results cached by TTL. Cache is per-model-class.
314
+ * Every table a cached query touches: this model's table plus every FROM/JOIN
315
+ * table in `sql`. A write to any of these busts the entry (CACHE-DEC-01).
316
+ */
317
+ static _cacheTags(sql: string): string[];
318
+ /**
319
+ * Run a raw SQL query with results cached by TTL.
320
+ *
321
+ * Invalidation (CACHE-DEC-01): the entry is tagged by every table the query
322
+ * touches (this model's table plus any FROM/JOIN tables) in ONE process-wide
323
+ * shared cache, so a write through the ORM (save/delete/forceDelete/restore)
324
+ * to ANY of those tables busts it -- including a cross-table JOIN cached on a
325
+ * different model. `ttl <= 0` means NO-CACHE: the query runs and the rows are
326
+ * returned but nothing is stored, so every read hits the database.
334
327
  *
335
328
  * @param sql SQL query string.
336
329
  * @param params Bind parameters.
337
- * @param ttl Cache TTL in seconds (default 60).
330
+ * @param ttl Cache TTL in seconds (default 60; <= 0 = no-cache).
338
331
  * @param limit Max records to return (default 100).
339
332
  * @param offset Records to skip (default 0).
340
333
  * @param include Relationship names to eager-load on cache miss.
341
334
  */
342
335
  static cached<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, sql: string, params?: unknown[], ttl?: number, limit?: number, offset?: number, include?: string[]): Promise<T[]>;
343
336
  /**
344
- * Clear the per-model query cache.
337
+ * Invalidate every cached query that touches this model's table.
338
+ *
339
+ * Tag-scoped, NOT a wholesale flush: a cached JOIN on another model that reads
340
+ * this table is busted too (it carries this table's tag), while a query that
341
+ * never touches this table is left intact. Called after every ORM write
342
+ * (save/delete/forceDelete/restore) so a read-after-write never serves a
343
+ * stale/deleted row (CACHE-DEC-01).
345
344
  */
346
345
  static clearCache(): void;
347
346
  /**
@@ -380,6 +379,11 @@ export declare class BaseModel {
380
379
  hasOne<T extends BaseModel, R extends BaseModel>(this: T, relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R), foreignKey: string): Promise<R | null>;
381
380
  /**
382
381
  * Load has-many related model instances.
382
+ *
383
+ * With no explicit `limit` this returns the WHOLE set (paged internally, like
384
+ * the lazy accessor), never a silent row cap -- so an imperatively-loaded
385
+ * has_many yields the SAME row count as the lazy path. An explicit `limit`
386
+ * still pages.
383
387
  */
384
388
  hasMany<T extends BaseModel, R extends BaseModel>(this: T, relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R), foreignKey: string, limit?: number, offset?: number): Promise<R[]>;
385
389
  /**
@@ -394,10 +398,26 @@ export declare class BaseModel {
394
398
  /**
395
399
  * Process foreignKey fields on every registered model so the cross-model
396
400
  * _fkRegistry (and each model's belongsTo/hasMany) is fully wired regardless
397
- * of which model was used first. Idempotent _processForeignKeys() and
398
- * _applyFkRegistry() both guard against duplicates.
401
+ * of which model was used first, then attach the lazy relationship accessors.
402
+ * Idempotent every step guards against duplicates.
399
403
  */
400
404
  private static _processAllForeignKeys;
405
+ /**
406
+ * REL-NODE-AUTOWIRE-DEAD: attach a lazy-loading accessor for each declared
407
+ * relationship (belongsTo/hasOne/hasMany) on this model's prototype, so
408
+ * `post.author` / `author.posts` resolve on attribute access. The accessor is
409
+ * async (Node lazy load) and caches into `_relCache` — the SAME cache eager
410
+ * loading fills, so `toDict` stays consistent once a relation has been loaded.
411
+ * Reuses the imperative belongsTo()/hasOne() path and the cross-model registry;
412
+ * a soft-deleted child is excluded and the has-many read is uncapped.
413
+ */
414
+ static _wireRelationshipAccessors(): void;
415
+ /**
416
+ * Lazy has-many read for a relationship accessor: excludes soft-deleted
417
+ * children and returns the WHOLE set (adapterQuery is uncapped, so the tail is
418
+ * never lost). Ordered by the child PK for a stable read.
419
+ */
420
+ private static _loadHasManyLazy;
401
421
  /**
402
422
  * Resolve a model class by name from the registry.
403
423
  */