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.
- package/CLAUDE.md +60 -25
- package/package.json +1 -2
- package/packages/cli/dist/bin.js +20620 -18995
- package/packages/cli/src/bin.ts +28 -71
- package/packages/cli/src/commands/migrate.ts +36 -75
- package/packages/cli/src/commands/migrateRollback.ts +10 -1
- package/packages/cli/src/commands/test.ts +92 -21
- package/packages/core/dist/index.js +20459 -18815
- package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
- package/packages/core/src/ai.ts +28 -12
- package/packages/core/src/api.ts +13 -5
- package/packages/core/src/background.ts +9 -3
- package/packages/core/src/devAdmin.ts +135 -20
- package/packages/core/src/dispatchPipeline.ts +185 -1
- package/packages/core/src/docs.ts +33 -5
- package/packages/core/src/env.ts +1 -1
- package/packages/core/src/errorOverlay.ts +39 -48
- package/packages/core/src/fakeData.ts +15 -0
- package/packages/core/src/index.ts +17 -6
- package/packages/core/src/logger.ts +892 -572
- package/packages/core/src/mcp.ts +9 -1
- package/packages/core/src/messenger.ts +31 -4
- package/packages/core/src/middleware.ts +169 -43
- package/packages/core/src/portTakeover.ts +232 -0
- package/packages/core/src/request.ts +57 -8
- package/packages/core/src/response.ts +67 -0
- package/packages/core/src/router.ts +35 -7
- package/packages/core/src/server.ts +450 -190
- package/packages/core/src/static.ts +81 -12
- package/packages/core/src/testClient.ts +126 -137
- package/packages/core/src/testing.ts +16 -12
- package/packages/core/src/types.ts +21 -9
- package/packages/core/src/version.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketBackplane.ts +2 -2
- package/packages/frond/dist/index.js +31 -13
- package/packages/frond/src/engine.ts +39 -7
- package/packages/orm/dist/index.js +10879 -9258
- package/packages/orm/src/adapters/firebird.ts +200 -27
- package/packages/orm/src/adapters/mongodb.ts +160 -10
- package/packages/orm/src/adapters/mssql.ts +38 -11
- package/packages/orm/src/adapters/mysql.ts +24 -1
- package/packages/orm/src/adapters/odbc.ts +127 -29
- package/packages/orm/src/adapters/postgres.ts +18 -0
- package/packages/orm/src/adapters/sqlite.ts +93 -14
- package/packages/orm/src/autoCrud.ts +72 -8
- package/packages/orm/src/baseModel.ts +323 -71
- package/packages/orm/src/cachedDatabase.ts +48 -1
- package/packages/orm/src/database.ts +162 -59
- package/packages/orm/src/fakeData.ts +6 -2
- package/packages/orm/src/index.ts +4 -1
- package/packages/orm/src/migration.ts +95 -52
- package/packages/orm/src/query.ts +16 -4
- package/packages/orm/src/seeder.ts +43 -25
- package/packages/orm/src/sqlTranslator.ts +104 -19
- package/packages/orm/src/types.ts +97 -21
- package/packages/orm/src/validation.ts +5 -1
- package/packages/swagger/dist/index.js +3 -2
- package/packages/swagger/src/generator.ts +19 -4
- package/packages/swagger/src/ui.ts +6 -4
- package/types/cli/src/bin.d.ts +0 -22
- package/types/core/src/api.d.ts +11 -4
- package/types/core/src/background.d.ts +5 -2
- package/types/core/src/devAdmin.d.ts +35 -0
- package/types/core/src/dispatchPipeline.d.ts +41 -1
- package/types/core/src/errorOverlay.d.ts +13 -13
- package/types/core/src/index.d.ts +9 -6
- package/types/core/src/logger.d.ts +111 -185
- package/types/core/src/middleware.d.ts +40 -5
- package/types/core/src/portTakeover.d.ts +50 -0
- package/types/core/src/request.d.ts +15 -0
- package/types/core/src/response.d.ts +29 -0
- package/types/core/src/server.d.ts +92 -0
- package/types/core/src/testClient.d.ts +29 -3
- package/types/core/src/testing.d.ts +16 -12
- package/types/core/src/types.d.ts +21 -9
- package/types/core/src/version.d.ts +11 -0
- package/types/core/src/websocketBackplane.d.ts +1 -1
- package/types/frond/src/engine.d.ts +10 -0
- package/types/orm/src/adapters/firebird.d.ts +61 -2
- package/types/orm/src/adapters/mongodb.d.ts +20 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +11 -0
- package/types/orm/src/adapters/odbc.d.ts +35 -4
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlite.d.ts +23 -4
- package/types/orm/src/baseModel.d.ts +45 -25
- package/types/orm/src/cachedDatabase.d.ts +27 -1
- package/types/orm/src/database.d.ts +56 -6
- package/types/orm/src/index.d.ts +3 -2
- package/types/orm/src/migration.d.ts +23 -5
- package/types/orm/src/query.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +15 -2
- package/types/orm/src/sqlTranslator.d.ts +17 -4
- package/types/orm/src/types.d.ts +75 -16
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
4
|
+
* Attach test expectations to functions and run them all at once.
|
|
5
5
|
*
|
|
6
|
-
* import { tests,
|
|
6
|
+
* import { tests, expectEqual, expectRaises, runAll } from "./testing.js";
|
|
7
7
|
*
|
|
8
8
|
* const add = tests(
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
35
|
-
export declare function
|
|
36
|
-
/**
|
|
37
|
-
export declare function
|
|
38
|
-
/**
|
|
39
|
-
export declare function
|
|
40
|
-
/**
|
|
41
|
-
export declare function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
137
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
398
|
-
*
|
|
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
|
*/
|