turbine-orm 0.23.1 → 0.24.0
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/README.md +4 -1
- package/dist/adapters/cockroachdb.d.ts +0 -1
- package/dist/adapters/cockroachdb.js +0 -1
- package/dist/adapters/index.d.ts +0 -1
- package/dist/adapters/index.js +0 -1
- package/dist/adapters/yugabytedb.d.ts +0 -1
- package/dist/adapters/yugabytedb.js +0 -1
- package/dist/cjs/cli/config.js +14 -0
- package/dist/cjs/cli/index.js +60 -0
- package/dist/cjs/schema-builder.js +29 -1
- package/dist/cjs/schema-sql.js +19 -5
- package/dist/cli/config.d.ts +14 -1
- package/dist/cli/config.js +13 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +61 -2
- package/dist/cli/loader.d.ts +0 -1
- package/dist/cli/loader.js +0 -1
- package/dist/cli/migrate.d.ts +0 -1
- package/dist/cli/migrate.js +0 -1
- package/dist/cli/observe-ui.d.ts +0 -1
- package/dist/cli/observe-ui.js +0 -1
- package/dist/cli/observe.d.ts +0 -1
- package/dist/cli/observe.js +0 -1
- package/dist/cli/studio-ui.generated.d.ts +0 -1
- package/dist/cli/studio-ui.generated.js +0 -1
- package/dist/cli/studio.d.ts +0 -1
- package/dist/cli/studio.js +0 -1
- package/dist/cli/ui.d.ts +0 -1
- package/dist/cli/ui.js +0 -1
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/dialect.d.ts +0 -1
- package/dist/dialect.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -1
- package/dist/generate.d.ts +0 -1
- package/dist/generate.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/introspect.d.ts +0 -1
- package/dist/introspect.js +0 -1
- package/dist/mssql.d.ts +0 -1
- package/dist/mssql.js +0 -1
- package/dist/mysql.d.ts +0 -1
- package/dist/mysql.js +0 -1
- package/dist/nested-write.d.ts +0 -1
- package/dist/nested-write.js +0 -1
- package/dist/observe.d.ts +0 -1
- package/dist/observe.js +0 -1
- package/dist/pipeline-submittable.d.ts +0 -1
- package/dist/pipeline-submittable.js +0 -1
- package/dist/pipeline.d.ts +0 -1
- package/dist/pipeline.js +0 -1
- package/dist/powdb.d.ts +0 -1
- package/dist/powdb.js +0 -1
- package/dist/powql.d.ts +0 -1
- package/dist/powql.js +0 -1
- package/dist/query/builder.d.ts +0 -1
- package/dist/query/builder.js +0 -1
- package/dist/query/index.d.ts +0 -1
- package/dist/query/index.js +0 -1
- package/dist/query/types.d.ts +0 -1
- package/dist/query/types.js +0 -1
- package/dist/query/utils.d.ts +0 -1
- package/dist/query/utils.js +0 -1
- package/dist/realtime.d.ts +0 -1
- package/dist/realtime.js +0 -1
- package/dist/schema-builder.d.ts +6 -4
- package/dist/schema-builder.js +29 -2
- package/dist/schema-sql.d.ts +0 -1
- package/dist/schema-sql.js +19 -6
- package/dist/schema.d.ts +0 -1
- package/dist/schema.js +0 -1
- package/dist/serverless.d.ts +0 -1
- package/dist/serverless.js +0 -1
- package/dist/sqlite.d.ts +0 -1
- package/dist/sqlite.js +0 -1
- package/dist/typed-sql.d.ts +0 -1
- package/dist/typed-sql.js +0 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -51,8 +51,9 @@ Performance is at parity with Prisma and Drizzle — the real reasons to choose
|
|
|
51
51
|
## Quick Start
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
# 1. Install
|
|
54
|
+
# 1. Install (the CLI also needs tsx to load .ts config/schema files)
|
|
55
55
|
npm install turbine-orm
|
|
56
|
+
npm install --save-dev tsx
|
|
56
57
|
|
|
57
58
|
# 2. Initialize project
|
|
58
59
|
npx turbine init --url postgres://user:pass@localhost:5432/mydb
|
|
@@ -61,6 +62,8 @@ npx turbine init --url postgres://user:pass@localhost:5432/mydb
|
|
|
61
62
|
npx turbine generate
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
> **CLI prerequisites.** The `turbine` CLI loads your `turbine.config.ts` / `turbine/schema.ts` directly, so a fresh project needs (1) `tsx` installed — otherwise `.ts` config loading fails with *"Loading .ts config / schema files requires tsx to be installed"* — and (2) `"type": "module"` in `package.json`, since Turbine is ESM. Without it you'll hit `Error [ERR_REQUIRE_ESM]: Cannot require() ES Module`. `create-next-app` sets neither by default. See [USING-TURBINE-ORM.md §0](docs/USING-TURBINE-ORM.md) for details.
|
|
66
|
+
|
|
64
67
|
Works with both ESM and CommonJS:
|
|
65
68
|
|
|
66
69
|
```typescript
|
package/dist/adapters/index.d.ts
CHANGED
package/dist/adapters/index.js
CHANGED
package/dist/cjs/cli/config.js
CHANGED
|
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.looksLikeSchemaFilePath = looksLikeSchemaFilePath;
|
|
42
43
|
exports.loadConfig = loadConfig;
|
|
43
44
|
exports.findConfigFile = findConfigFile;
|
|
44
45
|
exports.resolveConfig = resolveConfig;
|
|
@@ -46,6 +47,19 @@ exports.configTemplate = configTemplate;
|
|
|
46
47
|
const node_fs_1 = require("node:fs");
|
|
47
48
|
const node_path_1 = require("node:path");
|
|
48
49
|
const node_url_1 = require("node:url");
|
|
50
|
+
/**
|
|
51
|
+
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
52
|
+
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
53
|
+
* to introspect (default `public`); the schema-builder file goes in `schemaFile`.
|
|
54
|
+
* A value containing a path separator or a JS/TS extension is almost certainly a
|
|
55
|
+
* mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
|
|
56
|
+
* silently matches zero tables. Used by `turbine generate` to fail loudly.
|
|
57
|
+
*/
|
|
58
|
+
function looksLikeSchemaFilePath(schema) {
|
|
59
|
+
if (!schema)
|
|
60
|
+
return false;
|
|
61
|
+
return schema.includes('/') || schema.includes('\\') || /\.(ts|mts|cts|js|mjs|cjs|json)$/i.test(schema);
|
|
62
|
+
}
|
|
49
63
|
// ---------------------------------------------------------------------------
|
|
50
64
|
// Config file names, in priority order
|
|
51
65
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -120,6 +120,9 @@ function parseArgs() {
|
|
|
120
120
|
case '--allow-drift':
|
|
121
121
|
result.allowDrift = true;
|
|
122
122
|
break;
|
|
123
|
+
case '--allow-empty':
|
|
124
|
+
result.allowEmpty = true;
|
|
125
|
+
break;
|
|
123
126
|
case '--force':
|
|
124
127
|
case '-f':
|
|
125
128
|
result.force = true;
|
|
@@ -242,10 +245,28 @@ async function loadSchemaFile(schemaFile) {
|
|
|
242
245
|
(0, ui_js_1.newline)();
|
|
243
246
|
console.log(` ${(0, ui_js_1.dim)('Hint: install')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to load .ts files:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
|
|
244
247
|
}
|
|
248
|
+
printCjsHintIfApplicable(err);
|
|
245
249
|
}
|
|
246
250
|
process.exit(1);
|
|
247
251
|
}
|
|
248
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* When a config/schema import blows up with the CommonJS-vs-ESM interop error
|
|
255
|
+
* (`Cannot require() ES Module` / `ERR_REQUIRE_ESM`), the root cause is almost
|
|
256
|
+
* always a project whose `package.json` lacks `"type": "module"`: tsx transpiles
|
|
257
|
+
* the `.ts` file to CJS and then can't `require()` Turbine's ESM build. Point the
|
|
258
|
+
* user at the one-line fix instead of leaving them with a raw Node stack trace.
|
|
259
|
+
*/
|
|
260
|
+
function printCjsHintIfApplicable(err) {
|
|
261
|
+
const msg = err.message;
|
|
262
|
+
if (msg.includes('ERR_REQUIRE_ESM') ||
|
|
263
|
+
msg.includes('require() of ES Module') ||
|
|
264
|
+
msg.includes('Cannot require() ES Module')) {
|
|
265
|
+
(0, ui_js_1.newline)();
|
|
266
|
+
console.log(` ${(0, ui_js_1.dim)('Hint: add')} ${(0, ui_js_1.cyan)('"type": "module"')} ${(0, ui_js_1.dim)('to your')} ${(0, ui_js_1.cyan)('package.json')}${(0, ui_js_1.dim)('.')}`);
|
|
267
|
+
console.log(` ${(0, ui_js_1.dim)('Turbine is an ESM package; without it, Node/tsx tries to')} ${(0, ui_js_1.cyan)('require()')} ${(0, ui_js_1.dim)('it and fails.')}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
249
270
|
// ---------------------------------------------------------------------------
|
|
250
271
|
// Command: init
|
|
251
272
|
// ---------------------------------------------------------------------------
|
|
@@ -443,6 +464,24 @@ async function cmdGenerate(args, config) {
|
|
|
443
464
|
(0, ui_js_1.banner)();
|
|
444
465
|
const url = requireUrl(config);
|
|
445
466
|
const startTime = performance.now();
|
|
467
|
+
// Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
|
|
468
|
+
// NOT the path to your schema-builder file — that goes in `schemaFile`. If the
|
|
469
|
+
// configured `schema` looks like a file path, introspection would silently
|
|
470
|
+
// match zero tables and emit an empty client. Fail loudly instead.
|
|
471
|
+
if (!args.allowEmpty && (0, config_js_1.looksLikeSchemaFilePath)(config.schema)) {
|
|
472
|
+
(0, ui_js_1.error)(`The "schema" option looks like a file path: ${(0, ui_js_1.cyan)(config.schema)}`);
|
|
473
|
+
(0, ui_js_1.newline)();
|
|
474
|
+
console.log(` ${(0, ui_js_1.dim)('"schema" is the Postgres schema NAME to introspect')} ${(0, ui_js_1.dim)('(default:')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
|
|
475
|
+
console.log(` ${(0, ui_js_1.dim)('The path to your defineSchema() file belongs in')} ${(0, ui_js_1.cyan)('schemaFile')}${(0, ui_js_1.dim)('.')}`);
|
|
476
|
+
(0, ui_js_1.newline)();
|
|
477
|
+
console.log(` ${(0, ui_js_1.dim)('Fix your')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(':')}`);
|
|
478
|
+
console.log(` ${(0, ui_js_1.green)('schema:')} ${(0, ui_js_1.cyan)("'public'")}${(0, ui_js_1.dim)(", // or omit — introspects the 'public' schema")}`);
|
|
479
|
+
console.log(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (used by `turbine push`)')}`);
|
|
480
|
+
(0, ui_js_1.newline)();
|
|
481
|
+
console.log(` ${(0, ui_js_1.dim)('Re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')} ${(0, ui_js_1.dim)('to introspect this literal schema name anyway.')}`);
|
|
482
|
+
(0, ui_js_1.newline)();
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
446
485
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
447
486
|
(0, ui_js_1.label)('Schema', config.schema);
|
|
448
487
|
(0, ui_js_1.label)('Output', config.out);
|
|
@@ -459,6 +498,24 @@ async function cmdGenerate(args, config) {
|
|
|
459
498
|
const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
|
|
460
499
|
const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
|
|
461
500
|
spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables, ${(0, ui_js_1.bold)(String(totalColumns))} columns, ${(0, ui_js_1.bold)(String(totalRelations))} relations`);
|
|
501
|
+
// Guard: zero tables means the generated client would be empty. That is almost
|
|
502
|
+
// always a misconfiguration (wrong `schema`, an include/exclude that filtered
|
|
503
|
+
// everything, or a database with no tables yet) rather than intent. Fail loudly
|
|
504
|
+
// instead of silently emitting an empty typed client.
|
|
505
|
+
if (tableNames.length === 0 && !args.allowEmpty) {
|
|
506
|
+
(0, ui_js_1.newline)();
|
|
507
|
+
(0, ui_js_1.error)(`Introspection matched 0 tables in schema ${(0, ui_js_1.cyan)(config.schema)} — refusing to generate an empty client.`);
|
|
508
|
+
(0, ui_js_1.newline)();
|
|
509
|
+
console.log(` ${(0, ui_js_1.dim)('Common causes:')}`);
|
|
510
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('schema')} ${(0, ui_js_1.dim)('points at the wrong Postgres namespace')} ${(0, ui_js_1.dim)('(it is the schema NAME, default')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
|
|
511
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('You meant to set')} ${(0, ui_js_1.cyan)('schemaFile')} ${(0, ui_js_1.dim)('(your defineSchema() file), not')} ${(0, ui_js_1.cyan)('schema')}${(0, ui_js_1.dim)('.')}`);
|
|
512
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('include')}/${(0, ui_js_1.cyan)('exclude')} ${(0, ui_js_1.dim)('filtered out every table.')}`);
|
|
513
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('The database has no tables yet — run')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('or a migration first.')}`);
|
|
514
|
+
(0, ui_js_1.newline)();
|
|
515
|
+
console.log(` ${(0, ui_js_1.dim)('If an empty client is genuinely what you want, re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')}${(0, ui_js_1.dim)('.')}`);
|
|
516
|
+
(0, ui_js_1.newline)();
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
462
519
|
// Print table summary
|
|
463
520
|
if (args.verbose) {
|
|
464
521
|
(0, ui_js_1.newline)();
|
|
@@ -1134,6 +1191,7 @@ function showGenerateHelp() {
|
|
|
1134
1191
|
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
1135
1192
|
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
1136
1193
|
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
1194
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1137
1195
|
(0, ui_js_1.newline)();
|
|
1138
1196
|
}
|
|
1139
1197
|
function showPushHelp() {
|
|
@@ -1342,6 +1400,8 @@ async function main() {
|
|
|
1342
1400
|
catch (err) {
|
|
1343
1401
|
if (args.command !== 'init') {
|
|
1344
1402
|
(0, ui_js_1.warn)(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
|
|
1403
|
+
if (err instanceof Error)
|
|
1404
|
+
printCjsHintIfApplicable(err);
|
|
1345
1405
|
}
|
|
1346
1406
|
}
|
|
1347
1407
|
const overrides = {
|
|
@@ -30,16 +30,28 @@ exports.table = table;
|
|
|
30
30
|
exports.applyManyToManyRelations = applyManyToManyRelations;
|
|
31
31
|
/** Maps shorthand names to actual Postgres type strings */
|
|
32
32
|
const TYPE_MAP = {
|
|
33
|
-
serial
|
|
33
|
+
// `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
|
|
34
|
+
// returns them as numbers — so the generated `number` type is accurate.
|
|
35
|
+
// For 64-bit auto-increment keys use `bigserial` (int8), noting that values
|
|
36
|
+
// above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
|
|
37
|
+
// `serial` previously emitted BIGSERIAL.)
|
|
38
|
+
serial: 'SERIAL',
|
|
39
|
+
bigserial: 'BIGSERIAL',
|
|
34
40
|
bigint: 'BIGINT',
|
|
35
41
|
integer: 'INTEGER',
|
|
36
42
|
smallint: 'SMALLINT',
|
|
37
43
|
text: 'TEXT',
|
|
38
44
|
varchar: 'VARCHAR',
|
|
39
45
|
boolean: 'BOOLEAN',
|
|
46
|
+
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware) — Turbine
|
|
47
|
+
// has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
|
|
40
48
|
timestamp: 'TIMESTAMPTZ',
|
|
49
|
+
timestamptz: 'TIMESTAMPTZ',
|
|
41
50
|
date: 'DATE',
|
|
51
|
+
// `json` is an alias for JSONB (Turbine has always emitted JSONB). `jsonb`
|
|
52
|
+
// is the explicit spelling.
|
|
42
53
|
json: 'JSONB',
|
|
54
|
+
jsonb: 'JSONB',
|
|
43
55
|
uuid: 'UUID',
|
|
44
56
|
real: 'REAL',
|
|
45
57
|
double: 'DOUBLE PRECISION',
|
|
@@ -184,6 +196,11 @@ class ColumnBuilder {
|
|
|
184
196
|
};
|
|
185
197
|
}
|
|
186
198
|
serial() {
|
|
199
|
+
// See TYPE_MAP: `serial` is SERIAL (int4) as of 0.24.0. Use bigserial() for 64-bit.
|
|
200
|
+
this._config.type = 'SERIAL';
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
bigserial() {
|
|
187
204
|
this._config.type = 'BIGSERIAL';
|
|
188
205
|
return this;
|
|
189
206
|
}
|
|
@@ -216,6 +233,10 @@ class ColumnBuilder {
|
|
|
216
233
|
this._config.type = 'TIMESTAMPTZ';
|
|
217
234
|
return this;
|
|
218
235
|
}
|
|
236
|
+
timestamptz() {
|
|
237
|
+
this._config.type = 'TIMESTAMPTZ';
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
219
240
|
date() {
|
|
220
241
|
this._config.type = 'DATE';
|
|
221
242
|
return this;
|
|
@@ -224,6 +245,10 @@ class ColumnBuilder {
|
|
|
224
245
|
this._config.type = 'JSONB';
|
|
225
246
|
return this;
|
|
226
247
|
}
|
|
248
|
+
jsonb() {
|
|
249
|
+
this._config.type = 'JSONB';
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
227
252
|
uuid() {
|
|
228
253
|
this._config.type = 'UUID';
|
|
229
254
|
return this;
|
|
@@ -276,14 +301,17 @@ exports.ColumnBuilder = ColumnBuilder;
|
|
|
276
301
|
/** Type guard: is `prop` a known nullary ColumnBuilder type method? */
|
|
277
302
|
function isNullaryColumnType(prop) {
|
|
278
303
|
return (prop === 'serial' ||
|
|
304
|
+
prop === 'bigserial' ||
|
|
279
305
|
prop === 'bigint' ||
|
|
280
306
|
prop === 'integer' ||
|
|
281
307
|
prop === 'smallint' ||
|
|
282
308
|
prop === 'text' ||
|
|
283
309
|
prop === 'boolean' ||
|
|
284
310
|
prop === 'timestamp' ||
|
|
311
|
+
prop === 'timestamptz' ||
|
|
285
312
|
prop === 'date' ||
|
|
286
313
|
prop === 'json' ||
|
|
314
|
+
prop === 'jsonb' ||
|
|
287
315
|
prop === 'uuid' ||
|
|
288
316
|
prop === 'real' ||
|
|
289
317
|
prop === 'doublePrecision' ||
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -16,6 +16,15 @@ exports.schemaToSQLString = schemaToSQLString;
|
|
|
16
16
|
const pg_1 = __importDefault(require("pg"));
|
|
17
17
|
const dialect_js_1 = require("./dialect.js");
|
|
18
18
|
const schema_js_1 = require("./schema.js");
|
|
19
|
+
/**
|
|
20
|
+
* Whether a resolved column type is an auto-increment pseudo-type (SERIAL /
|
|
21
|
+
* BIGSERIAL). These carry an implicit sequence default and NOT NULL, and their
|
|
22
|
+
* underlying integer width (int4 vs int8) is never auto-migrated by diff — a
|
|
23
|
+
* width change on a live PK is destructive and must be done by hand.
|
|
24
|
+
*/
|
|
25
|
+
function isSerialType(type) {
|
|
26
|
+
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
27
|
+
}
|
|
19
28
|
// ---------------------------------------------------------------------------
|
|
20
29
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
21
30
|
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +172,7 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
|
|
|
163
172
|
// 2. Is a serial (BIGSERIAL implies NOT NULL), OR
|
|
164
173
|
// 3. Has a primary key (PKs are NOT NULL)
|
|
165
174
|
// A column is left nullable if .nullable() was called.
|
|
166
|
-
const isSerial = config.type
|
|
175
|
+
const isSerial = isSerialType(config.type);
|
|
167
176
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
168
177
|
const notNull = config.isNotNull && !implicitNotNull;
|
|
169
178
|
// DEFAULT
|
|
@@ -352,9 +361,13 @@ async function schemaDiff(schema, connectionString) {
|
|
|
352
361
|
result.reverseStatements.unshift(reverseSql);
|
|
353
362
|
continue;
|
|
354
363
|
}
|
|
355
|
-
// Check type mismatch
|
|
364
|
+
// Check type mismatch. Serial/bigserial columns are exempt: their
|
|
365
|
+
// underlying int4/int8 width must never be auto-altered on a live table
|
|
366
|
+
// (a downcast on a PK loses data / breaks the sequence). This also
|
|
367
|
+
// preserves back-compat for DBs whose `serial` columns were created as
|
|
368
|
+
// BIGSERIAL (int8) before 0.24.0 — `push` won't try to shrink them.
|
|
356
369
|
const expectedUdt = schemaTypeToUdt(config);
|
|
357
|
-
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
370
|
+
if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
|
|
358
371
|
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
359
372
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
360
373
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
@@ -364,7 +377,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
364
377
|
result.reverseStatements.unshift(reverseSql);
|
|
365
378
|
}
|
|
366
379
|
// Check NOT NULL mismatch
|
|
367
|
-
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type
|
|
380
|
+
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || isSerialType(config.type);
|
|
368
381
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
369
382
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
370
383
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
@@ -381,7 +394,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
381
394
|
result.reverseStatements.unshift(reverseSql);
|
|
382
395
|
}
|
|
383
396
|
// Check DEFAULT value mismatch
|
|
384
|
-
const isSerial = config.type
|
|
397
|
+
const isSerial = isSerialType(config.type);
|
|
385
398
|
if (!isSerial) {
|
|
386
399
|
const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
|
|
387
400
|
const dbDefault = dbCol.columnDefault;
|
|
@@ -460,6 +473,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
460
473
|
*/
|
|
461
474
|
function schemaTypeToUdt(config) {
|
|
462
475
|
const map = {
|
|
476
|
+
SERIAL: 'int4',
|
|
463
477
|
BIGSERIAL: 'int8',
|
|
464
478
|
BIGINT: 'int8',
|
|
465
479
|
INTEGER: 'int4',
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -33,6 +33,20 @@ export interface TurbineCliConfig {
|
|
|
33
33
|
*/
|
|
34
34
|
adapter?: import('../adapters/index.js').DatabaseAdapter;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Alias for {@link TurbineCliConfig}. Some docs and examples import the config
|
|
38
|
+
* type as `TurbineConfig`; both names refer to the same shape.
|
|
39
|
+
*/
|
|
40
|
+
export type TurbineConfig = TurbineCliConfig;
|
|
41
|
+
/**
|
|
42
|
+
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
43
|
+
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
44
|
+
* to introspect (default `public`); the schema-builder file goes in `schemaFile`.
|
|
45
|
+
* A value containing a path separator or a JS/TS extension is almost certainly a
|
|
46
|
+
* mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
|
|
47
|
+
* silently matches zero tables. Used by `turbine generate` to fail loudly.
|
|
48
|
+
*/
|
|
49
|
+
export declare function looksLikeSchemaFilePath(schema: string): boolean;
|
|
36
50
|
/**
|
|
37
51
|
* Attempt to load a turbine config file from the current directory.
|
|
38
52
|
* Returns the config if found, or an empty object.
|
|
@@ -66,4 +80,3 @@ export interface CliOverrides {
|
|
|
66
80
|
*/
|
|
67
81
|
export declare function resolveConfig(fileConfig: TurbineCliConfig, overrides: CliOverrides): ResolvedConfig;
|
|
68
82
|
export declare function configTemplate(connectionString?: string): string;
|
|
69
|
-
//# sourceMappingURL=config.d.ts.map
|
package/dist/cli/config.js
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
+
/**
|
|
11
|
+
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
12
|
+
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
13
|
+
* to introspect (default `public`); the schema-builder file goes in `schemaFile`.
|
|
14
|
+
* A value containing a path separator or a JS/TS extension is almost certainly a
|
|
15
|
+
* mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
|
|
16
|
+
* silently matches zero tables. Used by `turbine generate` to fail loudly.
|
|
17
|
+
*/
|
|
18
|
+
export function looksLikeSchemaFilePath(schema) {
|
|
19
|
+
if (!schema)
|
|
20
|
+
return false;
|
|
21
|
+
return schema.includes('/') || schema.includes('\\') || /\.(ts|mts|cts|js|mjs|cjs|json)$/i.test(schema);
|
|
22
|
+
}
|
|
10
23
|
// ---------------------------------------------------------------------------
|
|
11
24
|
// Config file names, in priority order
|
|
12
25
|
// ---------------------------------------------------------------------------
|
|
@@ -110,4 +123,3 @@ ${urlLine}
|
|
|
110
123
|
export default config;
|
|
111
124
|
`;
|
|
112
125
|
}
|
|
113
|
-
//# sourceMappingURL=config.js.map
|
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
26
26
|
import { generate } from '../generate.js';
|
|
27
27
|
import { introspect } from '../introspect.js';
|
|
28
28
|
import { schemaDiff, schemaPush } from '../schema-sql.js';
|
|
29
|
-
import { configTemplate, findConfigFile, loadConfig, resolveConfig } from './config.js';
|
|
29
|
+
import { configTemplate, findConfigFile, loadConfig, looksLikeSchemaFilePath, resolveConfig } from './config.js';
|
|
30
30
|
import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
|
|
31
31
|
import { createMigration, listMigrationFiles, migrateDown, migrateStatus, migrateUp } from './migrate.js';
|
|
32
32
|
import { startObserve } from './observe.js';
|
|
@@ -85,6 +85,9 @@ function parseArgs() {
|
|
|
85
85
|
case '--allow-drift':
|
|
86
86
|
result.allowDrift = true;
|
|
87
87
|
break;
|
|
88
|
+
case '--allow-empty':
|
|
89
|
+
result.allowEmpty = true;
|
|
90
|
+
break;
|
|
88
91
|
case '--force':
|
|
89
92
|
case '-f':
|
|
90
93
|
result.force = true;
|
|
@@ -207,10 +210,28 @@ async function loadSchemaFile(schemaFile) {
|
|
|
207
210
|
newline();
|
|
208
211
|
console.log(` ${dim('Hint: install')} ${cyan('tsx')} ${dim('to load .ts files:')} ${cyan('npm install --save-dev tsx')}`);
|
|
209
212
|
}
|
|
213
|
+
printCjsHintIfApplicable(err);
|
|
210
214
|
}
|
|
211
215
|
process.exit(1);
|
|
212
216
|
}
|
|
213
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* When a config/schema import blows up with the CommonJS-vs-ESM interop error
|
|
220
|
+
* (`Cannot require() ES Module` / `ERR_REQUIRE_ESM`), the root cause is almost
|
|
221
|
+
* always a project whose `package.json` lacks `"type": "module"`: tsx transpiles
|
|
222
|
+
* the `.ts` file to CJS and then can't `require()` Turbine's ESM build. Point the
|
|
223
|
+
* user at the one-line fix instead of leaving them with a raw Node stack trace.
|
|
224
|
+
*/
|
|
225
|
+
function printCjsHintIfApplicable(err) {
|
|
226
|
+
const msg = err.message;
|
|
227
|
+
if (msg.includes('ERR_REQUIRE_ESM') ||
|
|
228
|
+
msg.includes('require() of ES Module') ||
|
|
229
|
+
msg.includes('Cannot require() ES Module')) {
|
|
230
|
+
newline();
|
|
231
|
+
console.log(` ${dim('Hint: add')} ${cyan('"type": "module"')} ${dim('to your')} ${cyan('package.json')}${dim('.')}`);
|
|
232
|
+
console.log(` ${dim('Turbine is an ESM package; without it, Node/tsx tries to')} ${cyan('require()')} ${dim('it and fails.')}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
214
235
|
// ---------------------------------------------------------------------------
|
|
215
236
|
// Command: init
|
|
216
237
|
// ---------------------------------------------------------------------------
|
|
@@ -408,6 +429,24 @@ async function cmdGenerate(args, config) {
|
|
|
408
429
|
banner();
|
|
409
430
|
const url = requireUrl(config);
|
|
410
431
|
const startTime = performance.now();
|
|
432
|
+
// Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
|
|
433
|
+
// NOT the path to your schema-builder file — that goes in `schemaFile`. If the
|
|
434
|
+
// configured `schema` looks like a file path, introspection would silently
|
|
435
|
+
// match zero tables and emit an empty client. Fail loudly instead.
|
|
436
|
+
if (!args.allowEmpty && looksLikeSchemaFilePath(config.schema)) {
|
|
437
|
+
error(`The "schema" option looks like a file path: ${cyan(config.schema)}`);
|
|
438
|
+
newline();
|
|
439
|
+
console.log(` ${dim('"schema" is the Postgres schema NAME to introspect')} ${dim('(default:')} ${cyan('public')}${dim(').')}`);
|
|
440
|
+
console.log(` ${dim('The path to your defineSchema() file belongs in')} ${cyan('schemaFile')}${dim('.')}`);
|
|
441
|
+
newline();
|
|
442
|
+
console.log(` ${dim('Fix your')} ${cyan('turbine.config.ts')}${dim(':')}`);
|
|
443
|
+
console.log(` ${green('schema:')} ${cyan("'public'")}${dim(", // or omit — introspects the 'public' schema")}`);
|
|
444
|
+
console.log(` ${green('schemaFile:')} ${cyan(`'${config.schema}'`)}${dim(', // your defineSchema() file (used by `turbine push`)')}`);
|
|
445
|
+
newline();
|
|
446
|
+
console.log(` ${dim('Re-run with')} ${cyan('--allow-empty')} ${dim('to introspect this literal schema name anyway.')}`);
|
|
447
|
+
newline();
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
411
450
|
label('Database', redactUrl(url));
|
|
412
451
|
label('Schema', config.schema);
|
|
413
452
|
label('Output', config.out);
|
|
@@ -424,6 +463,24 @@ async function cmdGenerate(args, config) {
|
|
|
424
463
|
const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
|
|
425
464
|
const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
|
|
426
465
|
spinner.succeed(`Found ${bold(String(tableNames.length))} tables, ${bold(String(totalColumns))} columns, ${bold(String(totalRelations))} relations`);
|
|
466
|
+
// Guard: zero tables means the generated client would be empty. That is almost
|
|
467
|
+
// always a misconfiguration (wrong `schema`, an include/exclude that filtered
|
|
468
|
+
// everything, or a database with no tables yet) rather than intent. Fail loudly
|
|
469
|
+
// instead of silently emitting an empty typed client.
|
|
470
|
+
if (tableNames.length === 0 && !args.allowEmpty) {
|
|
471
|
+
newline();
|
|
472
|
+
error(`Introspection matched 0 tables in schema ${cyan(config.schema)} — refusing to generate an empty client.`);
|
|
473
|
+
newline();
|
|
474
|
+
console.log(` ${dim('Common causes:')}`);
|
|
475
|
+
console.log(` ${dim('•')} ${cyan('schema')} ${dim('points at the wrong Postgres namespace')} ${dim('(it is the schema NAME, default')} ${cyan('public')}${dim(').')}`);
|
|
476
|
+
console.log(` ${dim('•')} ${dim('You meant to set')} ${cyan('schemaFile')} ${dim('(your defineSchema() file), not')} ${cyan('schema')}${dim('.')}`);
|
|
477
|
+
console.log(` ${dim('•')} ${cyan('include')}/${cyan('exclude')} ${dim('filtered out every table.')}`);
|
|
478
|
+
console.log(` ${dim('•')} ${dim('The database has no tables yet — run')} ${cyan('turbine push')} ${dim('or a migration first.')}`);
|
|
479
|
+
newline();
|
|
480
|
+
console.log(` ${dim('If an empty client is genuinely what you want, re-run with')} ${cyan('--allow-empty')}${dim('.')}`);
|
|
481
|
+
newline();
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
427
484
|
// Print table summary
|
|
428
485
|
if (args.verbose) {
|
|
429
486
|
newline();
|
|
@@ -1099,6 +1156,7 @@ function showGenerateHelp() {
|
|
|
1099
1156
|
console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
|
|
1100
1157
|
console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
|
|
1101
1158
|
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
1159
|
+
console.log(` ${cyan('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1102
1160
|
newline();
|
|
1103
1161
|
}
|
|
1104
1162
|
function showPushHelp() {
|
|
@@ -1307,6 +1365,8 @@ async function main() {
|
|
|
1307
1365
|
catch (err) {
|
|
1308
1366
|
if (args.command !== 'init') {
|
|
1309
1367
|
warn(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
|
|
1368
|
+
if (err instanceof Error)
|
|
1369
|
+
printCjsHintIfApplicable(err);
|
|
1310
1370
|
}
|
|
1311
1371
|
}
|
|
1312
1372
|
const overrides = {
|
|
@@ -1398,4 +1458,3 @@ async function main() {
|
|
|
1398
1458
|
}
|
|
1399
1459
|
}
|
|
1400
1460
|
main();
|
|
1401
|
-
//# sourceMappingURL=index.js.map
|
package/dist/cli/loader.d.ts
CHANGED
|
@@ -59,4 +59,3 @@ export declare function getTsLoaderError(): string | null;
|
|
|
59
59
|
export declare function registerTsLoader(): Promise<TsLoaderStatus>;
|
|
60
60
|
/** Reset the loader state — used by unit tests only. */
|
|
61
61
|
export declare function _resetTsLoaderStateForTests(): void;
|
|
62
|
-
//# sourceMappingURL=loader.d.ts.map
|
package/dist/cli/loader.js
CHANGED
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -150,4 +150,3 @@ export declare function migrateDown(connectionString: string, migrationsDir: str
|
|
|
150
150
|
export declare function migrateStatus(connectionString: string, migrationsDir: string, options?: {
|
|
151
151
|
dialect?: Dialect;
|
|
152
152
|
}): Promise<MigrationStatus[]>;
|
|
153
|
-
//# sourceMappingURL=migrate.d.ts.map
|
package/dist/cli/migrate.js
CHANGED
package/dist/cli/observe-ui.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
1
|
export declare const OBSERVE_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark\" />\n <title>Turbine Observe</title>\n <style>\n :root {\n --bg: #0a0a0b;\n --bg-elev: #111113;\n --bg-hover: #1a1a1d;\n --border: #26262b;\n --text: #e6e6ea;\n --text-dim: #8a8a93;\n --accent: #60a5fa;\n --green: #4ade80;\n --red: #f87171;\n --orange: #fb923c;\n --purple: #a78bfa;\n --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n --sans: system-ui, -apple-system, sans-serif;\n --radius: 6px;\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { background: var(--bg); color: var(--text); font-family: var(--sans); font-size: 14px; padding: 24px; }\n h1 { font-size: 20px; margin-bottom: 4px; }\n .subtitle { color: var(--text-dim); margin-bottom: 24px; }\n .controls { display: flex; gap: 8px; margin-bottom: 24px; }\n .controls button {\n background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius);\n color: var(--text); padding: 6px 12px; cursor: pointer; font-size: 13px;\n }\n .controls button.active { border-color: var(--accent); color: var(--accent); }\n .card {\n background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius);\n padding: 16px; margin-bottom: 16px;\n }\n .card h2 { font-size: 14px; color: var(--text-dim); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }\n table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 12px; }\n th { text-align: left; padding: 6px 8px; color: var(--text-dim); border-bottom: 1px solid var(--border); }\n td { padding: 6px 8px; border-bottom: 1px solid var(--border); }\n .num { text-align: right; }\n .error-rate { color: var(--red); }\n .low-error { color: var(--green); }\n svg { width: 100%; height: 200px; }\n .chart-line { fill: none; stroke-width: 1.5; }\n .line-avg { stroke: var(--accent); }\n .line-p95 { stroke: var(--orange); }\n .line-p99 { stroke: var(--red); }\n .legend { display: flex; gap: 16px; margin-top: 8px; font-size: 12px; color: var(--text-dim); }\n .legend span::before { content: ''; display: inline-block; width: 12px; height: 2px; margin-right: 4px; vertical-align: middle; }\n .legend .l-avg::before { background: var(--accent); }\n .legend .l-p95::before { background: var(--orange); }\n .legend .l-p99::before { background: var(--red); }\n .empty { color: var(--text-dim); text-align: center; padding: 40px; }\n </style>\n</head>\n<body>\n <h1>Turbine Observe</h1>\n <p class=\"subtitle\">Query performance metrics</p>\n <div class=\"controls\">\n <button data-range=\"1h\" class=\"active\">1h</button>\n <button data-range=\"6h\">6h</button>\n <button data-range=\"24h\">24h</button>\n <button data-range=\"7d\">7d</button>\n </div>\n <div class=\"card\" id=\"latency-card\">\n <h2>Latency over time</h2>\n <div id=\"chart\"></div>\n <div class=\"legend\">\n <span class=\"l-avg\">avg</span>\n <span class=\"l-p95\">p95</span>\n <span class=\"l-p99\">p99</span>\n </div>\n </div>\n <div class=\"card\" id=\"models-card\">\n <h2>Top models</h2>\n <div id=\"models-table\"></div>\n </div>\n <div class=\"card\" id=\"errors-card\">\n <h2>Error rates</h2>\n <div id=\"errors-table\"></div>\n </div>\n <script>\n let currentRange = '1h';\n const token = document.cookie.match(/turbine_observe_token=([a-f0-9]+)/)?.[1] || '';\n const headers = { 'x-turbine-token': token };\n\n document.querySelector('.controls').addEventListener('click', e => {\n if (e.target.tagName !== 'BUTTON') return;\n document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));\n e.target.classList.add('active');\n currentRange = e.target.dataset.range;\n refresh();\n });\n\n async function fetchJson(path) {\n const res = await fetch(path, { headers });\n if (!res.ok) return null;\n return res.json();\n }\n\n function buildSvgPath(points, width, height, maxY) {\n if (points.length === 0) return '';\n const xStep = width / Math.max(points.length - 1, 1);\n return points.map((y, i) => {\n const px = i * xStep;\n const py = height - (y / maxY) * height;\n return (i === 0 ? 'M' : 'L') + px.toFixed(1) + ',' + py.toFixed(1);\n }).join(' ');\n }\n\n function renderChart(data) {\n const el = document.getElementById('chart');\n if (!data || data.length === 0) { el.innerHTML = '<p class=\"empty\">No data yet</p>'; return; }\n const width = 800; const height = 180;\n const allVals = data.flatMap(d => [d.avg_ms, d.p95_ms, d.p99_ms]);\n const maxY = Math.max(...allVals, 1) * 1.1;\n const avgPath = buildSvgPath(data.map(d => d.avg_ms), width, height, maxY);\n const p95Path = buildSvgPath(data.map(d => d.p95_ms), width, height, maxY);\n const p99Path = buildSvgPath(data.map(d => d.p99_ms), width, height, maxY);\n el.innerHTML = '<svg viewBox=\"0 0 ' + width + ' ' + height + '\" preserveAspectRatio=\"none\">'\n + '<path class=\"chart-line line-avg\" d=\"' + avgPath + '\"/>'\n + '<path class=\"chart-line line-p95\" d=\"' + p95Path + '\"/>'\n + '<path class=\"chart-line line-p99\" d=\"' + p99Path + '\"/>'\n + '</svg>';\n }\n\n function escapeHtml(s) {\n if (s == null) return '';\n return String(s)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n function renderModels(data) {\n const el = document.getElementById('models-table');\n if (!data || data.length === 0) { el.innerHTML = '<p class=\"empty\">No data yet</p>'; return; }\n let html = '<table><thead><tr><th>Model</th><th>Action</th><th class=\"num\">Count</th><th class=\"num\">Avg (ms)</th><th class=\"num\">P95 (ms)</th><th class=\"num\">P99 (ms)</th></tr></thead><tbody>';\n for (const row of data) {\n html += '<tr><td>' + escapeHtml(row.model) + '</td><td>' + escapeHtml(row.action) + '</td>'\n + '<td class=\"num\">' + row.count + '</td>'\n + '<td class=\"num\">' + row.avg_ms.toFixed(1) + '</td>'\n + '<td class=\"num\">' + row.p95_ms.toFixed(1) + '</td>'\n + '<td class=\"num\">' + row.p99_ms.toFixed(1) + '</td></tr>';\n }\n html += '</tbody></table>';\n el.innerHTML = html;\n }\n\n function renderErrors(data) {\n const el = document.getElementById('errors-table');\n if (!data || data.length === 0) { el.innerHTML = '<p class=\"empty\">No errors</p>'; return; }\n let html = '<table><thead><tr><th>Model</th><th>Action</th><th class=\"num\">Total</th><th class=\"num\">Errors</th><th class=\"num\">Rate</th></tr></thead><tbody>';\n for (const row of data) {\n const rate = row.count > 0 ? (row.error_count / row.count * 100).toFixed(1) : '0.0';\n const cls = parseFloat(rate) > 5 ? 'error-rate' : 'low-error';\n html += '<tr><td>' + escapeHtml(row.model) + '</td><td>' + escapeHtml(row.action) + '</td>'\n + '<td class=\"num\">' + row.count + '</td>'\n + '<td class=\"num\">' + row.error_count + '</td>'\n + '<td class=\"num ' + cls + '\">' + rate + '%</td></tr>';\n }\n html += '</tbody></table>';\n el.innerHTML = html;\n }\n\n async function refresh() {\n const [latency, models] = await Promise.all([\n fetchJson('/api/latency?range=' + currentRange),\n fetchJson('/api/models?range=' + currentRange),\n ]);\n renderChart(latency);\n renderModels(models);\n // Derive errors from models data\n const withErrors = (models || []).filter(m => m.error_count > 0);\n renderErrors(withErrors);\n }\n\n refresh();\n setInterval(refresh, 60000);\n </script>\n</body>\n</html>";
|
|
2
|
-
//# sourceMappingURL=observe-ui.d.ts.map
|
package/dist/cli/observe-ui.js
CHANGED
package/dist/cli/observe.d.ts
CHANGED
package/dist/cli/observe.js
CHANGED