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/cli/observe.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* _turbine_metrics. Same security model as Studio: loopback binding,
|
|
6
6
|
* random token, HttpOnly cookie, CSP headers, read-only transactions.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
9
9
|
import { createServer } from 'node:http';
|
|
10
10
|
import pg from 'pg';
|
|
11
11
|
import { absorbCheckedOutErrors } from '../connection-guard.js';
|
|
12
|
+
import { constantTimeEqual, createTicketGate, isAllowedHostHeader, redeemTicket, } from './local-server-guard.js';
|
|
13
|
+
import { openInBrowser, responseErrorText } from './local-server-output.js';
|
|
12
14
|
import { OBSERVE_HTML } from './observe-ui.js';
|
|
13
15
|
import { callerKey, checkRateLimit } from './rate-limit.js';
|
|
14
16
|
import { redactUrl } from './ui.js';
|
|
@@ -27,31 +29,41 @@ export async function startObserve(options) {
|
|
|
27
29
|
console.error(`[turbine] observe pool error: ${redactUrl(err.message)}`);
|
|
28
30
|
});
|
|
29
31
|
absorbCheckedOutErrors(pool);
|
|
30
|
-
const probe = await pool.connect();
|
|
31
|
-
try {
|
|
32
|
-
await probe.query('SELECT 1');
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
probe.release();
|
|
36
|
-
}
|
|
37
32
|
const authToken = randomBytes(24).toString('hex');
|
|
33
|
+
const ticketGate = createTicketGate();
|
|
38
34
|
const rateLimiter = new Map();
|
|
39
35
|
const server = createServer((req, res) => {
|
|
40
|
-
handleRequest(req, res, pool, options, authToken, rateLimiter).catch((err) => {
|
|
41
|
-
sendJson(res, 500, { error:
|
|
36
|
+
handleRequest(req, res, pool, options, authToken, rateLimiter, ticketGate).catch((err) => {
|
|
37
|
+
sendJson(res, 500, { error: responseErrorText(err) });
|
|
42
38
|
});
|
|
43
39
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
try {
|
|
41
|
+
const probe = await pool.connect();
|
|
42
|
+
try {
|
|
43
|
+
await probe.query('SELECT 1');
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
probe.release();
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
server.once('error', reject);
|
|
50
|
+
server.listen(options.port, options.host, () => {
|
|
51
|
+
server.off('error', reject);
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
49
54
|
});
|
|
50
|
-
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// A start that fails (probe refused, port taken) must not leave the probe
|
|
58
|
+
// connection idling in a pool nobody holds: it would keep a database session
|
|
59
|
+
// open and the process alive until the idle timeout.
|
|
60
|
+
await pool.end().catch(() => { });
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
51
63
|
const hostPart = options.host.includes(':') && !options.host.startsWith('[') ? `[${options.host}]` : options.host;
|
|
52
|
-
const url = `http://${hostPart}:${options.port}/?token=${
|
|
64
|
+
const url = `http://${hostPart}:${options.port}/?token=${ticketGate.token}`;
|
|
53
65
|
if (options.openBrowser) {
|
|
54
|
-
|
|
66
|
+
openInBrowser(url);
|
|
55
67
|
}
|
|
56
68
|
return {
|
|
57
69
|
authToken,
|
|
@@ -65,15 +77,31 @@ export async function startObserve(options) {
|
|
|
65
77
|
// ---------------------------------------------------------------------------
|
|
66
78
|
// Request routing
|
|
67
79
|
// ---------------------------------------------------------------------------
|
|
68
|
-
export async function handleRequest(req, res, pool, options, authToken, rateLimiter
|
|
80
|
+
export async function handleRequest(req, res, pool, options, authToken, rateLimiter,
|
|
81
|
+
/** The one-time login ticket. Absent on a hand-built call: the ticket is then `authToken` itself. */
|
|
82
|
+
ticketGate = gateFor(authToken)) {
|
|
69
83
|
const hostPart = options.host.includes(':') && !options.host.startsWith('[') ? `[${options.host}]` : options.host;
|
|
70
84
|
const expectedOrigin = `http://${hostPart}:${options.port}`;
|
|
85
|
+
// DNS rebinding: a page on another domain that re-resolves to this port still
|
|
86
|
+
// sends its own hostname in `Host`. Checked before any route or token logic.
|
|
87
|
+
if (!isAllowedHostHeader(req.headers.host, options.host, options.port)) {
|
|
88
|
+
sendJson(res, 403, { error: 'host not allowed: open Observe at the address printed in the terminal' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
71
91
|
const origin = req.headers.origin;
|
|
72
92
|
if (origin && origin !== expectedOrigin) {
|
|
73
93
|
sendJson(res, 403, { error: 'cross-origin requests not allowed' });
|
|
74
94
|
return;
|
|
75
95
|
}
|
|
76
|
-
|
|
96
|
+
// A request target that is not a URL (`GET //`) is a client error, not a 500.
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(req.url ?? '/', expectedOrigin);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
sendJson(res, 400, { error: 'malformed request target' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
77
105
|
const pathname = url.pathname;
|
|
78
106
|
if (pathname === '/' || pathname === '/index.html') {
|
|
79
107
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
@@ -81,13 +109,28 @@ export async function handleRequest(req, res, pool, options, authToken, rateLimi
|
|
|
81
109
|
return;
|
|
82
110
|
}
|
|
83
111
|
const queryToken = url.searchParams.get('token');
|
|
84
|
-
if (queryToken
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
112
|
+
if (queryToken) {
|
|
113
|
+
// The printed URL's token is a ONE-TIME ticket, exchanged here for the
|
|
114
|
+
// session cookie by the first GET that carries it. HEAD never spends it.
|
|
115
|
+
const outcome = req.method === 'GET' ? redeemTicket(ticketGate, queryToken) : 'invalid';
|
|
116
|
+
if (outcome === 'granted') {
|
|
117
|
+
res.writeHead(302, {
|
|
118
|
+
Location: '/',
|
|
119
|
+
'Set-Cookie': `turbine_observe_token=${authToken}; Path=/; HttpOnly; SameSite=Strict`,
|
|
120
|
+
});
|
|
121
|
+
res.end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (outcome === 'used') {
|
|
125
|
+
if (isAuthorized(req, authToken)) {
|
|
126
|
+
res.writeHead(302, { Location: '/' });
|
|
127
|
+
res.end();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
sendText(res, 403, 'This login link was already used. Restart Observe to get a new one.');
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
91
134
|
}
|
|
92
135
|
sendHtml(res, 200, OBSERVE_HTML, cspNonce());
|
|
93
136
|
return;
|
|
@@ -133,13 +176,18 @@ function isAuthorized(req, expectedToken) {
|
|
|
133
176
|
}
|
|
134
177
|
return false;
|
|
135
178
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
179
|
+
/**
|
|
180
|
+
* The ticket for a caller that passed none (a hand-built dispatch): `authToken`
|
|
181
|
+
* itself, remembered per token so it is still spent exactly once.
|
|
182
|
+
*/
|
|
183
|
+
const FALLBACK_GATES = new Map();
|
|
184
|
+
function gateFor(authToken) {
|
|
185
|
+
let gate = FALLBACK_GATES.get(authToken);
|
|
186
|
+
if (!gate) {
|
|
187
|
+
gate = createTicketGate(authToken);
|
|
188
|
+
FALLBACK_GATES.set(authToken, gate);
|
|
189
|
+
}
|
|
190
|
+
return gate;
|
|
143
191
|
}
|
|
144
192
|
// ---------------------------------------------------------------------------
|
|
145
193
|
// API handlers
|
|
@@ -188,7 +236,7 @@ async function apiLatency(res, pool, params) {
|
|
|
188
236
|
// this server writes no log of its own, so the HTTP response is the only
|
|
189
237
|
// channel either error could reach. Same shape as cli/studio.ts.
|
|
190
238
|
}
|
|
191
|
-
sendJson(res, 500, { error:
|
|
239
|
+
sendJson(res, 500, { error: responseErrorText(err) });
|
|
192
240
|
}
|
|
193
241
|
finally {
|
|
194
242
|
client.release();
|
|
@@ -229,7 +277,7 @@ async function apiModels(res, pool, params) {
|
|
|
229
277
|
// this server writes no log of its own, so the HTTP response is the only
|
|
230
278
|
// channel either error could reach. Same shape as cli/studio.ts.
|
|
231
279
|
}
|
|
232
|
-
sendJson(res, 500, { error:
|
|
280
|
+
sendJson(res, 500, { error: responseErrorText(err) });
|
|
233
281
|
}
|
|
234
282
|
finally {
|
|
235
283
|
client.release();
|
|
@@ -277,21 +325,3 @@ function sendText(res, status, text) {
|
|
|
277
325
|
res.writeHead(status, { ...SECURITY_HEADERS, 'Content-Security-Policy': JSON_CSP, 'Content-Type': 'text/plain' });
|
|
278
326
|
res.end(text);
|
|
279
327
|
}
|
|
280
|
-
// ---------------------------------------------------------------------------
|
|
281
|
-
// Browser open
|
|
282
|
-
// ---------------------------------------------------------------------------
|
|
283
|
-
function openUrl(url) {
|
|
284
|
-
const { platform: os } = process;
|
|
285
|
-
const { spawn } = require('node:child_process');
|
|
286
|
-
try {
|
|
287
|
-
if (os === 'darwin')
|
|
288
|
-
spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
289
|
-
else if (os === 'win32')
|
|
290
|
-
spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
|
|
291
|
-
else
|
|
292
|
-
spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
// Best effort
|
|
296
|
-
}
|
|
297
|
-
}
|
|
@@ -62,32 +62,15 @@
|
|
|
62
62
|
* name-carrying position that is not a string is refused as a SHAPE here,
|
|
63
63
|
* never skipped: skipping is the branch that leaks.
|
|
64
64
|
*/
|
|
65
|
+
import { RELATION_FILTER_WRAPPERS } from '../query/filters.js';
|
|
65
66
|
import type { SchemaMetadata, TableMetadata } from '../schema.js';
|
|
66
67
|
/**
|
|
67
|
-
* Relation-filter wrappers whose body is a clause against the relation's target
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* builder unguarded.
|
|
72
|
-
*
|
|
73
|
-
* 1. this one, the only NAMED definition;
|
|
74
|
-
* 2. `query/where-compile.ts`, inlined as `'some' in x || 'every' in x || …`;
|
|
75
|
-
* 3. `prisma-compat.ts`, SPLIT across `RELATION_QUANTIFIERS`
|
|
76
|
-
* (`some`/`every`/`none`) and two inline `k === 'is' || k === 'isNot'`
|
|
77
|
-
* tests, which is the copy most likely to drift because half of it does
|
|
78
|
-
* not look like a list at all.
|
|
79
|
-
*
|
|
80
|
-
* Copy 2 belongs to the SQL compiler and cannot import this one (a query-path
|
|
81
|
-
* module must not depend on `cli/`), so the deduplication has to go the other
|
|
82
|
-
* way: the list wants to live in `query/filters.ts` and be imported here, the
|
|
83
|
-
* way {@link COLUMN_REF_OPERATORS} above now is.
|
|
84
|
-
*
|
|
85
|
-
* That destination now EXISTS (`RELATION_FILTER_WRAPPERS` in `query/filters.ts`)
|
|
86
|
-
* and nothing imports it, including this file, so it is a fourth copy rather
|
|
87
|
-
* than a deduplication and this comment is still the only thing holding them in
|
|
88
|
-
* step. Moving each walk onto it is a change per walk, each with its own tests.
|
|
68
|
+
* Relation-filter wrappers whose body is a clause against the relation's target:
|
|
69
|
+
* the SQL compiler's own list (`query/filters.ts`), imported so a wrapper the
|
|
70
|
+
* compiler understands can never reach the builder unguarded. Re-exported for
|
|
71
|
+
* callers that still import it from here.
|
|
89
72
|
*/
|
|
90
|
-
export
|
|
73
|
+
export { RELATION_FILTER_WRAPPERS };
|
|
91
74
|
/**
|
|
92
75
|
* Recursion bound for the guard walk.
|
|
93
76
|
*
|
|
@@ -101,6 +84,43 @@ export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every"
|
|
|
101
84
|
* nothing the builder would accept is refused here for depth alone.
|
|
102
85
|
*/
|
|
103
86
|
export declare const PII_GUARD_MAX_DEPTH = 32;
|
|
87
|
+
/**
|
|
88
|
+
* Find a `lock` option anywhere a query level can carry one: the args
|
|
89
|
+
* themselves, or the options of any `with` entry at any depth. Returns the
|
|
90
|
+
* dotted path to the shallowest hit (`lock`, `with.posts.lock`), or `null`.
|
|
91
|
+
*
|
|
92
|
+
* WHY THIS IS NOT PART OF {@link assertNoPiiPredicates}. A locking read is not
|
|
93
|
+
* a hidden-column question, and the PII walk is conditional by design: Studio
|
|
94
|
+
* skips it under `--show-pii`, `explain_query` hands it an allowlisted subset
|
|
95
|
+
* of its arguments, and `compile_query` hands it only `where` for an aggregate.
|
|
96
|
+
* A read-only refusal has to hold in every one of those, so each surface calls
|
|
97
|
+
* this directly and unconditionally. It lives in this module because both
|
|
98
|
+
* surfaces already import it, which is what keeps the rule and its wording in
|
|
99
|
+
* one place.
|
|
100
|
+
*
|
|
101
|
+
* ANY VALUE other than `undefined` counts, `null` and `false` included. The
|
|
102
|
+
* builder treats exactly `undefined` as "no lock" and refuses every other
|
|
103
|
+
* non-lock value itself (E003), so answering "present" for all of them means
|
|
104
|
+
* the verdict here never depends on what the builder would have made of it.
|
|
105
|
+
*
|
|
106
|
+
* A `with` MAP is keyed by relation names, so a relation that happens to be
|
|
107
|
+
* called `lock` is a level to descend into and never a hit; only the option
|
|
108
|
+
* position on a level is read. A table column called `lock` is likewise safe:
|
|
109
|
+
* columns live under `where` / `orderBy` / `select`, which are not searched.
|
|
110
|
+
*
|
|
111
|
+
* Iterative and breadth-first, so a deeply nested payload costs time and no
|
|
112
|
+
* stack, and the shallowest hit is the one reported. The visited set makes it
|
|
113
|
+
* terminate on a cyclic object, which parsed JSON cannot produce but a caller
|
|
114
|
+
* of a pure helper can.
|
|
115
|
+
*/
|
|
116
|
+
export declare function findRowLockOption(args: Record<string, unknown>): string | null;
|
|
117
|
+
/**
|
|
118
|
+
* The refusal text for a `lock` found by {@link findRowLockOption}, shared so
|
|
119
|
+
* the two surfaces cannot word the same rule two ways. `path` is interpolated
|
|
120
|
+
* as given: it can carry a caller-chosen relation name, so each surface passes
|
|
121
|
+
* it through its own printable-name escaping first.
|
|
122
|
+
*/
|
|
123
|
+
export declare function rowLockRefusalMessage(path: string, tool: string): string;
|
|
104
124
|
/**
|
|
105
125
|
* The two things Studio and the MCP server genuinely do differently, plus the
|
|
106
126
|
* schema the walk resolves relation targets against.
|
|
@@ -62,34 +62,16 @@
|
|
|
62
62
|
* name-carrying position that is not a string is refused as a SHAPE here,
|
|
63
63
|
* never skipped: skipping is the branch that leaks.
|
|
64
64
|
*/
|
|
65
|
-
import {
|
|
65
|
+
import { isPlainColumnRefShape } from '../query/column-ref.js';
|
|
66
|
+
import { COLUMN_REF_OPERATORS, isColumnRef, RELATION_FILTER_WRAPPER_SET, RELATION_FILTER_WRAPPERS, } from '../query/filters.js';
|
|
66
67
|
import { ownLookup, resolveColumnName, resolveRelation } from '../query/utils.js';
|
|
67
68
|
/**
|
|
68
|
-
* Relation-filter wrappers whose body is a clause against the relation's target
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* builder unguarded.
|
|
73
|
-
*
|
|
74
|
-
* 1. this one, the only NAMED definition;
|
|
75
|
-
* 2. `query/where-compile.ts`, inlined as `'some' in x || 'every' in x || …`;
|
|
76
|
-
* 3. `prisma-compat.ts`, SPLIT across `RELATION_QUANTIFIERS`
|
|
77
|
-
* (`some`/`every`/`none`) and two inline `k === 'is' || k === 'isNot'`
|
|
78
|
-
* tests, which is the copy most likely to drift because half of it does
|
|
79
|
-
* not look like a list at all.
|
|
80
|
-
*
|
|
81
|
-
* Copy 2 belongs to the SQL compiler and cannot import this one (a query-path
|
|
82
|
-
* module must not depend on `cli/`), so the deduplication has to go the other
|
|
83
|
-
* way: the list wants to live in `query/filters.ts` and be imported here, the
|
|
84
|
-
* way {@link COLUMN_REF_OPERATORS} above now is.
|
|
85
|
-
*
|
|
86
|
-
* That destination now EXISTS (`RELATION_FILTER_WRAPPERS` in `query/filters.ts`)
|
|
87
|
-
* and nothing imports it, including this file, so it is a fourth copy rather
|
|
88
|
-
* than a deduplication and this comment is still the only thing holding them in
|
|
89
|
-
* step. Moving each walk onto it is a change per walk, each with its own tests.
|
|
69
|
+
* Relation-filter wrappers whose body is a clause against the relation's target:
|
|
70
|
+
* the SQL compiler's own list (`query/filters.ts`), imported so a wrapper the
|
|
71
|
+
* compiler understands can never reach the builder unguarded. Re-exported for
|
|
72
|
+
* callers that still import it from here.
|
|
90
73
|
*/
|
|
91
|
-
export
|
|
92
|
-
const RELATION_FILTER_WRAPPER_SET = new Set(RELATION_FILTER_WRAPPERS);
|
|
74
|
+
export { RELATION_FILTER_WRAPPERS };
|
|
93
75
|
/**
|
|
94
76
|
* Recursion bound for the guard walk.
|
|
95
77
|
*
|
|
@@ -129,6 +111,78 @@ const LEVEL_OBJECT_KEYS = new Set([
|
|
|
129
111
|
* which is what the builder itself branches on.
|
|
130
112
|
*/
|
|
131
113
|
const RELATION_PICK_KEYS = new Set(['pick', 'by', 'direction', 'nulls', 'plan']);
|
|
114
|
+
/**
|
|
115
|
+
* Find a `lock` option anywhere a query level can carry one: the args
|
|
116
|
+
* themselves, or the options of any `with` entry at any depth. Returns the
|
|
117
|
+
* dotted path to the shallowest hit (`lock`, `with.posts.lock`), or `null`.
|
|
118
|
+
*
|
|
119
|
+
* WHY THIS IS NOT PART OF {@link assertNoPiiPredicates}. A locking read is not
|
|
120
|
+
* a hidden-column question, and the PII walk is conditional by design: Studio
|
|
121
|
+
* skips it under `--show-pii`, `explain_query` hands it an allowlisted subset
|
|
122
|
+
* of its arguments, and `compile_query` hands it only `where` for an aggregate.
|
|
123
|
+
* A read-only refusal has to hold in every one of those, so each surface calls
|
|
124
|
+
* this directly and unconditionally. It lives in this module because both
|
|
125
|
+
* surfaces already import it, which is what keeps the rule and its wording in
|
|
126
|
+
* one place.
|
|
127
|
+
*
|
|
128
|
+
* ANY VALUE other than `undefined` counts, `null` and `false` included. The
|
|
129
|
+
* builder treats exactly `undefined` as "no lock" and refuses every other
|
|
130
|
+
* non-lock value itself (E003), so answering "present" for all of them means
|
|
131
|
+
* the verdict here never depends on what the builder would have made of it.
|
|
132
|
+
*
|
|
133
|
+
* A `with` MAP is keyed by relation names, so a relation that happens to be
|
|
134
|
+
* called `lock` is a level to descend into and never a hit; only the option
|
|
135
|
+
* position on a level is read. A table column called `lock` is likewise safe:
|
|
136
|
+
* columns live under `where` / `orderBy` / `select`, which are not searched.
|
|
137
|
+
*
|
|
138
|
+
* Iterative and breadth-first, so a deeply nested payload costs time and no
|
|
139
|
+
* stack, and the shallowest hit is the one reported. The visited set makes it
|
|
140
|
+
* terminate on a cyclic object, which parsed JSON cannot produce but a caller
|
|
141
|
+
* of a pure helper can.
|
|
142
|
+
*/
|
|
143
|
+
export function findRowLockOption(args) {
|
|
144
|
+
const queue = [{ level: args, parent: null, relation: '' }];
|
|
145
|
+
const seen = new Set();
|
|
146
|
+
for (let next = 0; next < queue.length; next++) {
|
|
147
|
+
const frame = queue[next];
|
|
148
|
+
if (seen.has(frame.level))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(frame.level);
|
|
151
|
+
if (frame.level.lock !== undefined)
|
|
152
|
+
return lockPath(frame);
|
|
153
|
+
const withClause = frame.level.with;
|
|
154
|
+
if (!isPlainObject(withClause))
|
|
155
|
+
continue;
|
|
156
|
+
for (const [relation, spec] of Object.entries(withClause)) {
|
|
157
|
+
if (isPlainObject(spec))
|
|
158
|
+
queue.push({ level: spec, parent: frame, relation });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/** `lock`, `with.posts.lock`, `with.posts.with.author.lock`: the path from the root to a hit. */
|
|
164
|
+
function lockPath(frame) {
|
|
165
|
+
const relations = [];
|
|
166
|
+
for (let at = frame; at !== null && at.parent !== null; at = at.parent) {
|
|
167
|
+
relations.push(at.relation);
|
|
168
|
+
}
|
|
169
|
+
const parts = [];
|
|
170
|
+
for (let i = relations.length - 1; i >= 0; i--)
|
|
171
|
+
parts.push('with', relations[i]);
|
|
172
|
+
parts.push('lock');
|
|
173
|
+
return parts.join('.');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The refusal text for a `lock` found by {@link findRowLockOption}, shared so
|
|
177
|
+
* the two surfaces cannot word the same rule two ways. `path` is interpolated
|
|
178
|
+
* as given: it can carry a caller-chosen relation name, so each surface passes
|
|
179
|
+
* it through its own printable-name escaping first.
|
|
180
|
+
*/
|
|
181
|
+
export function rowLockRefusalMessage(path, tool) {
|
|
182
|
+
return (`${tool} does not accept \`${path}\`: it is read-only, and a locking read takes row locks, which needs a ` +
|
|
183
|
+
'write-capable transaction in your application. Remove it from the query here (nothing else about the query ' +
|
|
184
|
+
'changes), and run the locking read inside db.$transaction(async (tx) => ...) in your code.');
|
|
185
|
+
}
|
|
132
186
|
/** An object or array value, i.e. something that can carry a nested name. */
|
|
133
187
|
function isObjectLike(value) {
|
|
134
188
|
return value !== null && typeof value === 'object';
|
|
@@ -224,23 +278,34 @@ export function assertNoPiiPredicates(args, rootTable, host) {
|
|
|
224
278
|
/**
|
|
225
279
|
* A column-keyed operator object (`{ gt: 5 }`, `{ sort, nulls }`, a JSON path,
|
|
226
280
|
* a vector distance) holds values, not names, with ONE exception: the
|
|
227
|
-
* comparison operators also accept `
|
|
228
|
-
* a column-to-column comparison in the same table. Scan for
|
|
281
|
+
* comparison operators also accept a column reference (`col('otherField')`),
|
|
282
|
+
* which compiles to a column-to-column comparison in the same table. Scan for
|
|
283
|
+
* that, including inside a `not` operator object, which compiles to
|
|
284
|
+
* `NOT (<inner>)` with the same operand positions.
|
|
285
|
+
*
|
|
286
|
+
* A reference carries a symbol brand that parsed JSON cannot produce, so the
|
|
287
|
+
* request data these tools compile never holds one, and the builder refuses
|
|
288
|
+
* the plain `{ col }` spelling. That spelling is refused HERE as well, as a
|
|
289
|
+
* shape: the guard must not depend on the builder to close a channel it is
|
|
290
|
+
* responsible for.
|
|
229
291
|
*
|
|
230
292
|
* The operator list is IMPORTED from `query/filters.ts` rather than copied:
|
|
231
293
|
* that is the set the SQL compiler itself branches on, and a guard that
|
|
232
294
|
* disagreed with it would be guarding a different query than the one that
|
|
233
|
-
* runs.
|
|
234
|
-
* channel this scan exists to close, silently.
|
|
295
|
+
* runs.
|
|
235
296
|
*/
|
|
236
|
-
const checkColumnRefs = (table, operatorNode) => {
|
|
297
|
+
const checkColumnRefs = (table, operatorNode, depth) => {
|
|
298
|
+
assertWithinDepth(depth);
|
|
237
299
|
for (const opKey of COLUMN_REF_OPERATORS) {
|
|
238
300
|
const operand = ownLookup(operatorNode, opKey);
|
|
239
|
-
if (
|
|
301
|
+
if (isColumnRef(operand)) {
|
|
302
|
+
checkColumnName(table, operand.col);
|
|
240
303
|
continue;
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
304
|
+
}
|
|
305
|
+
if (isPlainColumnRefShape(operand))
|
|
306
|
+
host.refuseShape(table, `${opKey}: { col }`);
|
|
307
|
+
if (opKey === 'not' && isPlainObject(operand))
|
|
308
|
+
checkColumnRefs(table, operand, depth + 1);
|
|
244
309
|
}
|
|
245
310
|
};
|
|
246
311
|
/**
|
|
@@ -295,7 +360,7 @@ export function assertNoPiiPredicates(args, rootTable, host) {
|
|
|
295
360
|
if (scope === 'relation' && !table.allColumns.includes(column))
|
|
296
361
|
host.refuseShape(table, key);
|
|
297
362
|
if (isPlainObject(value))
|
|
298
|
-
checkColumnRefs(table, value);
|
|
363
|
+
checkColumnRefs(table, value, depth + 1);
|
|
299
364
|
};
|
|
300
365
|
/** A where / orderBy / cursor object (or a Prisma-style array of them). */
|
|
301
366
|
const visitClause = (node, table, depth) => {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* Pure leaf: no filesystem, database, or process access. Passing `schema: null`
|
|
13
13
|
* yields a parse-only report (every item `parsed`, no map) for `--no-db`.
|
|
14
14
|
*/
|
|
15
|
+
import { nullRecord } from '../introspect.js';
|
|
16
|
+
import { ownLookup } from '../query/utils.js';
|
|
15
17
|
import { camelToSnake, snakeToCamel, withDbFieldNames } from '../schema.js';
|
|
16
18
|
export { DEFAULT_EXCLUDED_TABLES } from '../introspect.js';
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
@@ -101,7 +103,9 @@ export function resolvePrismaSchema(ast, schema, options = {}) {
|
|
|
101
103
|
const result = {
|
|
102
104
|
models: [],
|
|
103
105
|
enums: [],
|
|
104
|
-
|
|
106
|
+
// Prototype-less: models, fields, relations and enums are keyed by names the
|
|
107
|
+
// Prisma schema chose, and `__proto__` / `constructor` are legal names there.
|
|
108
|
+
map: { models: nullRecord(), enums: nullRecord() },
|
|
105
109
|
hasUnresolved: false,
|
|
106
110
|
parseWarnings: ast.warnings,
|
|
107
111
|
noDb,
|
|
@@ -110,7 +114,7 @@ export function resolvePrismaSchema(ast, schema, options = {}) {
|
|
|
110
114
|
for (const model of ast.models) {
|
|
111
115
|
const rt = modelTable.get(model.name);
|
|
112
116
|
const table = rt.table;
|
|
113
|
-
const tableMeta = table && resolvedSchema ? resolvedSchema.tables
|
|
117
|
+
const tableMeta = table && resolvedSchema ? ownLookup(resolvedSchema.tables, table) : undefined;
|
|
114
118
|
const accessor = table ? snakeToCamel(table) : null;
|
|
115
119
|
const status = noDb ? 'parsed' : table ? 'resolved' : 'unresolved';
|
|
116
120
|
const resolved = {
|
|
@@ -143,23 +147,23 @@ export function resolvePrismaSchema(ast, schema, options = {}) {
|
|
|
143
147
|
result.models.push(resolved);
|
|
144
148
|
// Build the verified map entry (skip in --no-db and for unresolved tables).
|
|
145
149
|
if (!noDb && table && accessor) {
|
|
146
|
-
const fields =
|
|
150
|
+
const fields = nullRecord();
|
|
147
151
|
for (const f of resolved.fields) {
|
|
148
152
|
if (f.status === 'resolved' && f.turbineField)
|
|
149
153
|
fields[f.prismaName] = f.turbineField;
|
|
150
154
|
}
|
|
151
|
-
const relations =
|
|
155
|
+
const relations = nullRecord();
|
|
152
156
|
for (const r of resolved.relations) {
|
|
153
157
|
if (r.status === 'resolved' && r.turbineName && r.cardinality) {
|
|
154
158
|
relations[r.prismaName] = { name: r.turbineName, cardinality: r.cardinality };
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
|
-
const compoundUniques =
|
|
161
|
+
const compoundUniques = nullRecord();
|
|
158
162
|
for (const c of resolved.compoundUniques) {
|
|
159
163
|
if (c.status === 'resolved' && c.turbineFields)
|
|
160
164
|
compoundUniques[c.selector] = c.turbineFields;
|
|
161
165
|
}
|
|
162
|
-
const clientDefaults = collectClientDefaults(model, fields, resolvedSchema?.tables
|
|
166
|
+
const clientDefaults = collectClientDefaults(model, fields, ownLookup(resolvedSchema?.tables ?? {}, table));
|
|
163
167
|
result.map.models[model.name] = {
|
|
164
168
|
table,
|
|
165
169
|
accessor,
|
|
@@ -259,7 +263,7 @@ function resolveScalarField(model, fieldName, tableMeta, noDb) {
|
|
|
259
263
|
if (noDb || !tableMeta) {
|
|
260
264
|
return { prismaName: fieldName, turbineField: null, column: null, status: noDb ? 'parsed' : 'unresolved' };
|
|
261
265
|
}
|
|
262
|
-
const turbineField = tableMeta.reverseColumnMap
|
|
266
|
+
const turbineField = ownLookup(tableMeta.reverseColumnMap, column);
|
|
263
267
|
if (turbineField) {
|
|
264
268
|
return { prismaName: fieldName, turbineField, column, status: 'resolved' };
|
|
265
269
|
}
|
|
@@ -305,9 +309,9 @@ function relationFkColumns(model, field) {
|
|
|
305
309
|
function collectClientDefaults(model, resolvedFields, tableMeta) {
|
|
306
310
|
if (!tableMeta)
|
|
307
311
|
return undefined;
|
|
308
|
-
const out =
|
|
312
|
+
const out = nullRecord();
|
|
309
313
|
for (const field of model.fields) {
|
|
310
|
-
const turbineField = resolvedFields
|
|
314
|
+
const turbineField = ownLookup(resolvedFields, field.name);
|
|
311
315
|
if (!turbineField)
|
|
312
316
|
continue;
|
|
313
317
|
const col = tableMeta.columns.find((c) => c.field === turbineField || c.name === turbineField);
|
|
@@ -467,11 +471,11 @@ function resolveCompoundUnique(model, key, tableMeta, noDb) {
|
|
|
467
471
|
if (noDb || !tableMeta)
|
|
468
472
|
return base;
|
|
469
473
|
const columns = key.fields.map((f) => fieldColumn(model, f));
|
|
470
|
-
const missing = columns.filter((c) => !tableMeta.reverseColumnMap
|
|
474
|
+
const missing = columns.filter((c) => !ownLookup(tableMeta.reverseColumnMap, c));
|
|
471
475
|
if (missing.length > 0) {
|
|
472
476
|
return { ...base, reason: `column(s) not found: ${missing.join(', ')}` };
|
|
473
477
|
}
|
|
474
|
-
const turbineFields = columns.map((c) => tableMeta.reverseColumnMap
|
|
478
|
+
const turbineFields = columns.map((c) => ownLookup(tableMeta.reverseColumnMap, c));
|
|
475
479
|
const want = [...columns].sort().join(',');
|
|
476
480
|
const matches = (setList) => setList.some((s) => [...s].sort().join(',') === want);
|
|
477
481
|
if (key.kind === 'id') {
|
|
@@ -87,11 +87,19 @@ function lineAt(src, offset) {
|
|
|
87
87
|
// ---------------------------------------------------------------------------
|
|
88
88
|
// Attribute-argument tokenizer
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
90
|
+
const QUOTED_ESCAPES = { '"': '"', '\\': '\\', n: '\n', t: '\t' };
|
|
91
|
+
/**
|
|
92
|
+
* Unquote a `"..."` literal, resolving the escapes Prisma supports.
|
|
93
|
+
*
|
|
94
|
+
* ONE left-to-right pass, because the escapes are not independent: after `\\`
|
|
95
|
+
* has become a backslash, that backslash must not be read again as the start of
|
|
96
|
+
* `\n` or `\t`. A chain of `.replace` calls does exactly that, so the literal
|
|
97
|
+
* `"C:\\new"` came out as `C:` + newline + `ew`.
|
|
98
|
+
*/
|
|
91
99
|
function unquote(raw) {
|
|
92
100
|
const s = raw.trim();
|
|
93
101
|
if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
|
|
94
|
-
return s.slice(1, -1).replace(/\\"/g,
|
|
102
|
+
return s.slice(1, -1).replace(/\\(["\\nt])/g, (_, escaped) => QUOTED_ESCAPES[escaped]);
|
|
95
103
|
}
|
|
96
104
|
return s;
|
|
97
105
|
}
|
|
@@ -423,7 +431,7 @@ function parseFieldLine(text, line, warnings) {
|
|
|
423
431
|
`Turbine generates the column from the database, so it IS present on the generated client.`);
|
|
424
432
|
}
|
|
425
433
|
for (const attr of attrs) {
|
|
426
|
-
const note = NOTED_FIELD_ATTRS[attr.name];
|
|
434
|
+
const note = Object.hasOwn(NOTED_FIELD_ATTRS, attr.name) ? NOTED_FIELD_ATTRS[attr.name] : undefined;
|
|
427
435
|
if (note)
|
|
428
436
|
warnings.push(`Line ${line}: field "${name}": ${note}`);
|
|
429
437
|
}
|
|
@@ -457,7 +465,7 @@ function parseModelBody(block, kind, src, warnings) {
|
|
|
457
465
|
// @@index and anything else: recorded in blockAttrs, unused. The
|
|
458
466
|
// few that change what the model MEANS get a warning rather than
|
|
459
467
|
// silence (see NOTED_BLOCK_ATTRS).
|
|
460
|
-
const note = NOTED_BLOCK_ATTRS[attr.name];
|
|
468
|
+
const note = Object.hasOwn(NOTED_BLOCK_ATTRS, attr.name) ? NOTED_BLOCK_ATTRS[attr.name] : undefined;
|
|
461
469
|
if (note)
|
|
462
470
|
warnings.push(`Line ${line}: model "${block.name}": ${note}`);
|
|
463
471
|
}
|