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
@@ -144,8 +144,21 @@ export declare class CachedDatabaseAdapter implements DatabaseAdapter {
144
144
  private backendSetRows;
145
145
  private backendGetOne;
146
146
  private backendSetOne;
147
+ /** ADR-0044 required capability — delegates to the wrapped adapter. */
148
+ connect(): void | Promise<void>;
149
+ /** ADR-0044 required capability — delegates to the wrapped adapter. */
150
+ getDatabaseType(): string;
151
+ /**
152
+ * ADR-0044 required capability — a native boolean, readable and writable.
153
+ * A getter/setter pair (not a plain field) so it genuinely delegates to the
154
+ * wrapped adapter rather than drifting out of sync with its real setting.
155
+ */
156
+ get autocommit(): boolean;
157
+ set autocommit(value: boolean);
158
+ get supportsAtomicBatch(): boolean;
159
+ set supportsAtomicBatch(value: boolean);
147
160
  execute(sql: string, params?: unknown[]): unknown;
148
- executeMany(sql: string, paramsList: unknown[][]): {
161
+ executeMany(sql: string, paramsList: unknown[][]): import("./types.js").DatabaseResult | {
149
162
  totalAffected: number;
150
163
  lastId?: number | bigint;
151
164
  };
@@ -173,6 +186,19 @@ export declare class CachedDatabaseAdapter implements DatabaseAdapter {
173
186
  fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], noCache?: boolean): Promise<T | null>;
174
187
  queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
175
188
  executeAsync(sql: string, params?: unknown[]): Promise<unknown>;
189
+ /**
190
+ * ADR-0044: the async passthrough executeMany() itself was missing (unlike
191
+ * its executeAsync/insertAsync siblings above), so adapterExecuteMany()'s
192
+ * `(adapter as any).executeManyAsync` check found nothing on THIS wrapper
193
+ * and fell through to the synchronous executeMany() below — which forwards
194
+ * to the wrapped adapter's OWN sync executeMany(), the throwing "Use
195
+ * executeManyAsync()" stub on every async-native adapter (Postgres/MySQL/
196
+ * MSSQL/Firebird/Mongo). Real bug, caught by executeManyFacadeTxn.test.ts.
197
+ */
198
+ executeManyAsync(sql: string, paramsList: unknown[][]): Promise<import("./types.js").DatabaseResult | {
199
+ totalAffected: number;
200
+ lastId?: number | bigint;
201
+ }>;
176
202
  insertAsync(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseResult>;
177
203
  updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): Promise<DatabaseResult>;
178
204
  deleteAsync(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): Promise<DatabaseResult>;
@@ -31,6 +31,32 @@ export declare function adapterFetch<T = Record<string, unknown>>(adapter: Datab
31
31
  export declare function adapterQuery<T = Record<string, unknown>>(adapter: DatabaseAdapter, sql: string, params?: unknown[]): Promise<T[]>;
32
32
  export declare function adapterFetchOne<T = Record<string, unknown>>(adapter: DatabaseAdapter, sql: string, params?: unknown[]): Promise<T | null>;
33
33
  export declare function adapterExecute(adapter: DatabaseAdapter, sql: string, params?: unknown[]): Promise<unknown>;
34
+ /**
35
+ * ADR-0044: the adapter-level batch primitive, called exactly once by
36
+ * Database#executeMany (never looped) — one aggregate DatabaseResult for the
37
+ * whole batch. Normalises whichever native shape an adapter returns: SQLite's
38
+ * `{success, affectedRows, lastId}` (the shared write shape already used by
39
+ * insert/update/delete) or an async-native adapter's `{totalAffected, lastId}`
40
+ * (pre-ADR-0044 shape, not yet unified per-adapter — normalised HERE at the
41
+ * one chokepoint every public write flows through, so the facade's contract
42
+ * is uniform without touching each of the five adapter files' internals).
43
+ */
44
+ export declare function adapterExecuteMany(adapter: DatabaseAdapter, sql: string, paramsList: unknown[][]): Promise<import("./types.js").DatabaseResult>;
45
+ /**
46
+ * Insert one row (or a batch) through the adapter's OWN native insert path
47
+ * (each adapter's `buildInsert()`/`Dialect`, feature 3's SQL builder
48
+ * consolidation) instead of hand-built SQL. This is the ONLY correct way to
49
+ * insert into a caller-named table/columns: Firebird's Dialect quotes only
50
+ * when it has to (an unquoted identifier folds to UPPERCASE, so quoting a
51
+ * lower-case name makes it unfindable — SQL error -204 "Table unknown"),
52
+ * while PostgreSQL/MSSQL/SQLite quote unconditionally. A caller that hand-
53
+ * quotes with one fixed style (e.g. always `"col"`) works on three engines
54
+ * and silently breaks on the fourth. seedTable()/seedOrm() route through
55
+ * this so their engine portability matches insert()/insertAsync()'s, which
56
+ * the write-path + provider contract suites already prove on all four real
57
+ * engines (features 9/10/11/12).
58
+ */
59
+ export declare function adapterInsert(adapter: DatabaseAdapter, table: string, data: Record<string, unknown>): Promise<unknown>;
34
60
  export declare function adapterStartTransaction(adapter: DatabaseAdapter): Promise<void>;
35
61
  export declare function adapterCommit(adapter: DatabaseAdapter): Promise<void>;
36
62
  export declare function adapterRollback(adapter: DatabaseAdapter): Promise<void>;
@@ -394,7 +420,19 @@ export declare class Database {
394
420
  * `try/catch` and convert, rather than testing the return value.
395
421
  */
396
422
  execute(sql: string, params?: unknown[]): Promise<boolean | unknown>;
397
- /** Insert one row (object) or a batch of rows (array of objects) into a table. */
423
+ /**
424
+ * Insert one row (object) or a batch of rows (array of objects) into a table.
425
+ *
426
+ * FAIL LOUD, matching update()/delete()/truncate(): a real driver failure
427
+ * (e.g. a NOT NULL / UNIQUE constraint violation) throws rather than
428
+ * resolving to `{ success: false, affectedRows: 0 }`. The async adapters
429
+ * (Postgres/MySQL/MSSQL/Firebird) already throw directly from insertAsync();
430
+ * SQLiteAdapter.insert() is the one adapter that CATCHES the driver error
431
+ * and returns a `{ success: false, error }` result instead (its own
432
+ * documented contract for the synchronous path) — assertWrote is what
433
+ * converts that into the same thrown DatabaseException every other engine
434
+ * already produces, exactly as it already does for update/delete/truncate.
435
+ */
398
436
  insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<DatabaseWriteResult>;
399
437
  /**
400
438
  * The table's primary-key column, introspected once and cached.
@@ -487,16 +525,28 @@ export declare class Database {
487
525
  nullable?: boolean;
488
526
  default?: unknown;
489
527
  primaryKey?: boolean;
528
+ primaryKeyPosition?: number | null;
490
529
  }[]>;
491
530
  /**
492
- * Execute a SQL statement with multiple parameter sets (batch insert/update).
493
- * Wraps all executions in a single transaction for atomicity and performance.
531
+ * Execute a SQL statement with multiple parameter sets as ONE aggregate
532
+ * batch (ADR-0044). Wraps the single delegated call in a transaction for
533
+ * atomicity — never loops #execute itself.
534
+ *
535
+ * BREAKING (ADR-0044, pre-3.14.0): used to return one result PER ROW
536
+ * (`unknown[]`, callers indexed into it) built by the FACADE looping
537
+ * execute()/adapterExecute() per chunk or per row. It now delegates to the
538
+ * adapter's OWN executeMany/executeManyAsync exactly once (DBA-D02: facade
539
+ * delegates once, never a facade row loop) and returns the SAME shared
540
+ * DatabaseResult shape insert()/update()/delete() already return
541
+ * ({success, affectedRows, lastId}) — affectedRows is the total ROW count,
542
+ * never the number of chunks/statements. A caller that indexed into the old
543
+ * per-row array must switch to inspecting the aggregate result.
494
544
  *
495
545
  * @param sql - The SQL statement with parameter placeholders.
496
- * @param paramSets - Array of parameter arrays, one per execution.
497
- * @returns Array of results from each execution.
546
+ * @param paramSets - Array of parameter arrays, one per row.
547
+ * @returns The aggregate DatabaseResult for the whole batch.
498
548
  */
499
- executeMany(sql: string, paramSets?: unknown[][]): Promise<unknown[]>;
549
+ executeMany(sql: string, paramSets?: unknown[][]): Promise<DatabaseWriteResult>;
500
550
  /** Return the last execute() error message, or null. */
501
551
  getError(): string | null;
502
552
  /**
@@ -1,14 +1,15 @@
1
1
  export type { FieldType, FieldDefinition, ModelDefinition, DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, QueryOptions, RelationshipDefinition, } from "./types.js";
2
+ export { REQUIRED_ADAPTER_CAPABILITIES, NOT_REQUIRED_ON_ADAPTER } from "./types.js";
2
3
  export { DatabaseResult } from "./databaseResult.js";
3
4
  export type { ColumnInfoResult } from "./databaseResult.js";
4
5
  export { Database, initDatabase, getAdapter, setAdapter, bindDatabase, createAdapterFromUrl, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons, wrapWithCache, resetRequestCaches } from "./database.js";
5
- export { adapterFetch, adapterQuery, adapterFetchOne, adapterExecute, adapterStartTransaction, adapterCommit, adapterRollback, adapterTableExists, adapterTables, adapterColumns, adapterCreateTable, extractLastInsertId, } from "./database.js";
6
+ export { adapterFetch, adapterQuery, adapterFetchOne, adapterExecute, adapterInsert, adapterStartTransaction, adapterCommit, adapterRollback, adapterTableExists, adapterTables, adapterColumns, adapterCreateTable, extractLastInsertId, } from "./database.js";
6
7
  export type { DatabaseConfig } from "./database.js";
7
8
  export { DatabaseUrl, redactCredentials } from "./databaseUrl.js";
8
9
  export type { DatabaseEngine } from "./databaseUrl.js";
9
10
  export { discoverModels } from "./model.js";
10
11
  export type { DiscoveredModel } from "./model.js";
11
- export { syncModels, ensureMigrationTable, getNextBatch, isMigrationApplied, recordMigration, applyMigration, rollback, getAppliedMigrations, getLastBatchMigrations, removeMigrationRecord, migrate, createMigration, status, Migration, splitStatements, parseSetTerm, normalizeQuotes, sortMigrationFiles, shouldSkipCreateTable, } from "./migration.js";
12
+ export { syncModels, ensureMigrationTable, getNextBatch, isMigrationApplied, recordMigration, applyMigration, rollback, getAppliedMigrations, getLastBatchMigrations, removeMigrationRecord, migrate, createMigration, status, Migration, splitStatements, parseSetTerm, normalizeQuotes, sortMigrationFiles, shouldSkipCreateTable, shouldSkipForFirebird, } from "./migration.js";
12
13
  export type { MigrationResult, MigrationStatus } from "./migration.js";
13
14
  export { AutoCrud, generateCrudRoutes, crudEligibleModels } from "./autoCrud.js";
14
15
  export type { AutoCrudOptions } from "./autoCrud.js";
@@ -1,5 +1,14 @@
1
1
  import type { DatabaseAdapter } from "./types.js";
2
2
  import type { DiscoveredModel } from "./model.js";
3
+ /**
4
+ * If stmt is an ALTER TABLE ... ADD on Firebird and the column already exists,
5
+ * returns a skip reason string. Returns null if the statement should execute normally.
6
+ *
7
+ * Exported (like its shouldSkipCreateTable sibling) so it can be driven
8
+ * directly against a REAL Firebird connection in
9
+ * test/migrationContract.test.ts -- no fake adapter needed.
10
+ */
11
+ export declare function shouldSkipForFirebird(db: DatabaseAdapter, stmt: string): Promise<string | null>;
3
12
  /**
4
13
  * Make CREATE TABLE idempotent on engines lacking IF NOT EXISTS.
5
14
  *
@@ -53,17 +62,26 @@ export declare function removeMigrationRecord(name: string): Promise<void>;
53
62
  /**
54
63
  * Rollback the last batch of migrations using .down.sql files.
55
64
  *
56
- * For each migration in the last batch (in reverse order):
57
- * 1. Looks for a corresponding .down.sql file on disk
58
- * 2. If found, reads and executes the SQL statements
59
- * 3. If not found, logs a warning
60
- * 4. Deletes the tracking record either way
65
+ * FAIL-SAFE (MIG-DEC-02, reuses the Python reference model): for each
66
+ * migration in the last batch (in reverse order), the down artifact must
67
+ * actually run before the tracking record is removed. A MISSING .down.sql
68
+ * (or, on the legacy Map API, no registered down function) or a FAILING down
69
+ * statement now THROWS instead of logging a warning/error and still deleting
70
+ * the record — the old behaviour was the exact MIG-ROLLBACK-DROPS-LEDGER bug:
71
+ * the schema stayed applied but the ledger row vanished, silently untracked.
72
+ * The DELETE runs inside the SAME transaction as the down statements, so a
73
+ * partially-executed down (some statements ran, a later one failed) rolls
74
+ * back too — no half-reversed schema left behind either.
61
75
  *
62
76
  * @param migrationsDir - Directory containing migration files (default: "migrations")
63
77
  * @param delimiter - SQL statement delimiter (default: ";")
64
78
  * @returns Array of the down-migration files that were run, e.g.
65
79
  * "000001_create_users.down.sql". (The legacy down-FUNCTION Map API returns the
66
80
  * bare migration name instead, since no .down.sql file is involved there.)
81
+ * @throws When a migration in the batch has no down artifact, or its down
82
+ * statement(s) fail — the batch stops at that migration; earlier
83
+ * migrations in the SAME call that already rolled back stay rolled back
84
+ * (each is its own transaction).
67
85
  *
68
86
  * NOTE on return form (intentional, cross-framework): migration return values reflect
69
87
  * WHAT each method acted on, so the forms differ by method and that is by design (not
@@ -10,5 +10,8 @@ export declare function buildQuery(tableName: string, options: QueryOptions, ext
10
10
  sql: string;
11
11
  countSql: string;
12
12
  params: unknown[];
13
+ limit: number;
14
+ offset: number;
15
+ page: number;
13
16
  };
14
17
  export declare function parseQueryString(query: Record<string, string>): QueryOptions;
@@ -21,7 +21,18 @@ export interface SeedOptions {
21
21
  overrides?: Record<string, unknown>;
22
22
  /** Delete every existing row in the target before seeding (P2). */
23
23
  clear?: boolean;
24
- /** PRNG seed for reproducible FakeData output (P3). */
24
+ /**
25
+ * PRNG seed for reproducible FakeData output (P3). Honoured by seedOrm and
26
+ * seedModels, which build and seed their own FakeData internally.
27
+ *
28
+ * NOT honoured by seedTable (SEED-TABLE-SEED-INERT, SEED-DEC-01, ratified
29
+ * 2026-08-11 — same principle as the no-op ForeignKeyField on_delete):
30
+ * seedTable has no generators of its own to seed — fieldMap callables are
31
+ * opaque — so this used to be a silent no-op there. Passing it to seedTable
32
+ * now THROWS instead. Build your own `new FakeData(seed)` and close over it
33
+ * in fieldMap: `const fake = new FakeData(42); seedTable(db, table, count,
34
+ * { name: () => fake.name() })`.
35
+ */
25
36
  seed?: number;
26
37
  /** Re-raise on the first failed row instead of skipping it (P1). */
27
38
  strict?: boolean;
@@ -63,8 +74,10 @@ export declare function autoFieldMap(db: DatabaseAdapter, table: string, fake?:
63
74
  * (or a static value). If not provided, no rows are inserted.
64
75
  * @param overrides - (legacy positional) Static values applied to every row.
65
76
  * Prefer `opts.overrides`.
66
- * @param opts - Seed options: `{ overrides, clear, seed, strict }`.
77
+ * @param opts - Seed options: `{ overrides, clear, strict }`. `opts.seed` is
78
+ * NOT honoured here (see {@link SeedOptions.seed}) and throws if supplied.
67
79
  * @returns A SeedSummary `{ seeded, failed, errors }`.
80
+ * @throws {Error} If `opts.seed` is defined (SEED-TABLE-SEED-INERT removal).
68
81
  *
69
82
  * @example
70
83
  * const fake = new FakeData();
@@ -31,18 +31,31 @@ export declare class SQLTranslator {
31
31
  * Does NOT convert if OFFSET is present (TOP doesn't support it).
32
32
  */
33
33
  static limitToTop(sql: string): string;
34
+ /** Replace string literals, quoted identifiers and comments with opaque
35
+ * `\x00N\x00` tokens (doubled-quote escapes handled). */
36
+ private static maskLiterals;
37
+ /** Inverse of maskLiterals. */
38
+ private static restoreLiterals;
39
+ private static readonly PRIMARY;
34
40
  /**
35
- * Convert || concatenation to CONCAT() for MySQL/MSSQL.
41
+ * Convert `||` string concatenation to `CONCAT(...)` for MySQL/MSSQL.
36
42
  *
37
- * 'a' || 'b' || 'c' → CONCAT('a', 'b', 'c')
43
+ * Rewrites ONLY `||` operators joining expression operands OUTSIDE any string
44
+ * literal or comment, and only the operand chain — never the whole statement:
45
+ * SELECT a || b FROM t -> SELECT CONCAT(a, b) FROM t
46
+ * WHERE data = 'a||b' -> WHERE data = 'a||b' (literal untouched)
38
47
  */
39
48
  static concatPipesToFunc(sql: string): string;
40
49
  /**
41
- * Convert TRUE/FALSE to 1/0 for engines without boolean type (Firebird).
50
+ * Convert bare TRUE/FALSE to 1/0 for engines without a boolean type. A
51
+ * TRUE/FALSE INSIDE a string literal is data and is left untouched
52
+ * (`WHERE label = 'TRUE'` is preserved).
42
53
  */
43
54
  static booleanToInt(sql: string): string;
44
55
  /**
45
- * Convert ILIKE to LOWER() LIKE LOWER() for engines without ILIKE.
56
+ * Convert `col ILIKE pattern` to `LOWER(col) LIKE LOWER(pattern)` for engines
57
+ * without ILIKE. The pattern operand is captured whole (a multi-word
58
+ * `'%two words%'` survives) and an ILIKE INSIDE a string literal is untouched.
46
59
  */
47
60
  static ilikeToLike(sql: string): string;
48
61
  /**
@@ -1,4 +1,4 @@
1
- export type FieldType = "string" | "integer" | "number" | "numeric" | "boolean" | "datetime" | "text" | "json" | "foreignKey";
1
+ export type FieldType = "string" | "integer" | "number" | "numeric" | "decimal" | "boolean" | "datetime" | "text" | "json" | "foreignKey";
2
2
  export interface FieldDefinition {
3
3
  type: FieldType;
4
4
  primaryKey?: boolean;
@@ -10,6 +10,15 @@ export interface FieldDefinition {
10
10
  min?: number;
11
11
  max?: number;
12
12
  pattern?: string;
13
+ /**
14
+ * For type "decimal": the fixed precision/scale of a real DECIMAL(p, s)
15
+ * column. `number`/`numeric` stay a floating type (the documented money
16
+ * guidance); a `decimal` field keeps the declared scale in the COLUMN, so
17
+ * createTable emits DECIMAL(precision, scale) — identical on
18
+ * PostgreSQL/MySQL/MSSQL/Firebird/SQLite. Default 10 / 2 when omitted.
19
+ */
20
+ precision?: number;
21
+ scale?: number;
13
22
  /** For type "foreignKey": the referenced model name (string) */
14
23
  references?: string;
15
24
  /** For type "foreignKey": override the has-many property name on the referenced model */
@@ -50,6 +59,12 @@ export interface ColumnInfo {
50
59
  nullable?: boolean;
51
60
  default?: unknown;
52
61
  primaryKey?: boolean;
62
+ /**
63
+ * ADR-0044 amendment (Feature 5 Decision 7, 2026-08-10): null for a
64
+ * non-key column; for a composite key this is the 1-based DECLARED
65
+ * PRIMARY KEY (...) order, not table-column order.
66
+ */
67
+ primaryKeyPosition?: number | null;
53
68
  }
54
69
  export interface DatabaseResult {
55
70
  success: boolean;
@@ -57,44 +72,88 @@ export interface DatabaseResult {
57
72
  lastId?: number | bigint | string;
58
73
  error?: string;
59
74
  }
75
+ /**
76
+ * ADR-0044 (feature 3, plan/v3/fixtures/adapter_contract.json): the exact
77
+ * fourteen adapter capabilities every DatabaseAdapter implementation must
78
+ * provide (DBA-S01). Kept as data so the conformance suite can check it
79
+ * without a second hand-maintained copy of the list.
80
+ */
81
+ export declare const REQUIRED_ADAPTER_CAPABILITIES: readonly ["connect", "close", "getDatabaseType", "execute", "executeMany", "fetch", "fetchOne", "startTransaction", "commit", "rollback", "autocommit", "getTables", "getColumns", "tableExists"];
82
+ /**
83
+ * ADR-0044 NOT_REQUIRED_ON_ADAPTER (DBA-S03): engine-neutral composition that
84
+ * the adapter CONTRACT does not require. Node keeps these as REQUIRED
85
+ * TypeScript interface members anyway (unlike Python/PHP/Ruby's stricter
86
+ * runtime reflection) because `database.ts`/`cachedDatabase.ts` call them
87
+ * directly at dozens of sites with no optional-chaining guard, so making them
88
+ * TS-optional ripples into a much larger refactor than this pass covers;
89
+ * this constant records the ADR's INTENT for the conformance suite to check
90
+ * even though the compiler will not enforce their absence.
91
+ */
92
+ export declare const NOT_REQUIRED_ON_ADAPTER: readonly ["query", "insert", "update", "delete", "truncate", "fetchAll", "createTable", "addColumn", "lastInsertId", "error", "sqlTranslation"];
60
93
  export interface DatabaseAdapter {
94
+ /** Connect (ADR-0044 canonical lifecycle name). May be sync or async. */
95
+ connect(): void | Promise<void>;
96
+ /** Return the canonical, credential-free engine name ("sqlite", "postgres", ...). */
97
+ getDatabaseType(): string;
61
98
  /** Execute a statement (INSERT, UPDATE, DELETE, DDL). */
62
99
  execute(sql: string, params?: unknown[]): unknown;
63
- /** Execute a single SQL statement with multiple parameter sets (batch). */
64
- executeMany(sql: string, paramsList: unknown[][]): {
100
+ /**
101
+ * Execute a single SQL statement with multiple parameter sets as ONE
102
+ * aggregate result (ADR-0044). The shared write shape `DatabaseResult`
103
+ * ({success, affectedRows, lastId?}) is the target; the pre-ADR-0044 async
104
+ * adapters (Postgres/MySQL/MSSQL/Firebird/Mongo) still return their
105
+ * original `{totalAffected, lastId?}` shape internally today — the union
106
+ * covers both while `adapterExecuteMany()` in database.ts normalises
107
+ * whichever shape it receives at the one chokepoint every public batch
108
+ * write flows through.
109
+ */
110
+ executeMany(sql: string, paramsList: unknown[][]): DatabaseResult | {
65
111
  totalAffected: number;
66
112
  lastId?: number | bigint;
67
113
  };
68
- /** Query rows. */
69
- query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
70
- /** Fetch rows with optional pagination (limit/skip). */
114
+ /** Fetch rows with optional pagination (limit/skip). Native list, no envelope. */
71
115
  fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[];
72
- /** Fetch a single row or null. */
116
+ /** Fetch a single row or null. No pagination count probe. */
73
117
  fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
74
- /** Insert one or more rows into a table, returns result with lastId. */
75
- insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
76
- /** Update rows in a table matching filter (object or string WHERE), returns affected row count. */
77
- update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult;
78
- /** Delete rows from a table matching filter (object, string WHERE, or array of objects). */
79
- delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult;
80
118
  /** Start a transaction. */
81
119
  startTransaction(): void;
82
120
  /** Commit the current transaction. */
83
121
  commit(): void;
84
122
  /** Rollback the current transaction. */
85
123
  rollback(): void;
124
+ /**
125
+ * Native boolean, readable and writable. A plain field is the idiomatic JS
126
+ * shape for "readable and writable" — no getter/setter ceremony needed.
127
+ */
128
+ autocommit: boolean;
129
+ /**
130
+ * ADR-0044 / DBA-P02: whether this adapter's deployment can guarantee an
131
+ * atomic multi-row batch write. Every built-in adapter defaults true (see
132
+ * each adapter's field initializer); a deployment that genuinely cannot (a
133
+ * standalone MongoDB without a replica set is the motivating real case)
134
+ * sets this false so executeMany rejects BEFORE the first write.
135
+ */
136
+ supportsAtomicBatch?: boolean;
86
137
  /** List all tables in the database. */
87
138
  getTables(): string[];
88
139
  /** List columns with types for a table. */
89
140
  getColumns(table: string): ColumnInfo[];
90
- /** Get the last inserted id (auto-increment integer, or a UUID/string PK). */
91
- lastInsertId(): number | bigint | string | null;
92
141
  /** Close the connection. */
93
142
  close(): void;
94
143
  /** Check if a table exists. */
95
144
  tableExists(name: string): boolean;
96
- /** Create a table from field definitions. */
145
+ /** Insert one or more rows into a table, returns result with lastId. */
146
+ insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
147
+ /** Update rows in a table matching filter (object or string WHERE), returns affected row count. */
148
+ update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult;
149
+ /** Delete rows from a table matching filter (object, string WHERE, or array of objects). */
150
+ delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult;
151
+ /** Query rows (legacy convenience, superseded by fetch). */
152
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
153
+ /** Create a table from field definitions (legacy, DDL composition lives above the adapter). */
97
154
  createTable(name: string, columns: Record<string, FieldDefinition>): void;
155
+ /** Get the last inserted id (legacy — prefer DatabaseResult.lastId from execute/executeMany). */
156
+ lastInsertId(): number | bigint | string | null;
98
157
  /** Get raw column info (legacy, used by migration). */
99
158
  getTableColumns?(name: string): Array<{
100
159
  name: string;
@@ -1,122 +0,0 @@
1
- /**
2
- * Tests for errorOverlay module.
3
- */
4
-
5
- import { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorOverlay.js";
6
-
7
- let passed = 0;
8
- let failed = 0;
9
-
10
- function assert(condition: boolean, message: string): void {
11
- if (condition) {
12
- passed++;
13
- } else {
14
- failed++;
15
- console.error(` FAIL: ${message}`);
16
- }
17
- }
18
-
19
- function assertIncludes(html: string, needle: string, label: string): void {
20
- assert(html.includes(needle), `${label}: expected HTML to include "${needle}"`);
21
- }
22
-
23
- function assertNotIncludes(html: string, needle: string, label: string): void {
24
- assert(!html.includes(needle), `${label}: expected HTML NOT to include "${needle}"`);
25
- }
26
-
27
- function makeError(): Error {
28
- try {
29
- throw new TypeError("something broke");
30
- } catch (e) {
31
- return e as Error;
32
- }
33
- }
34
-
35
- // ── renderErrorOverlay ──
36
-
37
- const err = makeError();
38
- const html = renderErrorOverlay(err);
39
-
40
- assert(typeof html === "string", "returns a string");
41
- assert(html.startsWith("<!DOCTYPE html>"), "starts with DOCTYPE");
42
- assertIncludes(html, "TypeError", "contains exception type");
43
- assertIncludes(html, "something broke", "contains exception message");
44
- assertIncludes(html, "errorOverlay.test", "contains file path");
45
- assertIncludes(html, "&#x25b6;", "contains error line marker");
46
- assertIncludes(html, "Stack Trace", "contains stack trace section");
47
- assertIncludes(html, "<details", "uses details element");
48
- assertIncludes(html, "open", "stack trace open by default");
49
- assertIncludes(html, "Environment", "contains environment section");
50
- assertIncludes(html, "Tina4 Node.js", "contains framework name");
51
- assertIncludes(html, "TINA4_DEBUG", "contains debug reference");
52
-
53
- // With request
54
- const htmlWithReq = renderErrorOverlay(err, {
55
- method: "GET",
56
- url: "/api/users",
57
- headers: { host: "localhost" },
58
- });
59
- assertIncludes(htmlWithReq, "GET", "request method shown");
60
- assertIncludes(htmlWithReq, "/api/users", "request URL shown");
61
- assertIncludes(htmlWithReq, "localhost", "request header shown");
62
- assertIncludes(htmlWithReq, "Request Details", "request section present");
63
-
64
- // Without request
65
- assertNotIncludes(html, "Request Details", "no request section when no request");
66
-
67
- // XSS escaping
68
- const xssErr = new Error('<script>alert("xss")</script>');
69
- const xssHtml = renderErrorOverlay(xssErr);
70
- assertNotIncludes(xssHtml, "<script>", "escapes script tags");
71
- assertIncludes(xssHtml, "&lt;script&gt;", "HTML-encodes script tags");
72
-
73
- // ── renderProductionError ──
74
-
75
- const prodHtml = renderProductionError();
76
- assert(prodHtml.startsWith("<!DOCTYPE html>"), "production: starts with DOCTYPE");
77
- assertIncludes(prodHtml, "500", "production: contains 500");
78
- assertIncludes(prodHtml, "Internal Server Error", "production: default message");
79
- assertNotIncludes(prodHtml, "Stack Trace", "production: no stack trace");
80
-
81
- const prod404 = renderProductionError(404, "Not Found");
82
- assertIncludes(prod404, "404", "production 404: contains code");
83
- assertIncludes(prod404, "Not Found", "production 404: contains message");
84
-
85
- // ── isDebugMode ──
86
-
87
- const origDebug = process.env.TINA4_DEBUG;
88
-
89
- process.env.TINA4_DEBUG = "true";
90
- assert(isDebugMode() === true, "isDebugMode: true => true");
91
-
92
- process.env.TINA4_DEBUG = "false";
93
- assert(isDebugMode() === false, "isDebugMode: false => false");
94
-
95
- process.env.TINA4_DEBUG = "TRUE";
96
- assert(isDebugMode() === true, "isDebugMode: TRUE (uppercase) => true (case insensitive)");
97
-
98
- process.env.TINA4_DEBUG = "1";
99
- assert(isDebugMode() === true, "isDebugMode: 1 => true");
100
-
101
- process.env.TINA4_DEBUG = "yes";
102
- assert(isDebugMode() === true, "isDebugMode: yes => true");
103
-
104
- process.env.TINA4_DEBUG = "on";
105
- assert(isDebugMode() === true, "isDebugMode: on => true");
106
-
107
- process.env.TINA4_DEBUG = "0";
108
- assert(isDebugMode() === false, "isDebugMode: 0 => false");
109
-
110
- delete process.env.TINA4_DEBUG;
111
- assert(isDebugMode() === false, "isDebugMode: unset => false");
112
-
113
- // Restore
114
- if (origDebug !== undefined) {
115
- process.env.TINA4_DEBUG = origDebug;
116
- }
117
-
118
- // ── Summary ──
119
- console.log(`\nerrorOverlay tests: ${passed} passed, ${failed} failed`);
120
- if (failed > 0) {
121
- process.exit(1);
122
- }