turbine-orm 0.91.0-next.e72e9b4 → 0.92.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 +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/dist/cli/index.js
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
|
|
@@ -43,7 +43,7 @@ import { closestName } from '../query/utils.js';
|
|
|
43
43
|
import { snakeToCamel } from '../schema.js';
|
|
44
44
|
import { DestructivePushRefusal, driftWarnings, schemaDiff, schemaPush } from '../schema-sql.js';
|
|
45
45
|
import { applyCodeFirstColumnTags, extractCodeFirstColumnTags } from './code-first-tags.js';
|
|
46
|
-
import { configTemplate, connectionStringHasPassword, DEFAULT_INIT_SEED_FILE, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
|
|
46
|
+
import { configTemplate, connectionStringHasPassword, DEFAULT_INIT_SEED_FILE, findConfigFile, firstNonEmpty, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
|
|
47
47
|
import { DESTRUCTIVE_KIND_LABEL } from './destructive.js';
|
|
48
48
|
import { errorText } from './error-text.js';
|
|
49
49
|
import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
|
|
@@ -2907,6 +2907,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
2907
2907
|
allowDestructive: args.allowDestructive,
|
|
2908
2908
|
onNoTransaction,
|
|
2909
2909
|
schema: config.schema,
|
|
2910
|
+
adapter: config.adapter,
|
|
2910
2911
|
});
|
|
2911
2912
|
}
|
|
2912
2913
|
catch (err) {
|
|
@@ -2925,6 +2926,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
2925
2926
|
allowDestructive: true,
|
|
2926
2927
|
onNoTransaction,
|
|
2927
2928
|
schema: config.schema,
|
|
2929
|
+
adapter: config.adapter,
|
|
2928
2930
|
});
|
|
2929
2931
|
}
|
|
2930
2932
|
if (result.applied.length > 0) {
|
|
@@ -3087,6 +3089,7 @@ async function cmdMigrateDeploy(args, config) {
|
|
|
3087
3089
|
const result = await migrateDeploy(url, config.migrationsDir, {
|
|
3088
3090
|
allowDrift: args.allowDrift,
|
|
3089
3091
|
schema: config.schema,
|
|
3092
|
+
adapter: config.adapter,
|
|
3090
3093
|
lockWaitMs: args.wait !== undefined ? args.wait * 1000 : undefined,
|
|
3091
3094
|
onLockWait: () => {
|
|
3092
3095
|
runSpinner.stop();
|
|
@@ -3198,6 +3201,7 @@ async function cmdMigrateDown(args, config) {
|
|
|
3198
3201
|
allowDestructive: args.allowDestructive,
|
|
3199
3202
|
allowDrift: args.allowDrift,
|
|
3200
3203
|
schema: config.schema,
|
|
3204
|
+
adapter: config.adapter,
|
|
3201
3205
|
});
|
|
3202
3206
|
}
|
|
3203
3207
|
catch (err) {
|
|
@@ -3215,6 +3219,7 @@ async function cmdMigrateDown(args, config) {
|
|
|
3215
3219
|
allowDestructive: true,
|
|
3216
3220
|
allowDrift: args.allowDrift,
|
|
3217
3221
|
schema: config.schema,
|
|
3222
|
+
adapter: config.adapter,
|
|
3218
3223
|
});
|
|
3219
3224
|
}
|
|
3220
3225
|
if (result.rolledBack.length > 0) {
|
|
@@ -4570,12 +4575,12 @@ async function cmdMcp(_args, config) {
|
|
|
4570
4575
|
// ---------------------------------------------------------------------------
|
|
4571
4576
|
async function cmdObserve(args) {
|
|
4572
4577
|
banner();
|
|
4573
|
-
const url = process.env.TURBINE_OBSERVE_URL;
|
|
4578
|
+
const url = firstNonEmpty(args.metricsUrl, process.env.TURBINE_OBSERVE_URL);
|
|
4574
4579
|
if (!url) {
|
|
4575
|
-
error('
|
|
4580
|
+
error('The observe command needs the metrics database: pass --metrics-url or set TURBINE_OBSERVE_URL.');
|
|
4576
4581
|
errorLine();
|
|
4577
|
-
errorLine(`${dim('
|
|
4578
|
-
errorLine(`${dim('Example:')} ${cyan('
|
|
4582
|
+
errorLine(`${dim('Point it at the Postgres database where metrics are stored.')}`);
|
|
4583
|
+
errorLine(`${dim('Example:')} ${cyan('npx turbine observe --metrics-url postgres://...')}`);
|
|
4579
4584
|
errorLine();
|
|
4580
4585
|
process.exit(1);
|
|
4581
4586
|
}
|
|
@@ -4891,7 +4896,7 @@ function showDoctorHelp() {
|
|
|
4891
4896
|
console.log(` ${cyan('--allow-pooler')} Run against a connection pooler anyway ${dim('(refused by default)')}`);
|
|
4892
4897
|
newline();
|
|
4893
4898
|
console.log(` ${dim('doctor refuses a transaction-pooling endpoint (a')} ${cyan('-pooler')} ${dim('or')} ${cyan('pgbouncer')}`);
|
|
4894
|
-
console.log(` ${dim('host, or port 6543). Point it at the direct endpoint instead: a pooler')}`);
|
|
4899
|
+
console.log(` ${dim('host, or port 6543 or 6432). Point it at the direct endpoint instead: a pooler')}`);
|
|
4895
4900
|
console.log(` ${dim('shares one server backend between clients, so neither the session state')}`);
|
|
4896
4901
|
console.log(` ${dim('doctor needs nor the session-scoped views it cites belong to it.')}`);
|
|
4897
4902
|
newline();
|
|
@@ -4949,9 +4954,10 @@ function showMcpHelp() {
|
|
|
4949
4954
|
console.log(` ${bold('Usage:')}`);
|
|
4950
4955
|
console.log(` npx turbine mcp ${dim('[options]')}`);
|
|
4951
4956
|
newline();
|
|
4952
|
-
console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes
|
|
4953
|
-
console.log(` read-only tools: schema, relation graph, join paths
|
|
4954
|
-
console.log(` doctor, EXPLAIN, table stats, sample rows
|
|
4957
|
+
console.log(` Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout and exposes eleven`);
|
|
4958
|
+
console.log(` read-only tools: schema overview, table detail, relation graph, join paths,`);
|
|
4959
|
+
console.log(` migration status, doctor, EXPLAIN, query compile, table stats, sample rows`);
|
|
4960
|
+
console.log(` and error lookup.`);
|
|
4955
4961
|
newline();
|
|
4956
4962
|
console.log(` ${bold('Options:')}`);
|
|
4957
4963
|
console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
|
|
@@ -4965,16 +4971,17 @@ function showObserveHelp() {
|
|
|
4965
4971
|
console.log(` ${bold('turbine observe')}, Query metrics dashboard`);
|
|
4966
4972
|
newline();
|
|
4967
4973
|
console.log(` ${bold('Usage:')}`);
|
|
4968
|
-
console.log(`
|
|
4974
|
+
console.log(` npx turbine observe --metrics-url postgres://... ${dim('[options]')}`);
|
|
4969
4975
|
newline();
|
|
4970
4976
|
console.log(` Reads the ${cyan('_turbine_metrics')} table written by ${cyan('db.$observe()')} and serves`);
|
|
4971
4977
|
console.log(` per-minute count / avg / p50 / p95 / p99 / error aggregates. Read-only,`);
|
|
4972
4978
|
console.log(` loopback-bound, behind a random per-session token, same model as Studio.`);
|
|
4973
4979
|
newline();
|
|
4974
|
-
console.log(` ${dim('The connection string comes from')} ${cyan('TURBINE_OBSERVE_URL')}${dim(', not')} ${cyan('DATABASE_URL')}${dim(':')}`);
|
|
4980
|
+
console.log(` ${dim('The connection string comes from')} ${cyan('--metrics-url')} ${dim('or')} ${cyan('TURBINE_OBSERVE_URL')}${dim(', not')} ${cyan('DATABASE_URL')}${dim(':')}`);
|
|
4975
4981
|
console.log(` ${dim('metrics are meant to live in a different database from the one they measure.')}`);
|
|
4976
4982
|
newline();
|
|
4977
4983
|
console.log(` ${bold('Options:')}`);
|
|
4984
|
+
console.log(` ${cyan('--metrics-url')} ${dim('<url>')} Metrics database ${dim('(default: TURBINE_OBSERVE_URL)')}`);
|
|
4978
4985
|
console.log(` ${cyan('--port')} ${dim('<n>')} HTTP port ${dim('(default: 4984)')}`);
|
|
4979
4986
|
console.log(` ${cyan('--host')} ${dim('<addr>')} Bind address ${dim('(default: 127.0.0.1)')}`);
|
|
4980
4987
|
console.log(` ${cyan('--no-open')} Don't auto-open the browser`);
|
package/dist/cli/observe-ui.d.ts
CHANGED
|
@@ -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>";
|