turbine-orm 0.81.0 → 0.90.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 +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/cjs/cli/studio.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
|
40
40
|
import type { PgCompatPool } from '../client.js';
|
|
41
41
|
import type { Dialect } from '../dialect.js';
|
|
42
42
|
import type { SchemaMetadata, TableMetadata } from '../schema.js';
|
|
43
|
+
import { type TicketGate } from './local-server-guard.js';
|
|
43
44
|
export interface StudioOptions {
|
|
44
45
|
url: string;
|
|
45
46
|
schema: string;
|
|
@@ -88,9 +89,13 @@ export interface StudioHandle {
|
|
|
88
89
|
path: string;
|
|
89
90
|
applied: number;
|
|
90
91
|
} | null;
|
|
91
|
-
/** Random per-process session token the UI sends via cookie. */
|
|
92
|
+
/** Random per-process session token the UI sends via cookie. Never part of `url`. */
|
|
92
93
|
authToken: string;
|
|
93
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Full URL including a ONE-TIME `?token=...` login ticket, safe to print for
|
|
96
|
+
* the user. The first GET that carries it is exchanged for the session cookie;
|
|
97
|
+
* the ticket is refused afterwards and is not the session credential.
|
|
98
|
+
*/
|
|
94
99
|
url: string;
|
|
95
100
|
}
|
|
96
101
|
export interface StudioContext {
|
|
@@ -154,6 +159,11 @@ export interface StudioContext {
|
|
|
154
159
|
reason: string;
|
|
155
160
|
columns: number;
|
|
156
161
|
};
|
|
162
|
+
/**
|
|
163
|
+
* The one-time login ticket behind the printed URL. Absent on a hand-built
|
|
164
|
+
* context, which then gets a gate whose ticket is `authToken` on first use.
|
|
165
|
+
*/
|
|
166
|
+
ticketGate?: TicketGate;
|
|
157
167
|
}
|
|
158
168
|
/**
|
|
159
169
|
* Start the Studio server. Returns a handle with the session token, a pre-built
|
|
@@ -196,6 +206,8 @@ export declare function relationLinksForTable(table: TableMetadata, metadata: Sc
|
|
|
196
206
|
export declare function apiTableRows(res: ServerResponse, ctx: StudioContext, rawTableName: string, params: URLSearchParams): Promise<void>;
|
|
197
207
|
export declare function resolveColumnName(table: TableMetadata, nameOrField: string): string | null;
|
|
198
208
|
export declare function isTextishType(pgType: string): boolean;
|
|
209
|
+
/** Most rows one Query-tab request returns, however large its `limit` or however absent. */
|
|
210
|
+
export declare const MAX_BUILDER_ROWS = 1000;
|
|
199
211
|
export declare function apiBuilder(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
|
|
200
212
|
export declare function apiRowWrite(req: IncomingMessage, res: ServerResponse, ctx: StudioContext, op: 'update' | 'insert' | 'delete'): Promise<void>;
|
|
201
213
|
interface SavedQuery {
|
package/dist/cjs/cli/studio.js
CHANGED
|
@@ -41,7 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
41
41
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
42
|
};
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
-
exports.PII_REDACTED = void 0;
|
|
44
|
+
exports.PII_REDACTED = exports.MAX_BUILDER_ROWS = void 0;
|
|
45
45
|
exports.startStudio = startStudio;
|
|
46
46
|
exports.handleRequest = handleRequest;
|
|
47
47
|
exports.apiDemoMode = apiDemoMode;
|
|
@@ -55,11 +55,9 @@ exports.apiListSavedQueries = apiListSavedQueries;
|
|
|
55
55
|
exports.apiCreateSavedQuery = apiCreateSavedQuery;
|
|
56
56
|
exports.apiDeleteSavedQuery = apiDeleteSavedQuery;
|
|
57
57
|
exports.forceTagAllColumnsPii = forceTagAllColumnsPii;
|
|
58
|
-
const node_child_process_1 = require("node:child_process");
|
|
59
58
|
const node_crypto_1 = require("node:crypto");
|
|
60
59
|
const node_fs_1 = require("node:fs");
|
|
61
60
|
const node_http_1 = require("node:http");
|
|
62
|
-
const node_os_1 = require("node:os");
|
|
63
61
|
const node_path_1 = require("node:path");
|
|
64
62
|
const pg_1 = __importDefault(require("pg"));
|
|
65
63
|
const connection_guard_js_1 = require("../connection-guard.js");
|
|
@@ -73,12 +71,21 @@ const index_js_1 = require("../query/index.js");
|
|
|
73
71
|
// redaction walk that answered that differently would redact a different query
|
|
74
72
|
// than the one that ran.
|
|
75
73
|
const utils_js_1 = require("../query/utils.js");
|
|
74
|
+
const local_server_guard_js_1 = require("./local-server-guard.js");
|
|
75
|
+
const local_server_output_js_1 = require("./local-server-output.js");
|
|
76
76
|
const pii_predicate_guard_js_1 = require("./pii-predicate-guard.js");
|
|
77
77
|
const pii_tags_js_1 = require("./pii-tags.js");
|
|
78
78
|
const rate_limit_js_1 = require("./rate-limit.js");
|
|
79
79
|
const studio_demo_js_1 = require("./studio-demo.js");
|
|
80
80
|
const studio_ui_generated_js_1 = require("./studio-ui.generated.js");
|
|
81
81
|
const ui_js_1 = require("./ui.js");
|
|
82
|
+
/**
|
|
83
|
+
* A name made safe to quote in a message. Catalog names (and any name echoed from
|
|
84
|
+
* a request) can carry control or bidi characters, and a message can end up in a
|
|
85
|
+
* terminal, a log or the UI's error box. Same helper `generate`, `status` and
|
|
86
|
+
* `doctor` print through.
|
|
87
|
+
*/
|
|
88
|
+
const shown = introspect_js_1.printableCatalogName;
|
|
82
89
|
// ---------------------------------------------------------------------------
|
|
83
90
|
// Main entry point
|
|
84
91
|
// ---------------------------------------------------------------------------
|
|
@@ -197,6 +204,7 @@ async function startStudio(options) {
|
|
|
197
204
|
};
|
|
198
205
|
}
|
|
199
206
|
const authToken = (0, node_crypto_1.randomBytes)(24).toString('hex');
|
|
207
|
+
const ticketGate = (0, local_server_guard_js_1.createTicketGate)();
|
|
200
208
|
const stateDir = (0, node_path_1.resolve)(options.stateDir ?? '.turbine');
|
|
201
209
|
const rateLimiter = new Map();
|
|
202
210
|
const ctx = {
|
|
@@ -204,6 +212,7 @@ async function startStudio(options) {
|
|
|
204
212
|
metadata,
|
|
205
213
|
options,
|
|
206
214
|
authToken,
|
|
215
|
+
ticketGate,
|
|
207
216
|
stateDir,
|
|
208
217
|
statementTimeout,
|
|
209
218
|
rateLimiter,
|
|
@@ -220,7 +229,7 @@ async function startStudio(options) {
|
|
|
220
229
|
};
|
|
221
230
|
const server = (0, node_http_1.createServer)((req, res) => {
|
|
222
231
|
handleRequest(req, res, ctx).catch((err) => {
|
|
223
|
-
sendJson(res, 500, { error:
|
|
232
|
+
sendJson(res, 500, { error: (0, local_server_output_js_1.responseErrorText)(err) });
|
|
224
233
|
});
|
|
225
234
|
});
|
|
226
235
|
await new Promise((resolve, reject) => {
|
|
@@ -230,9 +239,9 @@ async function startStudio(options) {
|
|
|
230
239
|
resolve();
|
|
231
240
|
});
|
|
232
241
|
});
|
|
233
|
-
const url = buildStudioUrl(options.host, options.port,
|
|
242
|
+
const url = buildStudioUrl(options.host, options.port, ticketGate.token);
|
|
234
243
|
if (options.openBrowser) {
|
|
235
|
-
|
|
244
|
+
(0, local_server_output_js_1.openInBrowser)(url);
|
|
236
245
|
}
|
|
237
246
|
return {
|
|
238
247
|
authToken,
|
|
@@ -261,31 +270,64 @@ function originFor(host, port) {
|
|
|
261
270
|
// ---------------------------------------------------------------------------
|
|
262
271
|
async function handleRequest(req, res, ctx) {
|
|
263
272
|
const expectedOrigin = originFor(ctx.options.host, ctx.options.port);
|
|
273
|
+
// DNS rebinding: a page on another domain that re-resolves to this loopback
|
|
274
|
+
// port still sends ITS hostname in `Host`. Checked first, before any route or
|
|
275
|
+
// token logic, so nothing about this server is observable to such a page.
|
|
276
|
+
if (!(0, local_server_guard_js_1.isAllowedHostHeader)(req.headers.host, ctx.options.host, ctx.options.port)) {
|
|
277
|
+
sendJson(res, 403, { error: 'host not allowed: open Studio at the address printed in the terminal' });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
264
280
|
// CORS: not needed, same-origin only. Explicitly refuse cross-origin.
|
|
265
281
|
const origin = req.headers.origin;
|
|
266
282
|
if (origin && origin !== expectedOrigin) {
|
|
267
283
|
sendJson(res, 403, { error: 'cross-origin requests not allowed' });
|
|
268
284
|
return;
|
|
269
285
|
}
|
|
270
|
-
|
|
286
|
+
// A request target that is not a URL (`GET //`) is a client error, not a 500.
|
|
287
|
+
let url;
|
|
288
|
+
try {
|
|
289
|
+
url = new URL(req.url ?? '/', expectedOrigin);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
sendJson(res, 400, { error: 'malformed request target' });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
271
295
|
const pathname = url.pathname;
|
|
272
|
-
// The index route serves the HTML shell
|
|
273
|
-
//
|
|
274
|
-
//
|
|
296
|
+
// The index route serves the HTML shell. Every other route requires the
|
|
297
|
+
// session token in the `x-turbine-token` header or the `turbine_studio_token`
|
|
298
|
+
// cookie. The printed URL's `?token=` is a ONE-TIME login ticket, not that
|
|
299
|
+
// token: the first GET carrying it is exchanged for the cookie, and the ticket
|
|
300
|
+
// is dead afterwards, so a copy left in browser history, `ps` output or a log
|
|
301
|
+
// opens nothing.
|
|
275
302
|
if (pathname === '/' || pathname === '/index.html') {
|
|
276
303
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
277
304
|
sendText(res, 405, 'Method Not Allowed');
|
|
278
305
|
return;
|
|
279
306
|
}
|
|
280
307
|
const queryToken = url.searchParams.get('token');
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
308
|
+
if (queryToken) {
|
|
309
|
+
ctx.ticketGate ??= (0, local_server_guard_js_1.createTicketGate)(ctx.authToken);
|
|
310
|
+
// HEAD must not spend the ticket: only the navigation that sets the cookie may.
|
|
311
|
+
const outcome = req.method === 'GET' ? (0, local_server_guard_js_1.redeemTicket)(ctx.ticketGate, queryToken) : 'invalid';
|
|
312
|
+
if (outcome === 'granted') {
|
|
313
|
+
res.writeHead(302, {
|
|
314
|
+
Location: '/',
|
|
315
|
+
'Set-Cookie': `turbine_studio_token=${ctx.authToken}; Path=/; HttpOnly; SameSite=Strict`,
|
|
316
|
+
});
|
|
317
|
+
res.end();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (outcome === 'used') {
|
|
321
|
+
if (isAuthorized(req, ctx.authToken)) {
|
|
322
|
+
// Already signed in (a reload of the printed URL): drop the ticket from the address bar.
|
|
323
|
+
res.writeHead(302, { Location: '/' });
|
|
324
|
+
res.end();
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
sendText(res, 403, 'This login link was already used. Restart Studio to get a new one.');
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
289
331
|
}
|
|
290
332
|
sendHtml(res, 200, studio_ui_generated_js_1.STUDIO_HTML, cspNonce());
|
|
291
333
|
return;
|
|
@@ -316,6 +358,22 @@ async function handleRequest(req, res, ctx) {
|
|
|
316
358
|
sendJson(res, 401, { error: 'unauthorized: use the URL printed in the terminal' });
|
|
317
359
|
return;
|
|
318
360
|
}
|
|
361
|
+
try {
|
|
362
|
+
await routeAuthorized(req, res, ctx, url, origin, expectedOrigin);
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
// A bad request body is the caller's mistake and gets its own status; every
|
|
366
|
+
// other failure is still the dispatcher's 500.
|
|
367
|
+
if (err instanceof RequestError) {
|
|
368
|
+
sendJson(res, err.status, { error: err.message });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/** The routes behind the auth gate. */
|
|
375
|
+
async function routeAuthorized(req, res, ctx, url, origin, expectedOrigin) {
|
|
376
|
+
const pathname = url.pathname;
|
|
319
377
|
if (pathname === '/api/schema' && req.method === 'GET') {
|
|
320
378
|
return apiSchema(res, ctx);
|
|
321
379
|
}
|
|
@@ -394,7 +452,7 @@ async function apiDemoMode(req, res, ctx) {
|
|
|
394
452
|
// ---------------------------------------------------------------------------
|
|
395
453
|
function isAuthorized(req, expectedToken) {
|
|
396
454
|
const headerToken = req.headers['x-turbine-token'];
|
|
397
|
-
if (typeof headerToken === 'string' && constantTimeEqual(headerToken, expectedToken)) {
|
|
455
|
+
if (typeof headerToken === 'string' && (0, local_server_guard_js_1.constantTimeEqual)(headerToken, expectedToken)) {
|
|
398
456
|
return true;
|
|
399
457
|
}
|
|
400
458
|
const cookieHeader = req.headers.cookie ?? '';
|
|
@@ -402,30 +460,22 @@ function isAuthorized(req, expectedToken) {
|
|
|
402
460
|
// `x_turbine_studio_token=...` matched first and the real cookie was never
|
|
403
461
|
// compared, locking the legitimate session out of its own Studio.
|
|
404
462
|
const match = /(?:^|;\s*)turbine_studio_token=([a-f0-9]+)/.exec(cookieHeader);
|
|
405
|
-
if (match?.[1] && constantTimeEqual(match[1], expectedToken)) {
|
|
463
|
+
if (match?.[1] && (0, local_server_guard_js_1.constantTimeEqual)(match[1], expectedToken)) {
|
|
406
464
|
return true;
|
|
407
465
|
}
|
|
408
466
|
return false;
|
|
409
467
|
}
|
|
410
|
-
function constantTimeEqual(a, b) {
|
|
411
|
-
// Hash both inputs to fixed-length 32-byte SHA-256 digests before comparing.
|
|
412
|
-
// This makes the comparison constant-length (timingSafeEqual never throws on a
|
|
413
|
-
// length mismatch) and leaks neither length nor content via timing.
|
|
414
|
-
const ah = (0, node_crypto_1.createHash)('sha256').update(a).digest();
|
|
415
|
-
const bh = (0, node_crypto_1.createHash)('sha256').update(b).digest();
|
|
416
|
-
return (0, node_crypto_1.timingSafeEqual)(ah, bh);
|
|
417
|
-
}
|
|
418
468
|
/**
|
|
419
469
|
* Build a helpful "unknown table" error that lists the available tables so the
|
|
420
470
|
* caller can spot a typo or schema mismatch immediately.
|
|
421
471
|
*/
|
|
422
472
|
function unknownTableMessage(name, ctx) {
|
|
423
|
-
const available = Object.keys(ctx.metadata.tables);
|
|
473
|
+
const available = Object.keys(ctx.metadata.tables).map(shown);
|
|
424
474
|
const list = available.length ? available.join(', ') : '(none)';
|
|
425
475
|
// turbine-prefix-ok: every caller passes this straight to sendJson as an HTTP
|
|
426
476
|
// body, never to a TurbineError, so nothing prepends a `[TURBINE_EXXX]` tag
|
|
427
477
|
// and the prefix is the browser's only signal of who produced the message.
|
|
428
|
-
return `[turbine] Unknown table "${name}" in schema "${ctx.options.schema}". Available: ${list}`;
|
|
478
|
+
return `[turbine] Unknown table "${shown(name)}" in schema "${shown(ctx.options.schema)}". Available: ${list}`;
|
|
429
479
|
}
|
|
430
480
|
// ---------------------------------------------------------------------------
|
|
431
481
|
// API: /api/schema
|
|
@@ -631,7 +681,7 @@ async function apiTableRows(res, ctx, rawTableName, params) {
|
|
|
631
681
|
filters = parseTableFilters(params.get('filters'), table, redactedPii);
|
|
632
682
|
}
|
|
633
683
|
catch (err) {
|
|
634
|
-
sendJson(res, 400, { error: `[turbine] ${
|
|
684
|
+
sendJson(res, 400, { error: `[turbine] ${(0, local_server_output_js_1.responseErrorText)(err)}` });
|
|
635
685
|
return;
|
|
636
686
|
}
|
|
637
687
|
// Parameter placeholder + case-insensitive LIKE condition differ by engine.
|
|
@@ -791,14 +841,14 @@ function parseTableFilters(raw, table, redactedPii) {
|
|
|
791
841
|
const f = item;
|
|
792
842
|
const col = typeof f.column === 'string' ? resolveColumnName(table, f.column) : null;
|
|
793
843
|
if (!col) {
|
|
794
|
-
throw new Error(`unknown filter column "${String(f.column)}" on table "${table.name}"`);
|
|
844
|
+
throw new Error(`unknown filter column "${shown(String(f.column))}" on table "${shown(table.name)}"`);
|
|
795
845
|
}
|
|
796
846
|
if (redactedPii.has(col)) {
|
|
797
|
-
throw new Error(`column "${col}" is PII-redacted; filtering on it is disabled (run with --show-pii to enable)`);
|
|
847
|
+
throw new Error(`column "${shown(col)}" is PII-redacted; filtering on it is disabled (run with --show-pii to enable)`);
|
|
798
848
|
}
|
|
799
849
|
const op = typeof f.op === 'string' ? f.op : '';
|
|
800
850
|
if (!FILTER_OP_NAMES.has(op)) {
|
|
801
|
-
throw new Error(`unknown filter op "${op}" (expected one of: ${[...FILTER_OP_NAMES].join(', ')})`);
|
|
851
|
+
throw new Error(`unknown filter op "${shown(String(op))}" (expected one of: ${[...FILTER_OP_NAMES].join(', ')})`);
|
|
802
852
|
}
|
|
803
853
|
if (op === 'isNull' || op === 'notNull') {
|
|
804
854
|
out.push({ column: col, op });
|
|
@@ -807,12 +857,12 @@ function parseTableFilters(raw, table, redactedPii) {
|
|
|
807
857
|
const value = f.value;
|
|
808
858
|
const t = typeof value;
|
|
809
859
|
if (value === null || value === undefined || (t !== 'string' && t !== 'number' && t !== 'boolean')) {
|
|
810
|
-
throw new Error(`filter on "${col}" needs a scalar value (use isNull/notNull for null checks)`);
|
|
860
|
+
throw new Error(`filter on "${shown(col)}" needs a scalar value (use isNull/notNull for null checks)`);
|
|
811
861
|
}
|
|
812
862
|
if (op === 'contains') {
|
|
813
863
|
const colMeta = table.columns.find((c) => c.name === col);
|
|
814
864
|
if (!colMeta || !isTextishType(colMeta.pgType)) {
|
|
815
|
-
throw new Error(`contains only applies to text columns ("${col}" is ${colMeta?.pgType ?? 'unknown'})`);
|
|
865
|
+
throw new Error(`contains only applies to text columns ("${shown(col)}" is ${shown(colMeta?.pgType ?? 'unknown')})`);
|
|
816
866
|
}
|
|
817
867
|
}
|
|
818
868
|
out.push({ column: col, op, value });
|
|
@@ -853,7 +903,7 @@ function assertNoPiiPredicates(args, tableName, metadata, showPii) {
|
|
|
853
903
|
metadata,
|
|
854
904
|
hiddenReason: (table, column) => (isRedactedColumn(table, column, showPii) ? 'is PII-tagged and redacted' : null),
|
|
855
905
|
refuseColumn: (table, column, reason) => {
|
|
856
|
-
throw new errors_js_1.ValidationError(`Column "${column}" on "${table.name}" ${reason}, so it cannot be used ` +
|
|
906
|
+
throw new errors_js_1.ValidationError(`Column "${shown(column)}" on "${shown(table.name)}" ${reason}, so it cannot be used ` +
|
|
857
907
|
`in a where, orderBy, cursor, or distinct: filtering, sorting, paging, or de-duplicating on a hidden ` +
|
|
858
908
|
`value reveals it. Restart Studio with --show-pii to query it.`);
|
|
859
909
|
},
|
|
@@ -863,12 +913,69 @@ function assertNoPiiPredicates(args, tableName, metadata, showPii) {
|
|
|
863
913
|
`Flatten the query, or restart Studio with --show-pii.`);
|
|
864
914
|
},
|
|
865
915
|
refuseShape: (table, key) => {
|
|
866
|
-
throw new errors_js_1.ValidationError(`Studio does not recognize "${key}" in a query on "${table.name}", so it cannot prove the ` +
|
|
916
|
+
throw new errors_js_1.ValidationError(`Studio does not recognize "${shown(key)}" in a query on "${shown(table.name)}", so it cannot prove the ` +
|
|
867
917
|
`query does not filter or sort on a PII-tagged and redacted column, and refuses it rather than ` +
|
|
868
918
|
`guessing. Remove it, or restart Studio with --show-pii.`);
|
|
869
919
|
},
|
|
870
920
|
});
|
|
871
921
|
}
|
|
922
|
+
/** Most rows one Query-tab request returns, however large its `limit` or however absent. */
|
|
923
|
+
exports.MAX_BUILDER_ROWS = 1000;
|
|
924
|
+
/**
|
|
925
|
+
* The args with `limit` / `take` held to {@link MAX_BUILDER_ROWS}, and a `limit`
|
|
926
|
+
* supplied when neither was. The Query tab renders every row it is sent, so an
|
|
927
|
+
* unlimited `findMany` on a large table would load the whole table into this
|
|
928
|
+
* process and then into the browser.
|
|
929
|
+
*/
|
|
930
|
+
function capBuilderRows(args) {
|
|
931
|
+
const next = { ...args };
|
|
932
|
+
for (const key of ['limit', 'take']) {
|
|
933
|
+
const value = next[key];
|
|
934
|
+
if (typeof value === 'number' && value > exports.MAX_BUILDER_ROWS)
|
|
935
|
+
next[key] = exports.MAX_BUILDER_ROWS;
|
|
936
|
+
}
|
|
937
|
+
if (next.limit === undefined && next.take === undefined)
|
|
938
|
+
next.limit = exports.MAX_BUILDER_ROWS;
|
|
939
|
+
const asked = typeof next.limit === 'number' ? next.limit : typeof next.take === 'number' ? next.take : undefined;
|
|
940
|
+
return { args: next, effectiveLimit: asked ?? exports.MAX_BUILDER_ROWS };
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Does a `select` anywhere in these args name a PII-tagged column? Walks the
|
|
944
|
+
* top-level `select` and every `with` level against its own target table.
|
|
945
|
+
*
|
|
946
|
+
* Why it exists: core refuses a PII column named in `select` unless the call
|
|
947
|
+
* passes `includePii: UNSAFE`, because `select` is ordinary request data. Studio
|
|
948
|
+
* is the one caller that may say yes to it, since every row it serves is
|
|
949
|
+
* redacted server-side before it leaves (see {@link redactBuilderRows}), so the
|
|
950
|
+
* flag is passed exactly when the query asks for such a column.
|
|
951
|
+
*
|
|
952
|
+
* It errs toward NOT finding one: past the depth cap, or on a shape it does not
|
|
953
|
+
* recognize, it answers false and core's own refusal reaches the user, which is
|
|
954
|
+
* a failed query and never a leaked column.
|
|
955
|
+
*/
|
|
956
|
+
function selectNamesPii(args, table, metadata, depth = 0) {
|
|
957
|
+
if (depth > 10 || args === null || typeof args !== 'object')
|
|
958
|
+
return false;
|
|
959
|
+
const { select, with: withClause } = args;
|
|
960
|
+
if (select && typeof select === 'object' && !Array.isArray(select)) {
|
|
961
|
+
for (const [key, wanted] of Object.entries(select)) {
|
|
962
|
+
if (!wanted)
|
|
963
|
+
continue;
|
|
964
|
+
const column = (0, utils_js_1.resolveColumnName)(table, key);
|
|
965
|
+
if (column !== undefined && table.columns.some((c) => c.name === column && c.pii === true))
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (withClause && typeof withClause === 'object' && !Array.isArray(withClause)) {
|
|
970
|
+
for (const [relKey, spec] of Object.entries(withClause)) {
|
|
971
|
+
const resolved = (0, utils_js_1.resolveRelation)(table.relations, relKey);
|
|
972
|
+
const target = resolved ? (0, utils_js_1.ownLookup)(metadata.tables, resolved.def.to) : undefined;
|
|
973
|
+
if (target && selectNamesPii(spec, target, metadata, depth + 1))
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
872
979
|
async function apiBuilder(req, res, ctx) {
|
|
873
980
|
const body = await readJsonBody(req);
|
|
874
981
|
const tableName = typeof body?.table === 'string' ? body.table : '';
|
|
@@ -877,6 +984,19 @@ async function apiBuilder(req, res, ctx) {
|
|
|
877
984
|
sendJson(res, 400, { error: unknownTableMessage(tableName, ctx) });
|
|
878
985
|
return;
|
|
879
986
|
}
|
|
987
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) {
|
|
988
|
+
sendJson(res, 400, { error: '`args` must be an object of findMany options' });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// A locking read takes row locks, and every read here runs inside
|
|
992
|
+
// `BEGIN READ ONLY`, which PostgreSQL answers with a raw 25006. Refused by name
|
|
993
|
+
// instead, before anything else is judged: the verdict must not depend on the
|
|
994
|
+
// PII state (the walk below is skipped under --show-pii) or on the database.
|
|
995
|
+
const locked = (0, pii_predicate_guard_js_1.findRowLockOption)(args);
|
|
996
|
+
if (locked !== null) {
|
|
997
|
+
sendJson(res, 400, { error: (0, pii_predicate_guard_js_1.rowLockRefusalMessage)(shown(locked), "Studio's Query tab") });
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
880
1000
|
// Fail-closed posture: the generated metadata exists but its PII tags could
|
|
881
1001
|
// not be read, so every column is treated as PII. The Query tab is then
|
|
882
1002
|
// REFUSED outright rather than half-served. A default projection that
|
|
@@ -901,12 +1021,13 @@ async function apiBuilder(req, res, ctx) {
|
|
|
901
1021
|
const target = (0, utils_js_1.ownLookup)(ctx.metadata.tables, tableName);
|
|
902
1022
|
if (target && !ctx.showPii && args.select === undefined && target.columns.every((c) => c.pii === true)) {
|
|
903
1023
|
sendJson(res, 400, {
|
|
904
|
-
error: `Every column of "${tableName}" is PII-tagged, so the default projection is empty. Name the columns you ` +
|
|
1024
|
+
error: `Every column of "${shown(tableName)}" is PII-tagged, so the default projection is empty. Name the columns you ` +
|
|
905
1025
|
'want in `select`, or run Studio with --show-pii.',
|
|
906
1026
|
});
|
|
907
1027
|
return;
|
|
908
1028
|
}
|
|
909
1029
|
let deferred;
|
|
1030
|
+
let effectiveLimit = exports.MAX_BUILDER_ROWS;
|
|
910
1031
|
try {
|
|
911
1032
|
const qi = new index_js_1.QueryInterface(ctx.pool, tableName, ctx.metadata, [], {
|
|
912
1033
|
warnOnUnlimited: false,
|
|
@@ -917,10 +1038,15 @@ async function apiBuilder(req, res, ctx) {
|
|
|
917
1038
|
dialect: ctx.dialect,
|
|
918
1039
|
});
|
|
919
1040
|
assertNoPiiPredicates(args, tableName, ctx.metadata, ctx.showPii === true);
|
|
920
|
-
|
|
1041
|
+
const capped = capBuilderRows(args);
|
|
1042
|
+
effectiveLimit = capped.effectiveLimit;
|
|
1043
|
+
// The flag is the request's own `includePii` untouched unless the query names a PII
|
|
1044
|
+
// column in a `select`, in which case Studio (which redacts every row) grants it.
|
|
1045
|
+
const grant = target && selectNamesPii(args, target, ctx.metadata) ? { includePii: index_js_1.UNSAFE } : {};
|
|
1046
|
+
deferred = qi.buildFindMany({ ...capped.args, ...grant });
|
|
921
1047
|
}
|
|
922
1048
|
catch (err) {
|
|
923
|
-
sendJson(res, 400, { error:
|
|
1049
|
+
sendJson(res, 400, { error: (0, local_server_output_js_1.responseErrorText)(err) });
|
|
924
1050
|
return;
|
|
925
1051
|
}
|
|
926
1052
|
const client = await ctx.pool.connect();
|
|
@@ -976,6 +1102,9 @@ async function apiBuilder(req, res, ctx) {
|
|
|
976
1102
|
rows: redactedRows.map((r) => serializeRow(r)),
|
|
977
1103
|
rowCount: result.rowCount ?? result.rows.length,
|
|
978
1104
|
elapsedMs,
|
|
1105
|
+
// True when the row cap, not the query, may be what ended the list.
|
|
1106
|
+
truncated: effectiveLimit >= exports.MAX_BUILDER_ROWS && redactedRows.length >= exports.MAX_BUILDER_ROWS,
|
|
1107
|
+
rowLimit: exports.MAX_BUILDER_ROWS,
|
|
979
1108
|
});
|
|
980
1109
|
}
|
|
981
1110
|
catch (err) {
|
|
@@ -987,7 +1116,7 @@ async function apiBuilder(req, res, ctx) {
|
|
|
987
1116
|
/* ignore */
|
|
988
1117
|
}
|
|
989
1118
|
}
|
|
990
|
-
sendJson(res, 400, { error:
|
|
1119
|
+
sendJson(res, 400, { error: (0, local_server_output_js_1.responseErrorText)(err) });
|
|
991
1120
|
}
|
|
992
1121
|
finally {
|
|
993
1122
|
client.release();
|
|
@@ -1021,7 +1150,7 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1021
1150
|
return;
|
|
1022
1151
|
}
|
|
1023
1152
|
if (table.isView) {
|
|
1024
|
-
sendJson(res, 400, { error: `[turbine] "${tableName}" is a view; Studio cannot write to it.` });
|
|
1153
|
+
sendJson(res, 400, { error: `[turbine] "${shown(tableName)}" is a view; Studio cannot write to it.` });
|
|
1025
1154
|
return;
|
|
1026
1155
|
}
|
|
1027
1156
|
// Bulk form: `rows` replaces `data` (insert) / `where` (delete).
|
|
@@ -1059,7 +1188,9 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1059
1188
|
const rec = rowData;
|
|
1060
1189
|
const badKey = firstUnknownColumn(table, rec);
|
|
1061
1190
|
if (badKey) {
|
|
1062
|
-
sendJson(res, 400, {
|
|
1191
|
+
sendJson(res, 400, {
|
|
1192
|
+
error: `[turbine] unknown column "${shown(badKey)}" on table "${shown(tableName)}"${rowLabel}`,
|
|
1193
|
+
});
|
|
1063
1194
|
return;
|
|
1064
1195
|
}
|
|
1065
1196
|
if (!Object.keys(rec).some((k) => rec[k] !== undefined)) {
|
|
@@ -1072,7 +1203,7 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1072
1203
|
if (op === 'update' || op === 'delete') {
|
|
1073
1204
|
if (table.primaryKey.length === 0) {
|
|
1074
1205
|
sendJson(res, 400, {
|
|
1075
|
-
error: `[turbine] "${tableName}" has no primary key; single-row writes require one.`,
|
|
1206
|
+
error: `[turbine] "${shown(tableName)}" has no primary key; single-row writes require one.`,
|
|
1076
1207
|
});
|
|
1077
1208
|
return;
|
|
1078
1209
|
}
|
|
@@ -1100,7 +1231,7 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1100
1231
|
if (op === 'update') {
|
|
1101
1232
|
const badKey = firstUnknownColumn(table, data);
|
|
1102
1233
|
if (badKey) {
|
|
1103
|
-
sendJson(res, 400, { error: `[turbine] unknown column "${badKey}" on table "${tableName}"` });
|
|
1234
|
+
sendJson(res, 400, { error: `[turbine] unknown column "${shown(badKey)}" on table "${shown(tableName)}"` });
|
|
1104
1235
|
return;
|
|
1105
1236
|
}
|
|
1106
1237
|
if (!Object.keys(data).some((k) => data[k] !== undefined)) {
|
|
@@ -1128,7 +1259,7 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1128
1259
|
}
|
|
1129
1260
|
}
|
|
1130
1261
|
catch (err) {
|
|
1131
|
-
sendJson(res, 400, { error:
|
|
1262
|
+
sendJson(res, 400, { error: (0, local_server_output_js_1.responseErrorText)(err) });
|
|
1132
1263
|
return;
|
|
1133
1264
|
}
|
|
1134
1265
|
const client = await ctx.pool.connect();
|
|
@@ -1190,7 +1321,7 @@ async function apiRowWrite(req, res, ctx, op) {
|
|
|
1190
1321
|
catch {
|
|
1191
1322
|
/* ignore */
|
|
1192
1323
|
}
|
|
1193
|
-
sendJson(res, 400, { error:
|
|
1324
|
+
sendJson(res, 400, { error: (0, local_server_output_js_1.responseErrorText)(err) });
|
|
1194
1325
|
}
|
|
1195
1326
|
finally {
|
|
1196
1327
|
client.release();
|
|
@@ -1226,11 +1357,11 @@ function extractPkWhere(table, where) {
|
|
|
1226
1357
|
const pkWhere = {};
|
|
1227
1358
|
for (const pkCol of table.primaryKey) {
|
|
1228
1359
|
if (!resolved.has(pkCol)) {
|
|
1229
|
-
return { error: `\`where\` must fully cover the primary key (missing "${pkCol}")` };
|
|
1360
|
+
return { error: `\`where\` must fully cover the primary key (missing "${shown(pkCol)}")` };
|
|
1230
1361
|
}
|
|
1231
1362
|
const v = resolved.get(pkCol);
|
|
1232
1363
|
if (v === undefined || v === null || typeof v === 'object') {
|
|
1233
|
-
return { error: `primary key "${pkCol}" must be a scalar value in \`where\`` };
|
|
1364
|
+
return { error: `primary key "${shown(pkCol)}" must be a scalar value in \`where\`` };
|
|
1234
1365
|
}
|
|
1235
1366
|
const field = table.reverseColumnMap[pkCol] ?? pkCol;
|
|
1236
1367
|
pkWhere[field] = v;
|
|
@@ -1291,6 +1422,8 @@ function apiListSavedQueries(res, ctx, params) {
|
|
|
1291
1422
|
const filtered = table ? queries.filter((q) => q.table === table) : queries;
|
|
1292
1423
|
sendJson(res, 200, { queries: filtered });
|
|
1293
1424
|
}
|
|
1425
|
+
/** Most saved queries Studio will keep; each is bounded by the 1 MB body limit. */
|
|
1426
|
+
const MAX_SAVED_QUERIES = 500;
|
|
1294
1427
|
async function apiCreateSavedQuery(req, res, ctx) {
|
|
1295
1428
|
const body = await readJsonBody(req);
|
|
1296
1429
|
const table = typeof body?.table === 'string' ? body.table : '';
|
|
@@ -1309,6 +1442,12 @@ async function apiCreateSavedQuery(req, res, ctx) {
|
|
|
1309
1442
|
return;
|
|
1310
1443
|
}
|
|
1311
1444
|
const data = loadSavedQueries(ctx);
|
|
1445
|
+
if (data.queries.length >= MAX_SAVED_QUERIES) {
|
|
1446
|
+
sendJson(res, 400, {
|
|
1447
|
+
error: `Studio keeps at most ${MAX_SAVED_QUERIES} saved queries. Delete one before saving another.`,
|
|
1448
|
+
});
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1312
1451
|
const entry = {
|
|
1313
1452
|
id: (0, node_crypto_1.randomUUID)(),
|
|
1314
1453
|
table,
|
|
@@ -1521,26 +1660,73 @@ function serializeRow(row) {
|
|
|
1521
1660
|
}
|
|
1522
1661
|
return out;
|
|
1523
1662
|
}
|
|
1663
|
+
/** A request the caller got wrong, carrying the HTTP status it deserves. */
|
|
1664
|
+
class RequestError extends Error {
|
|
1665
|
+
status;
|
|
1666
|
+
constructor(status, message) {
|
|
1667
|
+
super(message);
|
|
1668
|
+
this.status = status;
|
|
1669
|
+
this.name = 'RequestError';
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
/** Largest JSON request body Studio will read. */
|
|
1673
|
+
const MAX_BODY_BYTES = 1 << 20;
|
|
1674
|
+
/**
|
|
1675
|
+
* How much of an oversized body Studio will read and throw away before it gives
|
|
1676
|
+
* up and drops the connection. A client that is still sending when the server
|
|
1677
|
+
* answers and closes gets a TCP reset instead of the response, so the 413 would
|
|
1678
|
+
* be lost; discarding the rest first lets it arrive. Bounded so a body of
|
|
1679
|
+
* unbounded length cannot hold the connection open.
|
|
1680
|
+
*/
|
|
1681
|
+
const MAX_DISCARD_BYTES = 16 * 1024 * 1024;
|
|
1682
|
+
/**
|
|
1683
|
+
* Read a JSON request body, refusing one over {@link MAX_BODY_BYTES} (413) or
|
|
1684
|
+
* malformed (400). Nothing past the limit is ever buffered: an oversized body,
|
|
1685
|
+
* whether declared by `Content-Length` or discovered while streaming, is counted
|
|
1686
|
+
* and discarded chunk by chunk, then answered with 413.
|
|
1687
|
+
*/
|
|
1524
1688
|
async function readJsonBody(req) {
|
|
1689
|
+
const declared = Number(req.headers?.['content-length']);
|
|
1690
|
+
let tooBig = Number.isFinite(declared) && declared > MAX_BODY_BYTES;
|
|
1525
1691
|
const chunks = [];
|
|
1526
1692
|
let total = 0;
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1693
|
+
// destroyOnReturn: false, because destroying a half-read request tears down
|
|
1694
|
+
// its socket and the response with it.
|
|
1695
|
+
const source = typeof req.iterator === 'function' ? req.iterator({ destroyOnReturn: false }) : req;
|
|
1696
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
1697
|
+
for (;;) {
|
|
1698
|
+
const next = await iterator.next();
|
|
1699
|
+
if (next.done)
|
|
1700
|
+
break;
|
|
1701
|
+
const buf = next.value;
|
|
1530
1702
|
total += buf.length;
|
|
1531
|
-
if (total >
|
|
1532
|
-
|
|
1533
|
-
|
|
1703
|
+
if (!tooBig && total > MAX_BODY_BYTES) {
|
|
1704
|
+
tooBig = true;
|
|
1705
|
+
chunks.length = 0;
|
|
1706
|
+
}
|
|
1707
|
+
if (!tooBig)
|
|
1708
|
+
chunks.push(buf);
|
|
1709
|
+
else if (total > MAX_DISCARD_BYTES) {
|
|
1710
|
+
req.destroy?.();
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1534
1713
|
}
|
|
1714
|
+
if (tooBig)
|
|
1715
|
+
throw new RequestError(413, `request body too large (limit ${MAX_BODY_BYTES} bytes)`);
|
|
1535
1716
|
const raw = Buffer.concat(chunks).toString('utf8');
|
|
1536
1717
|
if (!raw)
|
|
1537
1718
|
return {};
|
|
1719
|
+
let parsed;
|
|
1538
1720
|
try {
|
|
1539
|
-
|
|
1721
|
+
parsed = JSON.parse(raw);
|
|
1540
1722
|
}
|
|
1541
1723
|
catch {
|
|
1542
|
-
throw new
|
|
1724
|
+
throw new RequestError(400, 'invalid json body');
|
|
1543
1725
|
}
|
|
1726
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1727
|
+
throw new RequestError(400, 'request body must be a JSON object');
|
|
1728
|
+
}
|
|
1729
|
+
return parsed;
|
|
1544
1730
|
}
|
|
1545
1731
|
function sendJson(res, status, body) {
|
|
1546
1732
|
const payload = JSON.stringify(body);
|
|
@@ -1580,14 +1766,3 @@ function sendHtml(res, status, template, nonce) {
|
|
|
1580
1766
|
});
|
|
1581
1767
|
res.end(body);
|
|
1582
1768
|
}
|
|
1583
|
-
function openUrl(url) {
|
|
1584
|
-
const cmd = (0, node_os_1.platform)() === 'darwin' ? 'open' : (0, node_os_1.platform)() === 'win32' ? 'cmd' : 'xdg-open';
|
|
1585
|
-
const args = (0, node_os_1.platform)() === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
1586
|
-
try {
|
|
1587
|
-
const child = (0, node_child_process_1.spawn)(cmd, args, { stdio: 'ignore', detached: true });
|
|
1588
|
-
child.unref();
|
|
1589
|
-
}
|
|
1590
|
-
catch {
|
|
1591
|
-
// Non-fatal: user can click the URL manually.
|
|
1592
|
-
}
|
|
1593
|
-
}
|
package/dist/cjs/cli/ui.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare const symbols: {
|
|
|
31
31
|
readonly arrow: "→" | "->";
|
|
32
32
|
readonly arrowRight: ">" | "▸";
|
|
33
33
|
readonly info: "i" | "ℹ";
|
|
34
|
-
readonly warning: "
|
|
34
|
+
readonly warning: "!" | "⚠";
|
|
35
35
|
readonly dot: "." | "∙";
|
|
36
36
|
readonly line: "-" | "─";
|
|
37
37
|
readonly vertLine: "|" | "│";
|