turbine-orm 0.91.0-next.e72e9b4 → 0.92.0-next.8724ade
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 +23 -24
- package/dist/cjs/cli/config.d.ts +5 -0
- package/dist/cjs/cli/config.js +1 -0
- package/dist/cjs/cli/index.d.ts +17 -17
- package/dist/cjs/cli/index.js +34 -27
- package/dist/cjs/cli/observe-ui.d.ts +1 -1
- package/dist/cjs/cli/observe-ui.js +1 -1
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/powdb.d.ts +10 -6
- package/dist/cjs/powdb.js +4 -2
- package/dist/cjs/query/builder.d.ts +8 -0
- package/dist/cjs/query/builder.js +31 -23
- package/dist/cjs/query/relations.d.ts +21 -3
- package/dist/cjs/query/relations.js +331 -68
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +44 -1
- package/dist/cjs/query/where.d.ts +15 -0
- package/dist/cjs/schema-builder.d.ts +8 -5
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +1 -0
- package/dist/cli/index.d.ts +17 -17
- package/dist/cli/index.js +35 -28
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +1 -1
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/powdb.d.ts +10 -6
- package/dist/powdb.js +4 -2
- package/dist/query/builder.d.ts +8 -0
- package/dist/query/builder.js +18 -10
- package/dist/query/relations.d.ts +21 -3
- package/dist/query/relations.js +330 -69
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +43 -0
- package/dist/query/where.d.ts +15 -0
- package/dist/schema-builder.d.ts +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,39 +21,38 @@ Six reasons, each with the mechanism that makes it true:
|
|
|
21
21
|
1. **One dependency.** `dependencies` is `{ "pg": "^8.15.0" }`. No engine binary, no WASM compiler, no adapter packages in lockstep. The optional engines (SQLite, MySQL, SQL Server, PowDB, all tiered Experimental in [STABILITY.md](STABILITY.md)) are peer dependencies or Node builtins you install only if you use them.
|
|
22
22
|
2. **Written from scratch.** Turbine is not a layer over Knex or a query-builder library. Query compilation is plain string building with an FNV-1a shape fingerprint into a bounded LRU of SQL templates, so there is no plan cache to size and no compiler running on your event loop.
|
|
23
23
|
3. **Nested relations in one statement.** A `with` clause compiles to correlated `json_agg` subqueries, so users with posts with comments is one round trip, typed end to end: `users[0].posts[0].comments[0].author.name` autocompletes with no annotation.
|
|
24
|
-
4. **
|
|
24
|
+
4. **Faster than Drizzle and Prisma.** In the 0.91.0 run Drizzle took 1.4x as long as Turbine and Prisma 2.0x, by geometric mean, on a dedicated benchmark machine. The table is below; the losses are stated with the wins.
|
|
25
25
|
5. **Agents get a skill and typed tools, not a SQL prompt.** `npx turbine skill` installs a query-writing skill whose every claim is executed against a live database before release; `npx turbine mcp` exposes eleven read-only MCP tools, including a relation graph and a join-path finder that returns the `with` clause to write. Every tool runs inside `BEGIN READ ONLY`, and PII-tagged columns are redacted before rows reach a model.
|
|
26
26
|
6. **The dangerous operations ask first.** Destructive migration statements refuse to run without typed consent. `update`/`delete` with an empty `where` throws. Columns tagged `pii: true` are excluded from the emitted SQL's projections. `turbine doctor` reports missing FK indexes offline, no account, no telemetry.
|
|
27
27
|
|
|
28
28
|
## Benchmarks
|
|
29
29
|
|
|
30
|
-
Measured **2026-
|
|
30
|
+
Measured **2026-10-15 against turbine-orm 0.91.0**, versus **Drizzle 0.45.3** (relational queries) and **Prisma 7.10.0** (`@prisma/adapter-pg`, `relationJoins` on), with a hand-written `pg` control arm. Run by the repository's [`benchmarks.yml`](.github/workflows/benchmarks.yml) workflow on a dedicated machine: AMD Ryzen 9 7950X host, 4-vCPU 16 GB Firecracker VM, client and PostgreSQL 17 (Docker) on the same VM over a Unix socket. PostgreSQL 17.11, Node v24.18.0, `pg` 8.23.1 under every arm. Same schema, same data (1K users, 10K posts, 50K comments), same pool config. Each figure is the median over 200 rounds with arm order rotated per round, taken as the median of 3 full runs.
|
|
31
31
|
|
|
32
|
-
| Scenario | Turbine 0.
|
|
32
|
+
| Scenario | Turbine 0.91.0 | Drizzle 0.45.3 | Prisma 7.10.0 | raw pg |
|
|
33
33
|
|---|---|---|---|---|
|
|
34
|
-
| findMany, 100 users
|
|
35
|
-
| findMany, 50 users + posts
|
|
36
|
-
| findMany, 10 users
|
|
37
|
-
| findUnique
|
|
38
|
-
| findUnique, user + posts + comments
|
|
39
|
-
| count, all users | **0.
|
|
40
|
-
| stream,
|
|
41
|
-
| atomic increment
|
|
42
|
-
| pipeline, 5-query batch | **0.
|
|
43
|
-
| hot findUnique,
|
|
34
|
+
| findMany, 100 users | **0.204 ms** | 0.307 ms | 0.349 ms | 0.198 ms |
|
|
35
|
+
| findMany, 50 users + posts *(tie)* | 1.470 ms | 1.488 ms | 2.930 ms | 0.791 ms |
|
|
36
|
+
| findMany, 10 users + posts + comments | **0.795 ms** | 0.899 ms | 2.303 ms | n/a |
|
|
37
|
+
| findUnique by primary key | **0.090 ms** | 0.151 ms | 0.179 ms | 0.074 ms |
|
|
38
|
+
| findUnique, user + posts + comments | **0.278 ms** | 0.467 ms | 0.599 ms | n/a |
|
|
39
|
+
| count, all users | **0.080 ms** | 0.115 ms | 0.170 ms | 0.077 ms |
|
|
40
|
+
| stream 50,000 comments, per row *(tie)* | 55.27 ms | 57.33 ms | 85.99 ms | 56.38 ms |
|
|
41
|
+
| atomic increment | **0.175 ms** | 0.185 ms | 0.274 ms | 0.143 ms |
|
|
42
|
+
| pipeline, 5-query batch | **0.333 ms** | 0.707 ms | 0.782 ms | 0.365 ms |
|
|
43
|
+
| hot findUnique, rotating ids | **0.078 ms** | 0.143 ms | 0.159 ms | 0.072 ms |
|
|
44
44
|
|
|
45
45
|
What that run says:
|
|
46
46
|
|
|
47
|
-
- **
|
|
48
|
-
-
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
-
|
|
52
|
-
- The sub-0.15 ms scenarios carry roughly one third uncertainty in their absolute values. Orderings held across runs; margins should not be quoted, and that includes L2's 2.5%.
|
|
47
|
+
- **Across all ten scenarios Drizzle took 1.40x as long as Turbine and Prisma 2.00x**, by geometric mean. Turbine won or tied every one against Drizzle (2 ties) and won all ten against Prisma.
|
|
48
|
+
- **Against hand-written `pg`, Turbine ran at 1.14x, Drizzle at 1.61x and Prisma at 2.17x** (geometric mean over the 8 scenarios with a control). Where a query returns nested rows, the control decodes them into objects with `Date` timestamps as the ORMs do. The 0.71.0 figure of 1.08x was adjusted by hand against a control that did more work than Turbine; this one is not adjusted.
|
|
49
|
+
- **Pipelining is the clearest win over a network**: five dashboard queries 25 ms from the database take 25 ms as one batch against 171 ms as a transaction.
|
|
50
|
+
- **Where Turbine loses:** deep `OFFSET` pagination unless `forceCustomPlan` is set, and per-row streaming against the Drizzle 1.0 release candidate (`findManyStreamBatches` ties it).
|
|
51
|
+
- Figures under 0.15 ms carry roughly one third uncertainty in their absolute values. Orderings held across runs; those margins should not be quoted.
|
|
53
52
|
|
|
54
|
-
Full method, the
|
|
53
|
+
Full method, the extra scenarios, streaming, latency and the losses: [`benchmarks/RESULTS-0.91.0.md`](https://github.com/zvndev/turbine-orm/blob/main/benchmarks/RESULTS-0.91.0.md), or the charts at [turbineorm.dev/benchmarks](https://turbineorm.dev/benchmarks).
|
|
55
54
|
|
|
56
|
-
Reproduce it
|
|
55
|
+
Reproduce it on a fresh machine with `gh workflow run benchmarks.yml`, or locally with `benchmarks/run-round.sh` (see [`benchmarks/README.md`](benchmarks/README.md)).
|
|
57
56
|
|
|
58
57
|
## Quick Start
|
|
59
58
|
|
|
@@ -272,7 +271,7 @@ Going deep on one database means the parts other ORMs push to raw SQL are typed
|
|
|
272
271
|
|
|
273
272
|
## Serverless and edge
|
|
274
273
|
|
|
275
|
-
The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **
|
|
274
|
+
The core is driver-agnostic: hand any pg-compatible pool to `turbineHttp()` and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under **149 kB brotli** (edge entry under **117 kB**) with `pg` external, enforced by `size-limit` in CI at those exact numbers; run `npm run size` for the current figure.
|
|
276
275
|
|
|
277
276
|
```typescript
|
|
278
277
|
import { Pool } from '@neondatabase/serverless';
|
|
@@ -359,11 +358,11 @@ It is also built to be extended rather than wrapped. All SQL generation routes t
|
|
|
359
358
|
|---|---|---|---|---|---|
|
|
360
359
|
| **Engine / runtime** | No engine binary (`pg` only) | Client + TS/WASM query compiler | No engine | No engine | Kysely as query runner; SQL built by MikroORM (v7) |
|
|
361
360
|
| **Runtime deps** | 1 (`pg`) | `@prisma/client` + required driver adapter | 0 | 0 | `@mikro-orm/core` (0 deps) + a driver package |
|
|
362
|
-
| **Main bundle (brotli)** | under
|
|
361
|
+
| **Main bundle (brotli)** | under 149 kB import graph (CI-enforced), `pg` external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
|
|
363
362
|
| **Studio** | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; [Gateway](https://gateway.drizzle.team/) self-hosted, free | None | AdminJS integration guide (CRUD panel) |
|
|
364
363
|
| **Error PII safety** | Keys only by default | Values in messages | Raw pg errors | Raw pg errors | |
|
|
365
364
|
| **Migrations** | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None | Generated by diffing entities against a snapshot or the database; TS/JS classes |
|
|
366
|
-
| **Edge runtime** | One import swap, under
|
|
365
|
+
| **Edge runtime** | One import swap, under 117 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
|
|
367
366
|
| **Pipeline batching** | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual | `flush()` runs queued changes in one transaction |
|
|
368
367
|
| **Typed errors** | `isRetryable` discriminant | Error codes only | None | None | `DriverException` subclasses (`UniqueConstraintViolationException`, ...) |
|
|
369
368
|
| **Nested relations** | 1 query, deep type inference | 1 query per relation by default; single-query `relationJoins` is Preview | 1 query, `relations()` re-declaration | Manual (`jsonArrayFrom`) | `populate`; default `balanced`: joins to-one, one query per to-many relation |
|
package/dist/cjs/cli/config.d.ts
CHANGED
|
@@ -189,6 +189,11 @@ export interface ResolvedConfig {
|
|
|
189
189
|
relationNames?: Record<string, Record<string, string>>;
|
|
190
190
|
/** Column type overrides for the generator, or undefined when none is declared. */
|
|
191
191
|
columnTypes?: Record<string, string>;
|
|
192
|
+
/**
|
|
193
|
+
* The config file's `adapter`, passed to `migrate up` / `deploy` / `down` so a
|
|
194
|
+
* CockroachDB or YugabyteDB project takes the migration lock its engine supports.
|
|
195
|
+
*/
|
|
196
|
+
adapter?: import('../adapters/index.js').DatabaseAdapter;
|
|
192
197
|
}
|
|
193
198
|
export interface CliOverrides {
|
|
194
199
|
url?: string;
|
package/dist/cjs/cli/config.js
CHANGED
|
@@ -231,6 +231,7 @@ function resolveConfig(fileConfig, overrides) {
|
|
|
231
231
|
importExtension: overrides.importExtension ?? fileConfig.importExtension ?? 'auto',
|
|
232
232
|
keepColumnNames: overrides.keepColumnNames ?? fileConfig.keepColumnNames ?? false,
|
|
233
233
|
legacyToManyUniques: overrides.legacyToManyUniques ?? fileConfig.legacyToManyUniques ?? false,
|
|
234
|
+
adapter: fileConfig.adapter,
|
|
234
235
|
};
|
|
235
236
|
}
|
|
236
237
|
/**
|
package/dist/cjs/cli/index.d.ts
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* turbine-orm CLI
|
|
4
4
|
*
|
|
5
|
-
* Commands:
|
|
6
|
-
* turbine init
|
|
7
|
-
* turbine generate | pull
|
|
8
|
-
* turbine migrate-from-prisma
|
|
9
|
-
* turbine push
|
|
10
|
-
* turbine migrate create <name>
|
|
11
|
-
* turbine migrate up
|
|
12
|
-
* turbine migrate deploy
|
|
13
|
-
* turbine migrate down
|
|
14
|
-
* turbine migrate status
|
|
15
|
-
* turbine seed
|
|
16
|
-
* turbine status
|
|
17
|
-
* turbine doctor
|
|
18
|
-
* turbine studio
|
|
19
|
-
* turbine mcp
|
|
20
|
-
* turbine observe
|
|
21
|
-
* turbine skill
|
|
5
|
+
* Commands (each has its own `--help`, which is the full flag list):
|
|
6
|
+
* turbine init Initialize a Turbine project
|
|
7
|
+
* turbine generate | pull Introspect the database and generate TypeScript types
|
|
8
|
+
* turbine migrate-from-prisma Parse a schema.prisma and emit a Prisma->Turbine name map + report
|
|
9
|
+
* turbine push Apply defineSchema() definitions to the database (destructive ops gated)
|
|
10
|
+
* turbine migrate create <name> Create a SQL migration file (--auto | --from-diff | --recipe <name>)
|
|
11
|
+
* turbine migrate up Apply pending migrations
|
|
12
|
+
* turbine migrate deploy Apply pending migrations without prompts
|
|
13
|
+
* turbine migrate down Roll back the last migration
|
|
14
|
+
* turbine migrate status Show migration status
|
|
15
|
+
* turbine seed Run the seed file
|
|
16
|
+
* turbine status Show a schema summary
|
|
17
|
+
* turbine doctor Index + cached-plan triage
|
|
18
|
+
* turbine studio Launch the local read-only web UI (--demo for a seeded sample DB)
|
|
19
|
+
* turbine mcp Start the read-only MCP server over JSON-RPC stdio
|
|
20
|
+
* turbine observe Launch the metrics dashboard (--metrics-url or TURBINE_OBSERVE_URL)
|
|
21
|
+
* turbine skill Install the agent query skill
|
|
22
22
|
*
|
|
23
23
|
* Usage:
|
|
24
24
|
* DATABASE_URL=postgres://... npx turbine generate
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* turbine-orm CLI
|
|
5
5
|
*
|
|
6
|
-
* Commands:
|
|
7
|
-
* turbine init
|
|
8
|
-
* turbine generate | pull
|
|
9
|
-
* turbine migrate-from-prisma
|
|
10
|
-
* turbine push
|
|
11
|
-
* turbine migrate create <name>
|
|
12
|
-
* turbine migrate up
|
|
13
|
-
* turbine migrate deploy
|
|
14
|
-
* turbine migrate down
|
|
15
|
-
* turbine migrate status
|
|
16
|
-
* turbine seed
|
|
17
|
-
* turbine status
|
|
18
|
-
* turbine doctor
|
|
19
|
-
* turbine studio
|
|
20
|
-
* turbine mcp
|
|
21
|
-
* turbine observe
|
|
22
|
-
* turbine skill
|
|
6
|
+
* Commands (each has its own `--help`, which is the full flag list):
|
|
7
|
+
* turbine init Initialize a Turbine project
|
|
8
|
+
* turbine generate | pull Introspect the database and generate TypeScript types
|
|
9
|
+
* turbine migrate-from-prisma Parse a schema.prisma and emit a Prisma->Turbine name map + report
|
|
10
|
+
* turbine push Apply defineSchema() definitions to the database (destructive ops gated)
|
|
11
|
+
* turbine migrate create <name> Create a SQL migration file (--auto | --from-diff | --recipe <name>)
|
|
12
|
+
* turbine migrate up Apply pending migrations
|
|
13
|
+
* turbine migrate deploy Apply pending migrations without prompts
|
|
14
|
+
* turbine migrate down Roll back the last migration
|
|
15
|
+
* turbine migrate status Show migration status
|
|
16
|
+
* turbine seed Run the seed file
|
|
17
|
+
* turbine status Show a schema summary
|
|
18
|
+
* turbine doctor Index + cached-plan triage
|
|
19
|
+
* turbine studio Launch the local read-only web UI (--demo for a seeded sample DB)
|
|
20
|
+
* turbine mcp Start the read-only MCP server over JSON-RPC stdio
|
|
21
|
+
* turbine observe Launch the metrics dashboard (--metrics-url or TURBINE_OBSERVE_URL)
|
|
22
|
+
* turbine skill Install the agent query skill
|
|
23
23
|
*
|
|
24
24
|
* Usage:
|
|
25
25
|
* DATABASE_URL=postgres://... npx turbine generate
|
|
@@ -2973,6 +2973,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
2973
2973
|
allowDestructive: args.allowDestructive,
|
|
2974
2974
|
onNoTransaction,
|
|
2975
2975
|
schema: config.schema,
|
|
2976
|
+
adapter: config.adapter,
|
|
2976
2977
|
});
|
|
2977
2978
|
}
|
|
2978
2979
|
catch (err) {
|
|
@@ -2991,6 +2992,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
2991
2992
|
allowDestructive: true,
|
|
2992
2993
|
onNoTransaction,
|
|
2993
2994
|
schema: config.schema,
|
|
2995
|
+
adapter: config.adapter,
|
|
2994
2996
|
});
|
|
2995
2997
|
}
|
|
2996
2998
|
if (result.applied.length > 0) {
|
|
@@ -3153,6 +3155,7 @@ async function cmdMigrateDeploy(args, config) {
|
|
|
3153
3155
|
const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, {
|
|
3154
3156
|
allowDrift: args.allowDrift,
|
|
3155
3157
|
schema: config.schema,
|
|
3158
|
+
adapter: config.adapter,
|
|
3156
3159
|
lockWaitMs: args.wait !== undefined ? args.wait * 1000 : undefined,
|
|
3157
3160
|
onLockWait: () => {
|
|
3158
3161
|
runSpinner.stop();
|
|
@@ -3264,6 +3267,7 @@ async function cmdMigrateDown(args, config) {
|
|
|
3264
3267
|
allowDestructive: args.allowDestructive,
|
|
3265
3268
|
allowDrift: args.allowDrift,
|
|
3266
3269
|
schema: config.schema,
|
|
3270
|
+
adapter: config.adapter,
|
|
3267
3271
|
});
|
|
3268
3272
|
}
|
|
3269
3273
|
catch (err) {
|
|
@@ -3281,6 +3285,7 @@ async function cmdMigrateDown(args, config) {
|
|
|
3281
3285
|
allowDestructive: true,
|
|
3282
3286
|
allowDrift: args.allowDrift,
|
|
3283
3287
|
schema: config.schema,
|
|
3288
|
+
adapter: config.adapter,
|
|
3284
3289
|
});
|
|
3285
3290
|
}
|
|
3286
3291
|
if (result.rolledBack.length > 0) {
|
|
@@ -4636,12 +4641,12 @@ async function cmdMcp(_args, config) {
|
|
|
4636
4641
|
// ---------------------------------------------------------------------------
|
|
4637
4642
|
async function cmdObserve(args) {
|
|
4638
4643
|
(0, ui_js_1.banner)();
|
|
4639
|
-
const url = process.env.TURBINE_OBSERVE_URL;
|
|
4644
|
+
const url = (0, config_js_1.firstNonEmpty)(args.metricsUrl, process.env.TURBINE_OBSERVE_URL);
|
|
4640
4645
|
if (!url) {
|
|
4641
|
-
(0, ui_js_1.error)('
|
|
4646
|
+
(0, ui_js_1.error)('The observe command needs the metrics database: pass --metrics-url or set TURBINE_OBSERVE_URL.');
|
|
4642
4647
|
(0, ui_js_1.errorLine)();
|
|
4643
|
-
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('
|
|
4644
|
-
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Example:')} ${(0, ui_js_1.cyan)('
|
|
4648
|
+
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Point it at the Postgres database where metrics are stored.')}`);
|
|
4649
|
+
(0, ui_js_1.errorLine)(`${(0, ui_js_1.dim)('Example:')} ${(0, ui_js_1.cyan)('npx turbine observe --metrics-url postgres://...')}`);
|
|
4645
4650
|
(0, ui_js_1.errorLine)();
|
|
4646
4651
|
process.exit(1);
|
|
4647
4652
|
}
|
|
@@ -4957,7 +4962,7 @@ function showDoctorHelp() {
|
|
|
4957
4962
|
console.log(` ${(0, ui_js_1.cyan)('--allow-pooler')} Run against a connection pooler anyway ${(0, ui_js_1.dim)('(refused by default)')}`);
|
|
4958
4963
|
(0, ui_js_1.newline)();
|
|
4959
4964
|
console.log(` ${(0, ui_js_1.dim)('doctor refuses a transaction-pooling endpoint (a')} ${(0, ui_js_1.cyan)('-pooler')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('pgbouncer')}`);
|
|
4960
|
-
console.log(` ${(0, ui_js_1.dim)('host, or port 6543). Point it at the direct endpoint instead: a pooler')}`);
|
|
4965
|
+
console.log(` ${(0, ui_js_1.dim)('host, or port 6543 or 6432). Point it at the direct endpoint instead: a pooler')}`);
|
|
4961
4966
|
console.log(` ${(0, ui_js_1.dim)('shares one server backend between clients, so neither the session state')}`);
|
|
4962
4967
|
console.log(` ${(0, ui_js_1.dim)('doctor needs nor the session-scoped views it cites belong to it.')}`);
|
|
4963
4968
|
(0, ui_js_1.newline)();
|
|
@@ -5015,9 +5020,10 @@ function showMcpHelp() {
|
|
|
5015
5020
|
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
5016
5021
|
console.log(` npx turbine mcp ${(0, ui_js_1.dim)('[options]')}`);
|
|
5017
5022
|
(0, ui_js_1.newline)();
|
|
5018
|
-
console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes
|
|
5019
|
-
console.log(` read-only tools: schema, relation graph, join paths
|
|
5020
|
-
console.log(` doctor, EXPLAIN, table stats, sample rows
|
|
5023
|
+
console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes eleven`);
|
|
5024
|
+
console.log(` read-only tools: schema overview, table detail, relation graph, join paths,`);
|
|
5025
|
+
console.log(` migration status, doctor, EXPLAIN, query compile, table stats, sample rows`);
|
|
5026
|
+
console.log(` and error lookup.`);
|
|
5021
5027
|
(0, ui_js_1.newline)();
|
|
5022
5028
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
5023
5029
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
@@ -5031,16 +5037,17 @@ function showObserveHelp() {
|
|
|
5031
5037
|
console.log(` ${(0, ui_js_1.bold)('turbine observe')}, Query metrics dashboard`);
|
|
5032
5038
|
(0, ui_js_1.newline)();
|
|
5033
5039
|
console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
|
|
5034
|
-
console.log(`
|
|
5040
|
+
console.log(` npx turbine observe --metrics-url postgres://... ${(0, ui_js_1.dim)('[options]')}`);
|
|
5035
5041
|
(0, ui_js_1.newline)();
|
|
5036
5042
|
console.log(` Reads the ${(0, ui_js_1.cyan)('_turbine_metrics')} table written by ${(0, ui_js_1.cyan)('db.$observe()')} and serves`);
|
|
5037
5043
|
console.log(` per-minute count / avg / p50 / p95 / p99 / error aggregates. Read-only,`);
|
|
5038
5044
|
console.log(` loopback-bound, behind a random per-session token, same model as Studio.`);
|
|
5039
5045
|
(0, ui_js_1.newline)();
|
|
5040
|
-
console.log(` ${(0, ui_js_1.dim)('The connection string comes from')} ${(0, ui_js_1.cyan)('TURBINE_OBSERVE_URL')}${(0, ui_js_1.dim)(', not')} ${(0, ui_js_1.cyan)('DATABASE_URL')}${(0, ui_js_1.dim)(':')}`);
|
|
5046
|
+
console.log(` ${(0, ui_js_1.dim)('The connection string comes from')} ${(0, ui_js_1.cyan)('--metrics-url')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('TURBINE_OBSERVE_URL')}${(0, ui_js_1.dim)(', not')} ${(0, ui_js_1.cyan)('DATABASE_URL')}${(0, ui_js_1.dim)(':')}`);
|
|
5041
5047
|
console.log(` ${(0, ui_js_1.dim)('metrics are meant to live in a different database from the one they measure.')}`);
|
|
5042
5048
|
(0, ui_js_1.newline)();
|
|
5043
5049
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
5050
|
+
console.log(` ${(0, ui_js_1.cyan)('--metrics-url')} ${(0, ui_js_1.dim)('<url>')} Metrics database ${(0, ui_js_1.dim)('(default: TURBINE_OBSERVE_URL)')}`);
|
|
5044
5051
|
console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4984)')}`);
|
|
5045
5052
|
console.log(` ${(0, ui_js_1.cyan)('--host')} ${(0, ui_js_1.dim)('<addr>')} Bind address ${(0, ui_js_1.dim)('(default: 127.0.0.1)')}`);
|
|
5046
5053
|
console.log(` ${(0, ui_js_1.cyan)('--no-open')} Don't auto-open the browser`);
|
|
@@ -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: #
|
|
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: #6f9bf7;\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 nonce=\"__CSP_NONCE__\">\n let currentRange = '1h';\n // Anchored on a cookie boundary so a decoy cookie whose name ends with this\n // one cannot shadow the real token.\n const token = document.cookie.match(/(?:^|;\\s*)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 // Numeric sink: counts are integer aggregates, but they are still values\n // arriving over the wire, so coerce rather than interpolating them raw.\n function num(v) {\n const n = Number(v);\n return Number.isFinite(n) ? String(n) : '0';\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\">' + 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\">' + num(row.count) + '</td>'\n + '<td class=\"num\">' + 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>";
|