tina4-nodejs 3.13.92 → 3.13.95
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 +170 -28
- package/README.md +2 -2
- package/package.json +13 -9
- package/packages/cli/dist/bin.js +33126 -30055
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +33062 -29908
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/devMailbox.ts +20 -44
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +7 -6
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +81 -13
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +6 -9
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +751 -414
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/childError.ts +72 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -143
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/frond/dist/index.js +74 -31
- package/packages/frond/src/engine.ts +99 -33
- package/packages/orm/dist/index.js +26554 -23400
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +64 -25
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +338 -198
- package/packages/orm/src/databaseResult.ts +65 -13
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -3
- package/packages/orm/src/migration.ts +18 -3
- package/packages/orm/src/queryBuilder.ts +38 -4
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +15 -4
- package/types/cli/src/bin.d.ts +92 -0
- package/types/cli/src/commands/build.d.ts +2 -0
- package/types/cli/src/commands/generate.d.ts +47 -0
- package/types/cli/src/commands/init.d.ts +1 -0
- package/types/cli/src/commands/metrics.d.ts +6 -0
- package/types/cli/src/commands/migrate.d.ts +1 -0
- package/types/cli/src/commands/migrateCreate.d.ts +1 -0
- package/types/cli/src/commands/migrateRollback.d.ts +1 -0
- package/types/cli/src/commands/migrateStatus.d.ts +1 -0
- package/types/cli/src/commands/queue.d.ts +20 -0
- package/types/cli/src/commands/routes.d.ts +1 -0
- package/types/cli/src/commands/seed.d.ts +1 -0
- package/types/cli/src/commands/serve.d.ts +6 -0
- package/types/cli/src/commands/test.d.ts +1 -0
- package/types/core/src/ai.d.ts +64 -0
- package/types/core/src/api.d.ts +262 -0
- package/types/core/src/auth.d.ts +177 -0
- package/types/core/src/authGate.d.ts +20 -0
- package/types/core/src/background.d.ts +34 -0
- package/types/core/src/cache.d.ts +163 -0
- package/types/core/src/constants.d.ts +38 -0
- package/types/core/src/container.d.ts +44 -0
- package/types/core/src/context/chunker.d.ts +31 -0
- package/types/core/src/context/index.d.ts +93 -0
- package/types/core/src/devAdmin.d.ts +179 -0
- package/types/core/src/devMailbox.d.ts +54 -0
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/docs.d.ts +141 -0
- package/types/core/src/docsAutoDiscovery.d.ts +6 -0
- package/types/core/src/dotenv.d.ts +87 -0
- package/types/core/src/env.d.ts +28 -0
- package/types/core/src/errorOverlay.d.ts +36 -0
- package/types/core/src/events.d.ts +75 -0
- package/types/core/src/fakeData.d.ts +55 -0
- package/types/core/src/feedback.d.ts +90 -0
- package/types/core/src/graphql.d.ts +207 -0
- package/types/core/src/health.d.ts +22 -0
- package/types/core/src/htmlElement.d.ts +75 -0
- package/types/core/src/i18n.d.ts +37 -0
- package/types/core/src/index.d.ts +92 -0
- package/types/core/src/job.d.ts +39 -0
- package/types/core/src/logger.d.ts +200 -0
- package/types/core/src/mcp.d.ts +248 -0
- package/types/core/src/messenger.d.ts +191 -0
- package/types/core/src/metrics.d.ts +41 -0
- package/types/core/src/middleware.d.ts +330 -0
- package/types/core/src/mqtt.d.ts +257 -0
- package/types/core/src/mqttMessage.d.ts +67 -0
- package/types/core/src/plan.d.ts +96 -0
- package/types/core/src/projectIndex.d.ts +56 -0
- package/types/core/src/queue.d.ts +268 -0
- package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
- package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
- package/types/core/src/rateLimiter.d.ts +49 -0
- package/types/core/src/request.d.ts +25 -0
- package/types/core/src/response.d.ts +28 -0
- package/types/core/src/routeDiscovery.d.ts +12 -0
- package/types/core/src/router.d.ts +366 -0
- package/types/core/src/scss.d.ts +19 -0
- package/types/core/src/server.d.ts +146 -0
- package/types/core/src/service.d.ts +115 -0
- package/types/core/src/session.d.ts +341 -0
- package/types/core/src/sessionHandlers/childError.d.ts +34 -0
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
- package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
- package/types/core/src/static.d.ts +2 -0
- package/types/core/src/test.d.ts +94 -0
- package/types/core/src/testClient.d.ts +36 -0
- package/types/core/src/testing.d.ts +58 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +242 -0
- package/types/core/src/validator.d.ts +52 -0
- package/types/core/src/websocket.d.ts +402 -0
- package/types/core/src/websocketBackplane.d.ts +166 -0
- package/types/core/src/websocketConnection.d.ts +54 -0
- package/types/core/src/wsdl.d.ts +101 -0
- package/types/frond/src/engine.d.ts +263 -0
- package/types/frond/src/index.d.ts +2 -0
- package/types/orm/src/adapters/firebird.d.ts +183 -0
- package/types/orm/src/adapters/mongodb.d.ts +81 -0
- package/types/orm/src/adapters/mssql.d.ts +77 -0
- package/types/orm/src/adapters/mysql.d.ts +67 -0
- package/types/orm/src/adapters/odbc.d.ts +94 -0
- package/types/orm/src/adapters/postgres.d.ts +86 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +68 -0
- package/types/orm/src/autoCrud.d.ts +73 -0
- package/types/orm/src/baseModel.d.ts +427 -0
- package/types/orm/src/cachedDatabase.d.ts +190 -0
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +655 -0
- package/types/orm/src/databaseResult.d.ts +109 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +241 -0
- package/types/orm/src/fakeData.d.ts +22 -0
- package/types/orm/src/index.d.ts +43 -0
- package/types/orm/src/migration.d.ts +275 -0
- package/types/orm/src/model.d.ts +7 -0
- package/types/orm/src/query.d.ts +14 -0
- package/types/orm/src/queryBuilder.d.ts +193 -0
- package/types/orm/src/realtime/index.d.ts +7 -0
- package/types/orm/src/realtime/models/attachment.d.ts +43 -0
- package/types/orm/src/realtime/models/channel.d.ts +32 -0
- package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
- package/types/orm/src/realtime/models/message.d.ts +36 -0
- package/types/orm/src/realtime/models/workspace.d.ts +26 -0
- package/types/orm/src/realtime/realtime.d.ts +24 -0
- package/types/orm/src/realtime/storage.d.ts +61 -0
- package/types/orm/src/seeder.d.ts +118 -0
- package/types/orm/src/sqlTranslator.d.ts +258 -0
- package/types/orm/src/types.d.ts +148 -0
- package/types/orm/src/validation.d.ts +6 -0
- package/types/swagger/src/generator.d.ts +46 -0
- package/types/swagger/src/index.d.ts +2 -0
- package/types/swagger/src/ui.d.ts +11 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
|
@@ -21,7 +21,9 @@ export {
|
|
|
21
21
|
adapterTableExists, adapterTables, adapterColumns, adapterCreateTable,
|
|
22
22
|
extractLastInsertId,
|
|
23
23
|
} from "./database.js";
|
|
24
|
-
export type { DatabaseConfig
|
|
24
|
+
export type { DatabaseConfig } from "./database.js";
|
|
25
|
+
export { DatabaseUrl, redactCredentials } from "./databaseUrl.js";
|
|
26
|
+
export type { DatabaseEngine } from "./databaseUrl.js";
|
|
25
27
|
export { discoverModels } from "./model.js";
|
|
26
28
|
export type { DiscoveredModel } from "./model.js";
|
|
27
29
|
export {
|
|
@@ -54,6 +56,14 @@ export type { ValidationError } from "./validation.js";
|
|
|
54
56
|
export { BaseModel, snakeToCamel, camelToSnake } from "./baseModel.js";
|
|
55
57
|
export { QueryBuilder } from "./queryBuilder.js";
|
|
56
58
|
export { SQLTranslator, QueryCache } from "./sqlTranslator.js";
|
|
59
|
+
export {
|
|
60
|
+
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
61
|
+
CONNECT_TIMEOUT_TOLERANCE_MS,
|
|
62
|
+
connectTimeoutMillis,
|
|
63
|
+
driverConnectTimeoutMillis,
|
|
64
|
+
connectTarget,
|
|
65
|
+
withConnectTimeout,
|
|
66
|
+
} from "./connectTimeout.js";
|
|
57
67
|
export { CachedDatabaseAdapter } from "./cachedDatabase.js";
|
|
58
68
|
export type { CachedAdapterOptions } from "./cachedDatabase.js";
|
|
59
69
|
export { FakeData } from "./fakeData.js";
|
|
@@ -62,8 +72,8 @@ export type { SeedSummary, SeedOptions } from "./seeder.js";
|
|
|
62
72
|
|
|
63
73
|
// DocStore — pymongo-style document store with a zero-config SQLite (JSON1) fallback
|
|
64
74
|
export {
|
|
65
|
-
ObjectId, InvalidId, SqliteDatabase, SqliteCollection, Cursor,
|
|
66
|
-
getCollection, isServerless, resetDefaultStore,
|
|
75
|
+
ObjectId, InvalidId, DocStoreDriverMissing, SqliteDatabase, SqliteCollection, Cursor,
|
|
76
|
+
getCollection, isServerless, resetDefaultStore, closeDocStore,
|
|
67
77
|
encodeValue, decodeValue, compileFilter,
|
|
68
78
|
} from "./docstore.js";
|
|
69
79
|
export type {
|
|
@@ -217,7 +217,14 @@ export async function syncModels(models: DiscoveredModel[]): Promise<void> {
|
|
|
217
217
|
console.log(` Created table: ${tableName}`);
|
|
218
218
|
} else {
|
|
219
219
|
// Check for new columns. SQLite exposes the legacy getTableColumns/
|
|
220
|
-
// addColumn helpers; other engines use
|
|
220
|
+
// addColumn helpers; other engines use getColumns()/ALTER TABLE.
|
|
221
|
+
//
|
|
222
|
+
// Collapsing this into getColumns() looks obviously right and is NOT:
|
|
223
|
+
// it broke the legacy NOT NULL migration_id path, which is the bug that
|
|
224
|
+
// wedged every migration for ~20 releases (python#93). getTableColumns
|
|
225
|
+
// reads PRAGMA directly; getColumns goes through schema splitting and
|
|
226
|
+
// does not return the same thing here. Removing it needs its own change
|
|
227
|
+
// with that path tested, not a drive-by in an interface tidy-up.
|
|
221
228
|
const existingCols = (adapter as any).getTableColumns
|
|
222
229
|
? (adapter as SQLiteAdapter).getTableColumns(tableName)
|
|
223
230
|
: await adapterColumns(adapter, tableName);
|
|
@@ -508,8 +515,16 @@ async function recordApplied(
|
|
|
508
515
|
*/
|
|
509
516
|
async function trackingColumns(db: DatabaseAdapter): Promise<Set<string>> {
|
|
510
517
|
try {
|
|
511
|
-
// The DatabaseAdapter contract exposes
|
|
512
|
-
|
|
518
|
+
// The DatabaseAdapter contract exposes getColumns() (feature 3: renamed from
|
|
519
|
+
// columns() to match the other three frameworks and the get- prefix used
|
|
520
|
+
// everywhere else).
|
|
521
|
+
//
|
|
522
|
+
// This reads through `as any`, so the compiler could not catch the rename
|
|
523
|
+
// here - it went silently to undefined, `cols` came back empty, migration_id
|
|
524
|
+
// was left out of the insert and every migration failed on the legacy
|
|
525
|
+
// NOT NULL column. Exactly the failure python#93 caused, from the opposite
|
|
526
|
+
// direction. Kept optional-chained for adapters that predate the contract.
|
|
527
|
+
const cols = await (db as any).getColumns?.(MIGRATION_TABLE);
|
|
513
528
|
if (!Array.isArray(cols)) return new Set();
|
|
514
529
|
return new Set(
|
|
515
530
|
cols.map((c: any) => String(c?.name ?? c ?? "").toLowerCase()).filter(Boolean),
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import type { DatabaseAdapter } from "./types.js";
|
|
21
21
|
import { getAdapter, adapterFetch, adapterFetchOne } from "./database.js";
|
|
22
|
+
import { DatabaseResult } from "./databaseResult.js";
|
|
22
23
|
|
|
23
24
|
export class QueryBuilder {
|
|
24
25
|
private table: string;
|
|
@@ -198,22 +199,55 @@ export class QueryBuilder {
|
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
/**
|
|
201
|
-
* Execute the query and return
|
|
202
|
+
* Execute the query and return a DatabaseResult.
|
|
202
203
|
*
|
|
203
|
-
*
|
|
204
|
+
* BREAKING (3.13.95, parity): this returned a bare array of rows. The other
|
|
205
|
+
* three frameworks all return the DatabaseResult that `db.fetch()` produces:
|
|
206
|
+
* Python get() -> DatabaseResult (orm/query_builder/__init__.py)
|
|
207
|
+
* PHP get(): mixed -> $this->db->fetch(...)
|
|
208
|
+
* Ruby get -> @db.fetch(...)
|
|
209
|
+
* Node was the odd one out, so the same builder chain returned a different
|
|
210
|
+
* TYPE per language and portable code could not read `.records`, `.count`,
|
|
211
|
+
* `.limit` or `.offset` off it.
|
|
212
|
+
*
|
|
213
|
+
* MIGRATION: read `.records` for the rows.
|
|
214
|
+
* before: const rows = await qb.get(); rows.length
|
|
215
|
+
* after: const result = await qb.get(); result.records.length
|
|
216
|
+
* DatabaseResult is iterable, so `for (const row of result)` and
|
|
217
|
+
* `[...result]` work unchanged, and `response()`/`res.json()` already
|
|
218
|
+
* auto-serialize it to a JSON array.
|
|
219
|
+
*
|
|
220
|
+
* No default LIMIT is applied when `.limit()` was never called (v3.13.39) --
|
|
221
|
+
* a silent cap here was a data-loss-on-read footgun. That is unchanged.
|
|
222
|
+
*
|
|
223
|
+
* @returns DatabaseResult carrying `.records`, `.count`, `.limit`, `.offset`.
|
|
204
224
|
*/
|
|
205
|
-
async get
|
|
225
|
+
async get(): Promise<DatabaseResult> {
|
|
206
226
|
this.ensureDb();
|
|
207
227
|
const sql = this.toSql();
|
|
208
228
|
const allParams = [...this.params, ...this.havingParams];
|
|
209
229
|
|
|
210
|
-
|
|
230
|
+
const rows = await adapterFetch(
|
|
211
231
|
this.db!,
|
|
212
232
|
sql,
|
|
213
233
|
allParams.length > 0 ? allParams : undefined,
|
|
214
234
|
this.limitVal,
|
|
215
235
|
this.offsetVal,
|
|
216
236
|
);
|
|
237
|
+
|
|
238
|
+
// Constructed exactly as Database._fetchWithLimit does, so a QueryBuilder
|
|
239
|
+
// result and a db.fetch() result are the same object in the same state --
|
|
240
|
+
// including leaving `count` to default to the row count and handing the
|
|
241
|
+
// adapter + sql through.
|
|
242
|
+
return new DatabaseResult(
|
|
243
|
+
rows as Record<string, unknown>[],
|
|
244
|
+
undefined,
|
|
245
|
+
undefined,
|
|
246
|
+
this.limitVal,
|
|
247
|
+
this.offsetVal,
|
|
248
|
+
this.db!,
|
|
249
|
+
sql,
|
|
250
|
+
);
|
|
217
251
|
}
|
|
218
252
|
|
|
219
253
|
/**
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
// ── SQL Translator ───────────────────────────────────────────
|
|
21
21
|
|
|
22
|
+
import { DatabaseUrl } from "./databaseUrl.js";
|
|
22
23
|
export class SQLTranslator {
|
|
23
24
|
/**
|
|
24
25
|
* Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
@@ -172,6 +173,271 @@ export class SQLTranslator {
|
|
|
172
173
|
if (idx === -1) return [null, name];
|
|
173
174
|
return [name.slice(0, idx), name.slice(idx + 1)];
|
|
174
175
|
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Hard per-statement bind-parameter ceiling per engine. 0 = never collapse.
|
|
179
|
+
* Sourced from test/fixtures/batch_write_contract.json, byte-identical in all
|
|
180
|
+
* four frameworks.
|
|
181
|
+
*/
|
|
182
|
+
static readonly MAX_BIND_PARAMS: Record<string, number> = {
|
|
183
|
+
sqlite: 999,
|
|
184
|
+
postgres: 65535,
|
|
185
|
+
mysql: 65535,
|
|
186
|
+
mssql: 2100,
|
|
187
|
+
firebird: 0,
|
|
188
|
+
odbc: 0,
|
|
189
|
+
mongodb: 0,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The four frameworks do not agree on what an engine calls itself — Python
|
|
194
|
+
* and PHP report "postgresql", Ruby and Node report "postgres". Without
|
|
195
|
+
* normalising, the cap lookup misses and the collapse silently does nothing
|
|
196
|
+
* on the engine with the largest win.
|
|
197
|
+
*/
|
|
198
|
+
static readonly ENGINE_ALIASES: Record<string, string> = {
|
|
199
|
+
postgresql: "postgres",
|
|
200
|
+
pgsql: "postgres",
|
|
201
|
+
sqlite3: "sqlite",
|
|
202
|
+
sqlserver: "mssql",
|
|
203
|
+
sqlsrv: "mssql",
|
|
204
|
+
mariadb: "mysql",
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// The `d` flag records group indices, so the head can be sliced at the exact
|
|
208
|
+
// start of the VALUES group rather than by hunting for a parenthesis (the
|
|
209
|
+
// column list has parentheses too).
|
|
210
|
+
private static readonly INSERT_VALUES =
|
|
211
|
+
/^\s*INSERT\s+INTO\s+.+?\s+VALUES\s*\(([^()]*)\)\s*$/dis;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Engines whose lastInsertId reports the FIRST generated id of a multi-row
|
|
215
|
+
* INSERT rather than the last. Verified live, not assumed: a 3-row insert
|
|
216
|
+
* into a fresh MySQL table reports 1 while MAX(id) is 3. SQLite, PostgreSQL
|
|
217
|
+
* and MSSQL already report the last, so collapsing does not change them.
|
|
218
|
+
*/
|
|
219
|
+
static readonly FIRST_ID_ENGINES: readonly string[] = ["mysql"];
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Normalise a collapsed batch's last id to the LAST row's id.
|
|
223
|
+
*
|
|
224
|
+
* A row-at-a-time batch reports the last row's id simply because the last
|
|
225
|
+
* statement inserted the last row. Collapsing rows into one statement changes
|
|
226
|
+
* that on any engine that reports the FIRST generated id, so this restores
|
|
227
|
+
* the contract instead of quietly redefining it. The ids in one statement are
|
|
228
|
+
* consecutive, so the last is `first + rows - 1`.
|
|
229
|
+
*/
|
|
230
|
+
static batchLastId(reportedId: unknown, rowsInChunk: number, engine: string): unknown {
|
|
231
|
+
const lower = (engine ?? "").toLowerCase();
|
|
232
|
+
const name = SQLTranslator.ENGINE_ALIASES[lower] ?? lower;
|
|
233
|
+
if (!SQLTranslator.FIRST_ID_ENGINES.includes(name)) return reportedId;
|
|
234
|
+
|
|
235
|
+
const n = typeof reportedId === "bigint" ? Number(reportedId) : Number(reportedId);
|
|
236
|
+
if (reportedId === null || reportedId === undefined || Number.isNaN(n)) {
|
|
237
|
+
return reportedId; // UUID/ULID key — no successor
|
|
238
|
+
}
|
|
239
|
+
return n + Math.max(rowsInChunk, 1) - 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Collapse a row-at-a-time INSERT batch into chunked multi-row VALUES.
|
|
244
|
+
*
|
|
245
|
+
* A batch that loops one INSERT per row pays a full network round-trip per
|
|
246
|
+
* row, and the round-trip — not SQL building — is the entire cost of a batch
|
|
247
|
+
* write. Measured over 500 rows: PostgreSQL 9848ms row-at-a-time against
|
|
248
|
+
* 15.8ms as a single multi-row statement (625x), MySQL 216x, MSSQL 121x.
|
|
249
|
+
*
|
|
250
|
+
* PURE: no I/O and no engine contact, so the chunking rules are checkable
|
|
251
|
+
* without a database. The live-engine runners prove the rows land.
|
|
252
|
+
*
|
|
253
|
+
* @returns Statements to run INSTEAD of the loop, or an EMPTY array meaning
|
|
254
|
+
* "not collapsible — keep looping", which is always correct.
|
|
255
|
+
*/
|
|
256
|
+
static buildBatchInserts(
|
|
257
|
+
sql: string,
|
|
258
|
+
paramSets: unknown[][],
|
|
259
|
+
engine: string,
|
|
260
|
+
): Array<[string, unknown[]]> {
|
|
261
|
+
const rows = paramSets ?? [];
|
|
262
|
+
if (rows.length < 2) return [];
|
|
263
|
+
|
|
264
|
+
const lower = (engine ?? "").toLowerCase();
|
|
265
|
+
const name = SQLTranslator.ENGINE_ALIASES[lower] ?? lower;
|
|
266
|
+
const cap = SQLTranslator.MAX_BIND_PARAMS[name] ?? 0;
|
|
267
|
+
// Firebird has no multi-row VALUES syntax (verified against a live 5.0.4:
|
|
268
|
+
// -104 Token unknown); ODBC's real ceiling depends on the driver behind it.
|
|
269
|
+
// Emitting SQL the engine cannot parse to save a round-trip is not a trade
|
|
270
|
+
// worth making.
|
|
271
|
+
if (cap <= 0) return [];
|
|
272
|
+
|
|
273
|
+
const upper = sql.toUpperCase();
|
|
274
|
+
// A collapsed statement returns N rows where the caller expects one, and
|
|
275
|
+
// conflict arbitration changes once rows share a statement.
|
|
276
|
+
if (
|
|
277
|
+
upper.includes("RETURNING") ||
|
|
278
|
+
upper.includes("ON CONFLICT") ||
|
|
279
|
+
upper.includes("ON DUPLICATE KEY")
|
|
280
|
+
) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const match = SQLTranslator.INSERT_VALUES.exec(sql);
|
|
285
|
+
if (match === null) return [];
|
|
286
|
+
|
|
287
|
+
// Every slot must be a bare placeholder. `now()` repeated per row inside one
|
|
288
|
+
// statement is not the same write as `now()` evaluated per statement.
|
|
289
|
+
const slots = match[1].split(",").map((s) => s.trim());
|
|
290
|
+
if (slots.length === 0 || slots.some((s) => s !== "?")) return [];
|
|
291
|
+
|
|
292
|
+
const columns = slots.length;
|
|
293
|
+
if (rows.some((params) => params.length !== columns)) return [];
|
|
294
|
+
|
|
295
|
+
const chunkRows = Math.max(1, Math.floor(cap / columns));
|
|
296
|
+
if (chunkRows < 2) return [];
|
|
297
|
+
|
|
298
|
+
const valuesStart = match.indices?.[1]?.[0];
|
|
299
|
+
if (valuesStart === undefined) return [];
|
|
300
|
+
const head = sql.slice(0, valuesStart - 1).trimEnd();
|
|
301
|
+
const oneRow = `(${new Array(columns).fill("?").join(", ")})`;
|
|
302
|
+
|
|
303
|
+
const statements: Array<[string, unknown[]]> = [];
|
|
304
|
+
for (let start = 0; start < rows.length; start += chunkRows) {
|
|
305
|
+
const chunk = rows.slice(start, start + chunkRows);
|
|
306
|
+
const flat: unknown[] = [];
|
|
307
|
+
for (const params of chunk) flat.push(...params);
|
|
308
|
+
statements.push([`${head} ${new Array(chunk.length).fill(oneRow).join(", ")}`, flat]);
|
|
309
|
+
}
|
|
310
|
+
return statements;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Blank out string literals, quoted identifiers and comments, so a keyword
|
|
315
|
+
* search sees only real SQL. Blanks are spaces of the SAME LENGTH (newlines
|
|
316
|
+
* preserved), so offsets and line structure still line up with the original.
|
|
317
|
+
*
|
|
318
|
+
* This exists because "does the caller's SQL already have a LIMIT?" used to be
|
|
319
|
+
* `sql.toUpperCase().split("--")[0].includes("LIMIT")`, and MEASURED on a real
|
|
320
|
+
* 150-row table with the 100-row cap in force, every one of these returned
|
|
321
|
+
* ALL 150 ROWS instead of 100:
|
|
322
|
+
*
|
|
323
|
+
* SELECT * FROM t WHERE label != 'LIMIT' ORDER BY id -- literal
|
|
324
|
+
* SELECT * FROM t ORDER BY id -- LIMIT 5 -- line comment
|
|
325
|
+
* SELECT * FROM t ORDER BY id /* LIMIT 5 *\/ -- block comment
|
|
326
|
+
*
|
|
327
|
+
* A column named `rate_limit` does it too. That is a silently UNCAPPED read of
|
|
328
|
+
* a whole table, which is the exact production incident the row cap exists to
|
|
329
|
+
* prevent, reachable through an ordinary column name.
|
|
330
|
+
*
|
|
331
|
+
* @param sql Raw SQL, exactly as the caller wrote it.
|
|
332
|
+
* @returns The same string with literals and comments replaced by spaces.
|
|
333
|
+
*/
|
|
334
|
+
static scrubSqlText(sql: string): string {
|
|
335
|
+
let out = "";
|
|
336
|
+
let i = 0;
|
|
337
|
+
const blank = (ch: string): string => (ch === "\n" ? "\n" : " ");
|
|
338
|
+
|
|
339
|
+
while (i < sql.length) {
|
|
340
|
+
const c = sql[i];
|
|
341
|
+
const next = sql[i + 1];
|
|
342
|
+
|
|
343
|
+
// '...' string literal, with '' as the embedded-quote escape
|
|
344
|
+
if (c === "'" || c === '"') {
|
|
345
|
+
const quote = c;
|
|
346
|
+
out += " ";
|
|
347
|
+
i++;
|
|
348
|
+
while (i < sql.length) {
|
|
349
|
+
if (sql[i] === quote) {
|
|
350
|
+
if (sql[i + 1] === quote) {
|
|
351
|
+
out += " ";
|
|
352
|
+
i += 2;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
out += " ";
|
|
356
|
+
i++;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
out += blank(sql[i]);
|
|
360
|
+
i++;
|
|
361
|
+
}
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// -- line comment, to end of line
|
|
366
|
+
if (c === "-" && next === "-") {
|
|
367
|
+
while (i < sql.length && sql[i] !== "\n") {
|
|
368
|
+
out += " ";
|
|
369
|
+
i++;
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// /* block comment */
|
|
375
|
+
if (c === "/" && next === "*") {
|
|
376
|
+
out += " ";
|
|
377
|
+
i += 2;
|
|
378
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) {
|
|
379
|
+
out += blank(sql[i]);
|
|
380
|
+
i++;
|
|
381
|
+
}
|
|
382
|
+
if (i < sql.length) {
|
|
383
|
+
out += " ";
|
|
384
|
+
i += 2;
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
out += c;
|
|
390
|
+
i++;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* True when the statement ENDS with its own LIMIT clause, so appending another
|
|
398
|
+
* would be wrong (and on SQLite, a syntax error).
|
|
399
|
+
*
|
|
400
|
+
* Anchored to the END on purpose. A bare "contains LIMIT" test also matches a
|
|
401
|
+
* LIMIT inside a subquery, where the OUTER statement still needs its cap. This
|
|
402
|
+
* is tina4-php's `SqlNormalizerTrait::hasTrailingLimit` regex, ported verbatim
|
|
403
|
+
* so all four frameworks answer identically: it accepts a numeric value, `?`,
|
|
404
|
+
* `$1` and `:name` placeholders, MySQL's `LIMIT a, b`, and a trailing OFFSET.
|
|
405
|
+
*
|
|
406
|
+
* @param sql Raw SQL; literals and comments are scrubbed before matching.
|
|
407
|
+
*/
|
|
408
|
+
static hasTrailingLimit(sql: string): boolean {
|
|
409
|
+
const val = String.raw`(?:\d+|\?|\$\d+|:\w+|%s)`;
|
|
410
|
+
const re = new RegExp(
|
|
411
|
+
String.raw`\bLIMIT\s+${val}(?:\s*,\s*${val})?(?:\s+OFFSET\s+${val})?\s*;?\s*$`,
|
|
412
|
+
"i",
|
|
413
|
+
);
|
|
414
|
+
return re.test(SQLTranslator.scrubSqlText(sql));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Append `LIMIT`/`OFFSET` to a statement unless it already carries its own.
|
|
419
|
+
*
|
|
420
|
+
* The clause goes on a NEW LINE. Appending it inline is the second half of the
|
|
421
|
+
* same bug: `SELECT * FROM t -- note` + ` LIMIT 100` puts the clause INSIDE the
|
|
422
|
+
* trailing comment, where SQLite silently ignores it and the whole table comes
|
|
423
|
+
* back. A newline cannot be commented out by a `--` that started on the line
|
|
424
|
+
* above. Trailing semicolons are stripped first for the same reason
|
|
425
|
+
* (`SELECT * FROM t;` + `LIMIT 100` is a syntax error).
|
|
426
|
+
*
|
|
427
|
+
* @param sql The caller's statement.
|
|
428
|
+
* @param limit Row cap to apply; a non-positive value means "no cap".
|
|
429
|
+
* @param offset Rows to skip; omitted or 0 emits no OFFSET.
|
|
430
|
+
*/
|
|
431
|
+
static appendLimit(sql: string, limit?: number, offset?: number): string {
|
|
432
|
+
if (limit === undefined || limit === null || limit <= 0) return sql;
|
|
433
|
+
if (SQLTranslator.hasTrailingLimit(sql)) return sql;
|
|
434
|
+
|
|
435
|
+
const trimmed = sql.replace(/[\s;]+$/, "");
|
|
436
|
+
const suffix = offset !== undefined && offset > 0
|
|
437
|
+
? `LIMIT ${limit} OFFSET ${offset}`
|
|
438
|
+
: `LIMIT ${limit}`;
|
|
439
|
+
return `${trimmed}\n${suffix}`;
|
|
440
|
+
}
|
|
175
441
|
}
|
|
176
442
|
|
|
177
443
|
// ── Query Cache ──────────────────────────────────────────────
|
|
@@ -196,14 +462,54 @@ export class QueryCache {
|
|
|
196
462
|
}
|
|
197
463
|
|
|
198
464
|
/**
|
|
199
|
-
*
|
|
465
|
+
* Stable identity of the DATABASE a cache entry came from.
|
|
466
|
+
*
|
|
467
|
+
* `engine://host:port/database` - and deliberately NOTHING else.
|
|
468
|
+
*
|
|
469
|
+
* WHY IT EXISTS: the key used to be `query:${sql}:${params}` with nothing
|
|
470
|
+
* naming the connection, so on any SHARED backend two databases cross-served
|
|
471
|
+
* each other's rows. Two apps pointed at one Redis, or one app with a primary
|
|
472
|
+
* and an analytics connection, silently read each other's data. Identical SQL
|
|
473
|
+
* text across tenants is the COMMON case, not an edge case, so the collision
|
|
474
|
+
* was the normal outcome.
|
|
475
|
+
*
|
|
476
|
+
* WHY NO CREDENTIALS: a password in the key means every rotation silently
|
|
477
|
+
* cold-starts the cache, and a shared backend's key namespace is visible to
|
|
478
|
+
* every tenant of that backend - a secret must never be folded into it. The
|
|
479
|
+
* username is out for the same reason plus a second: two connections
|
|
480
|
+
* differing only by role read the SAME rows and should share the entry.
|
|
481
|
+
*
|
|
482
|
+
* WHY NOTHING PER-PROCESS: no pid, no object id, no salt. Those would isolate
|
|
483
|
+
* the databases by ACCIDENT and destroy the point of a shared cache, because
|
|
484
|
+
* no instance would ever hit another instance's entry.
|
|
200
485
|
*/
|
|
201
|
-
static
|
|
486
|
+
static cacheIdentity(url: string): string {
|
|
487
|
+
try {
|
|
488
|
+
const parsed = new DatabaseUrl(url);
|
|
489
|
+
return `${parsed.engine}://${parsed.host ?? ""}:${parsed.port ?? ""}/${parsed.database}`;
|
|
490
|
+
} catch {
|
|
491
|
+
// An unparseable URL still needs a STABLE identity, and falling back to a
|
|
492
|
+
// constant would silently restore the cross-serving bug. The raw URL is
|
|
493
|
+
// stable and distinct; it is only reached for a URL the connection layer
|
|
494
|
+
// is about to reject anyway.
|
|
495
|
+
return url;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Generate a cache key from DATABASE IDENTITY + SQL + params.
|
|
501
|
+
*
|
|
502
|
+
* The NUL separators keep the three parts from running together, so a table
|
|
503
|
+
* named after the tail of a database name cannot forge another database's
|
|
504
|
+
* key. The key is not hashed here: the only backend with a key-length limit
|
|
505
|
+
* is memcached, and its backend already SHA-256-hashes whatever it is given.
|
|
506
|
+
*/
|
|
507
|
+
static queryKey(sql: string, params?: unknown[], identity = ""): string {
|
|
202
508
|
const paramStr = params ? JSON.stringify(params) : "";
|
|
203
|
-
|
|
204
|
-
return `query:${sql}:${paramStr}`;
|
|
509
|
+
return `query:${identity}\u0000${sql}\u0000${paramStr}`;
|
|
205
510
|
}
|
|
206
511
|
|
|
512
|
+
|
|
207
513
|
/**
|
|
208
514
|
* Get a cached value. Returns undefined if expired or missing.
|
|
209
515
|
*/
|
|
@@ -77,8 +77,8 @@ export interface DatabaseAdapter {
|
|
|
77
77
|
/** Insert one or more rows into a table, returns result with lastId. */
|
|
78
78
|
insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
|
|
79
79
|
|
|
80
|
-
/** Update rows in a table matching filter, returns affected row count. */
|
|
81
|
-
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown
|
|
80
|
+
/** Update rows in a table matching filter (object or string WHERE), returns affected row count. */
|
|
81
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult;
|
|
82
82
|
|
|
83
83
|
/** Delete rows from a table matching filter (object, string WHERE, or array of objects). */
|
|
84
84
|
delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult;
|
|
@@ -93,10 +93,10 @@ export interface DatabaseAdapter {
|
|
|
93
93
|
rollback(): void;
|
|
94
94
|
|
|
95
95
|
/** List all tables in the database. */
|
|
96
|
-
|
|
96
|
+
getTables(): string[];
|
|
97
97
|
|
|
98
98
|
/** List columns with types for a table. */
|
|
99
|
-
|
|
99
|
+
getColumns(table: string): ColumnInfo[];
|
|
100
100
|
|
|
101
101
|
/** Get the last inserted id (auto-increment integer, or a UUID/string PK). */
|
|
102
102
|
lastInsertId(): number | bigint | string | null;
|
|
@@ -115,6 +115,17 @@ export interface DatabaseAdapter {
|
|
|
115
115
|
|
|
116
116
|
/** Add a column to an existing table (legacy, used by migration). */
|
|
117
117
|
addColumn?(table: string, colName: string, def: FieldDefinition): void;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Stable identity of the DATABASE this adapter is connected to, as
|
|
121
|
+
* `engine://host:port/database` with NO credentials - set by whoever built
|
|
122
|
+
* the adapter from a URL or config.
|
|
123
|
+
*
|
|
124
|
+
* The query cache folds this into every key. Without it two databases sharing
|
|
125
|
+
* one cache backend cross-serve each other's rows, because identical SQL text
|
|
126
|
+
* across tenants is the common case.
|
|
127
|
+
*/
|
|
128
|
+
cacheIdentity?: string;
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
export interface PaginatedResult<T = Record<string, unknown>> {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kill any process listening on `port`. Returns true if anything was killed.
|
|
3
|
+
*
|
|
4
|
+
* Every PID is validated first. `parseInt` on a non-numeric lsof field yields
|
|
5
|
+
* 1, and SIGTERM to PID 1 is the container's own init -- which is exactly how a
|
|
6
|
+
* production container logged "Killed existing process on port 7148 (PID: 1
|
|
7
|
+
* ...)" and then exited 143, killing itself on startup.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The PIDs from `lsof -ti` output that are safe to signal.
|
|
11
|
+
*
|
|
12
|
+
* Pure so the safety rule can be tested directly. An unvalidated parse is a
|
|
13
|
+
* footgun with real teeth: where lsof prints a different shape than -ti
|
|
14
|
+
* implies, a non-numeric field becomes 0, and signalling PID 0 hits EVERY
|
|
15
|
+
* process in the caller's own process group -- the server kills itself. That
|
|
16
|
+
* is what produced "Killed existing process on port 7148 (PID: 1 ...)" in a
|
|
17
|
+
* real image, where the container then exited 143.
|
|
18
|
+
*
|
|
19
|
+
* Accepts only all-digit tokens; never PID 0 (our group), PID 1 (init),
|
|
20
|
+
* ourselves, or our own process group.
|
|
21
|
+
*/
|
|
22
|
+
export declare function selectablePids(lsofOutput: string, me: number, myGroup?: number): number[];
|
|
23
|
+
export interface CommandManifestEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
args?: string[];
|
|
27
|
+
subcommands?: string[];
|
|
28
|
+
/** True when the tina4 client implements this command, not the framework. */
|
|
29
|
+
delegated?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface CommandManifest {
|
|
32
|
+
framework: string;
|
|
33
|
+
version: string;
|
|
34
|
+
commands: CommandManifestEntry[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the machine-readable manifest of the CLI's command surface.
|
|
38
|
+
*
|
|
39
|
+
* Pure data: reads the module-level COMMANDS and DELEGATED registries plus the
|
|
40
|
+
* framework version — no bootstrap, no database, no migrations, no app imports.
|
|
41
|
+
* This is exactly what `commands --json` serialises and what the tina4 Rust
|
|
42
|
+
* client consumes to discover which commands this framework supports.
|
|
43
|
+
*
|
|
44
|
+
* Commands handed to the `tina4` client carry `delegated: true`, so the manifest
|
|
45
|
+
* describes the WHOLE surface the CLI accepts while still saying who implements
|
|
46
|
+
* each one. The client needs no change: its help renderer already drops manifest
|
|
47
|
+
* names that clash with its own natives.
|
|
48
|
+
*
|
|
49
|
+
* Shape (identical keys to the Python master):
|
|
50
|
+
* { framework: "nodejs", version: "<x.y.z>",
|
|
51
|
+
* commands: [{ name, summary, args?, subcommands?, delegated? }, ...] }
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildCommandManifest(): CommandManifest;
|
|
54
|
+
/**
|
|
55
|
+
* Emit the CLI's own command surface — the self-describing manifest.
|
|
56
|
+
*
|
|
57
|
+
* tina4nodejs commands human-readable list
|
|
58
|
+
* tina4nodejs commands --json machine-readable manifest (for the tina4 CLI)
|
|
59
|
+
*
|
|
60
|
+
* CHEAP + side-effect-free by contract: it only prints the static COMMANDS
|
|
61
|
+
* registry plus the framework version. It MUST NOT bootstrap the framework,
|
|
62
|
+
* open a database, run migrations, or import app modules — the Rust client
|
|
63
|
+
* calls this on `tina4 --help`, in any directory, so it must be instant and
|
|
64
|
+
* safe to run anywhere.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runCommands(args?: string[]): void;
|
|
67
|
+
export interface CommandSpec {
|
|
68
|
+
handler: (cmdArgs: string[]) => void | Promise<void>;
|
|
69
|
+
summary: string;
|
|
70
|
+
usage?: string;
|
|
71
|
+
args?: string[];
|
|
72
|
+
subcommands?: string[];
|
|
73
|
+
}
|
|
74
|
+
export declare const COMMANDS: Record<string, CommandSpec>;
|
|
75
|
+
export interface DelegatedSpec {
|
|
76
|
+
summary: string;
|
|
77
|
+
usage?: string;
|
|
78
|
+
args?: string[];
|
|
79
|
+
}
|
|
80
|
+
export declare const DELEGATED: Record<string, DelegatedSpec>;
|
|
81
|
+
export declare const CLIENT_BINARY = "tina4";
|
|
82
|
+
export declare const DELEGATION_GUARD_ENV = "TINA4_CLI_DELEGATED";
|
|
83
|
+
export declare const EXIT_CLIENT_UNAVAILABLE = 127;
|
|
84
|
+
export declare const EXIT_UNKNOWN_COMMAND = 1;
|
|
85
|
+
/**
|
|
86
|
+
* Run `tina4 <command> <args...>`, returning the client's exit code.
|
|
87
|
+
*
|
|
88
|
+
* Returns EXIT_CLIENT_UNAVAILABLE (127) with an actionable message when the
|
|
89
|
+
* client is not on PATH, or when the re-entry guard shows the resolved `tina4`
|
|
90
|
+
* came back to a framework CLI (a delegation loop).
|
|
91
|
+
*/
|
|
92
|
+
export declare function delegateToClient(command: string, args: string[]): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare function toSnake(name: string): string;
|
|
2
|
+
export declare function toTableName(name: string): string;
|
|
3
|
+
/** slug-of-anything → PascalCase (order-emails → OrderEmails). */
|
|
4
|
+
export declare function toPascal(name: string): string;
|
|
5
|
+
export declare function parseFields(fieldsStr: string): Array<[string, string]>;
|
|
6
|
+
export declare const DEFAULT_FIELDS: ReadonlyArray<[string, string]>;
|
|
7
|
+
/** Parsed --fields, or the default single `name` column when none given. */
|
|
8
|
+
export declare function fieldsOrDefault(fieldsStr: string): Array<[string, string]>;
|
|
9
|
+
export declare function parseCliArgs(args: string[]): {
|
|
10
|
+
flags: Record<string, string | boolean>;
|
|
11
|
+
positional: string[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Parse a `--every` duration ("5m", "30s", "2h", "1d", or bare seconds) → seconds.
|
|
15
|
+
* Falls back to 60s on an empty/unparseable value so a scaffold always has a
|
|
16
|
+
* valid ServiceRunner interval.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseEvery(every: string | boolean | undefined): number;
|
|
19
|
+
/**
|
|
20
|
+
* The canonical AI-FILL placeholder for a LOGIC-shaped stub — a tight, grounded
|
|
21
|
+
* fill-spec (not a vague `// TODO`) so a coding agent (or dev) completes it
|
|
22
|
+
* correctly. `throw new Error(...)` makes an unfilled scaffold fail LOUD; the
|
|
23
|
+
* greppable `AI-FILL` banner lets a human/agent jump to every gap. `use` names
|
|
24
|
+
* only REAL tina4-nodejs symbols (verified in source).
|
|
25
|
+
*/
|
|
26
|
+
export declare function aiFill(fn: string, spec: {
|
|
27
|
+
intent: string;
|
|
28
|
+
given?: string;
|
|
29
|
+
use: string;
|
|
30
|
+
ret?: string;
|
|
31
|
+
ground: string;
|
|
32
|
+
raise: string;
|
|
33
|
+
}, indent?: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* The lighter EXTEND marker for CRUD-shaped WORKING code — no throw (the
|
|
36
|
+
* boilerplate IS the feature); just a greppable hint at the natural extension
|
|
37
|
+
* point (custom validation / business rules / authorization).
|
|
38
|
+
*/
|
|
39
|
+
export declare function extend(note: string, hint?: string, indent?: string): string;
|
|
40
|
+
export interface GeneratorSpec {
|
|
41
|
+
handler: (name: string, flags: Record<string, string | boolean>) => void;
|
|
42
|
+
/** Arg/flag hint shown in `tina4nodejs help` (human only). */
|
|
43
|
+
usage: string;
|
|
44
|
+
summary: string;
|
|
45
|
+
}
|
|
46
|
+
export declare const GENERATORS: Record<string, GeneratorSpec>;
|
|
47
|
+
export declare function generate(what: string, name: string, extraArgs?: string[]): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initProject(name: string): Promise<void>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run the metrics report. Returns the process exit code; does NOT call
|
|
3
|
+
* process.exit (the bin wrapper does). 0 = ok / below threshold, 1 = gated
|
|
4
|
+
* failure, 2 = bad arguments / analysis error.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runMetrics(args?: string[]): number;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runMigrations(migrationDir?: string): Promise<void>;
|