turbine-orm 0.65.0 → 0.66.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 +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- package/package.json +6 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm CLI: the PII predicate guard
|
|
3
|
+
*
|
|
4
|
+
* A pure leaf module, the same role `cli/rate-limit.ts` and `cli/destructive.ts`
|
|
5
|
+
* play: no I/O, no state, and no import from a sibling CLI module. The two
|
|
6
|
+
* local tools that compile CALLER-SUPPLIED `findMany` args, Studio's
|
|
7
|
+
* `/api/builder` and the MCP server's `explain_query`, both walk their args
|
|
8
|
+
* through this one walker before handing them to `QueryInterface`.
|
|
9
|
+
*
|
|
10
|
+
* It is one module because it was two. Studio and MCP each carried their own
|
|
11
|
+
* `assertNoPiiPredicates` with the same name and the same job, plus their own
|
|
12
|
+
* copy of `RELATION_FILTER_WRAPPERS` and `PII_GUARD_MAX_DEPTH`, and they drifted
|
|
13
|
+
* exactly the way two hand-synced implementations do: a hole opened in one and
|
|
14
|
+
* was present in the other, and closing it in one place would have left the
|
|
15
|
+
* other open. Both tools now share the walk and differ only in the two things
|
|
16
|
+
* that genuinely differ, WHY a column is hidden and HOW a refusal is thrown,
|
|
17
|
+
* which arrive as callbacks on {@link PiiGuardHost}.
|
|
18
|
+
*
|
|
19
|
+
* WHAT IT IS FOR. Redacting the cells of a hidden column is not enough on its
|
|
20
|
+
* own: `where: { email: { startsWith: 'a' } }` answers a question ABOUT the
|
|
21
|
+
* hidden value, and so does an `isNull`, an `orderBy`, a `cursor` (which
|
|
22
|
+
* compiles to a `"email" > $1` range comparison, i.e. a clean binary search over
|
|
23
|
+
* the byte range), and, more weakly, a `distinct` (the cardinality of the hidden
|
|
24
|
+
* values). `select` is deliberately NOT refused: it returns values, and the
|
|
25
|
+
* values are redacted on the way out.
|
|
26
|
+
*
|
|
27
|
+
* WHY IT FAILS CLOSED. Every branch here answers one question, "can this shape
|
|
28
|
+
* be shown not to name a hidden column", and the honest answer for a shape the
|
|
29
|
+
* walker does not recognize is no. Waving unrecognized shapes through is what
|
|
30
|
+
* produced the pick-row hole (`orderBy: { rel: { pick: {...}, by: 'phone' } }`):
|
|
31
|
+
* `pick` is neither a combinator nor a relation nor a column of the target, so
|
|
32
|
+
* the walker checked the literal string `pick` against the column list, found
|
|
33
|
+
* nothing, and moved on, while `by` named the hidden column in its VALUE rather
|
|
34
|
+
* than its key. The specific keys are handled below, but the FAIL-CLOSED rule is
|
|
35
|
+
* the actual fix: an unrecognized key carrying a structure is refused, so the
|
|
36
|
+
* next orderBy shape the query builder grows is refused here until someone
|
|
37
|
+
* teaches this walker about it. Over-refusing is a usability bug; under-refusing
|
|
38
|
+
* is a data leak.
|
|
39
|
+
*/
|
|
40
|
+
import type { SchemaMetadata, TableMetadata } from '../schema.js';
|
|
41
|
+
/**
|
|
42
|
+
* Relation-filter wrappers whose body is a clause against the relation's target.
|
|
43
|
+
*
|
|
44
|
+
* THREE copies of this list exist in the tree, and they have to agree: a wrapper
|
|
45
|
+
* the compiler understands and this guard does not is a wrapper that reaches the
|
|
46
|
+
* builder unguarded.
|
|
47
|
+
*
|
|
48
|
+
* 1. this one, the only NAMED definition;
|
|
49
|
+
* 2. `query/where-compile.ts`, inlined as `'some' in x || 'every' in x || …`;
|
|
50
|
+
* 3. `prisma-compat.ts`, SPLIT across `RELATION_QUANTIFIERS`
|
|
51
|
+
* (`some`/`every`/`none`) and two inline `k === 'is' || k === 'isNot'`
|
|
52
|
+
* tests, which is the copy most likely to drift because half of it does
|
|
53
|
+
* not look like a list at all.
|
|
54
|
+
*
|
|
55
|
+
* Copy 2 belongs to the SQL compiler and cannot import this one (a query-path
|
|
56
|
+
* module must not depend on `cli/`), so the deduplication has to go the other
|
|
57
|
+
* way: the list wants to live in `query/filters.ts` and be imported here, the
|
|
58
|
+
* way {@link COLUMN_REF_OPERATORS} above now is. Until that export exists this
|
|
59
|
+
* comment is the only thing holding the three in step.
|
|
60
|
+
*/
|
|
61
|
+
export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every", "is", "isNot"];
|
|
62
|
+
/**
|
|
63
|
+
* Recursion bound for the guard walk.
|
|
64
|
+
*
|
|
65
|
+
* This number is NOT the security boundary: reaching it REFUSES the request
|
|
66
|
+
* (`refuseDepth`). It only bounds the walk on a pathological payload. Returning
|
|
67
|
+
* quietly at the cap, which both copies of this walker once did, meant padding a
|
|
68
|
+
* payload with enough nested `NOT` wrappers walked the guard off the end of its
|
|
69
|
+
* own recursion and then handed the untouched predicate to the builder. It sits
|
|
70
|
+
* well above the query builder's own depth-10 relation cap
|
|
71
|
+
* (`CircularRelationError`) and far above any hand-composed boolean nesting, so
|
|
72
|
+
* nothing the builder would accept is refused here for depth alone.
|
|
73
|
+
*/
|
|
74
|
+
export declare const PII_GUARD_MAX_DEPTH = 32;
|
|
75
|
+
/**
|
|
76
|
+
* The two things Studio and the MCP server genuinely do differently, plus the
|
|
77
|
+
* schema the walk resolves relation targets against.
|
|
78
|
+
*
|
|
79
|
+
* `hiddenReason` is the whole policy: Studio hides code-first `pii` tags unless
|
|
80
|
+
* `--show-pii`, the MCP server hides those AND secret-looking column names. The
|
|
81
|
+
* `refuse*` callbacks throw, they never return, because the two tools raise
|
|
82
|
+
* different error types (`ValidationError` vs a JSON-RPC error).
|
|
83
|
+
*/
|
|
84
|
+
export interface PiiGuardHost {
|
|
85
|
+
/** Schema the walk resolves relation targets against. */
|
|
86
|
+
metadata: SchemaMetadata;
|
|
87
|
+
/**
|
|
88
|
+
* Why `column` on `table` may not appear in a predicate, or `null` when it
|
|
89
|
+
* may. The string is interpolated into the refusal message, so it reads as a
|
|
90
|
+
* predicate: `"is PII-tagged and redacted"`.
|
|
91
|
+
*/
|
|
92
|
+
hiddenReason(table: TableMetadata, column: string): string | null;
|
|
93
|
+
/** Refuse: the query names a hidden column. Must throw. */
|
|
94
|
+
refuseColumn(table: TableMetadata, column: string, reason: string): never;
|
|
95
|
+
/** Refuse: the query is nested past {@link PII_GUARD_MAX_DEPTH}. Must throw. */
|
|
96
|
+
refuseDepth(maxDepth: number): never;
|
|
97
|
+
/**
|
|
98
|
+
* Refuse: `key` on `table` is a query shape this walker does not recognize, so
|
|
99
|
+
* it cannot be shown not to name a hidden column. Must throw.
|
|
100
|
+
*/
|
|
101
|
+
refuseShape(table: TableMetadata, key: string): never;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Walk one query level (`findMany` args, or one `with` entry's options) and
|
|
105
|
+
* refuse it if it filters, sorts, pages, or de-duplicates on a hidden column, or
|
|
106
|
+
* if it carries a shape that cannot be proven not to.
|
|
107
|
+
*
|
|
108
|
+
* `rootTable` may be `undefined` (the caller's table name did not resolve); the
|
|
109
|
+
* walk then does nothing, because the builder rejects the query a moment later
|
|
110
|
+
* by name and there is no metadata here to judge it against.
|
|
111
|
+
*/
|
|
112
|
+
export declare function assertNoPiiPredicates(args: Record<string, unknown>, rootTable: TableMetadata | undefined, host: PiiGuardHost): void;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm CLI: the PII predicate guard
|
|
4
|
+
*
|
|
5
|
+
* A pure leaf module, the same role `cli/rate-limit.ts` and `cli/destructive.ts`
|
|
6
|
+
* play: no I/O, no state, and no import from a sibling CLI module. The two
|
|
7
|
+
* local tools that compile CALLER-SUPPLIED `findMany` args, Studio's
|
|
8
|
+
* `/api/builder` and the MCP server's `explain_query`, both walk their args
|
|
9
|
+
* through this one walker before handing them to `QueryInterface`.
|
|
10
|
+
*
|
|
11
|
+
* It is one module because it was two. Studio and MCP each carried their own
|
|
12
|
+
* `assertNoPiiPredicates` with the same name and the same job, plus their own
|
|
13
|
+
* copy of `RELATION_FILTER_WRAPPERS` and `PII_GUARD_MAX_DEPTH`, and they drifted
|
|
14
|
+
* exactly the way two hand-synced implementations do: a hole opened in one and
|
|
15
|
+
* was present in the other, and closing it in one place would have left the
|
|
16
|
+
* other open. Both tools now share the walk and differ only in the two things
|
|
17
|
+
* that genuinely differ, WHY a column is hidden and HOW a refusal is thrown,
|
|
18
|
+
* which arrive as callbacks on {@link PiiGuardHost}.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT IS FOR. Redacting the cells of a hidden column is not enough on its
|
|
21
|
+
* own: `where: { email: { startsWith: 'a' } }` answers a question ABOUT the
|
|
22
|
+
* hidden value, and so does an `isNull`, an `orderBy`, a `cursor` (which
|
|
23
|
+
* compiles to a `"email" > $1` range comparison, i.e. a clean binary search over
|
|
24
|
+
* the byte range), and, more weakly, a `distinct` (the cardinality of the hidden
|
|
25
|
+
* values). `select` is deliberately NOT refused: it returns values, and the
|
|
26
|
+
* values are redacted on the way out.
|
|
27
|
+
*
|
|
28
|
+
* WHY IT FAILS CLOSED. Every branch here answers one question, "can this shape
|
|
29
|
+
* be shown not to name a hidden column", and the honest answer for a shape the
|
|
30
|
+
* walker does not recognize is no. Waving unrecognized shapes through is what
|
|
31
|
+
* produced the pick-row hole (`orderBy: { rel: { pick: {...}, by: 'phone' } }`):
|
|
32
|
+
* `pick` is neither a combinator nor a relation nor a column of the target, so
|
|
33
|
+
* the walker checked the literal string `pick` against the column list, found
|
|
34
|
+
* nothing, and moved on, while `by` named the hidden column in its VALUE rather
|
|
35
|
+
* than its key. The specific keys are handled below, but the FAIL-CLOSED rule is
|
|
36
|
+
* the actual fix: an unrecognized key carrying a structure is refused, so the
|
|
37
|
+
* next orderBy shape the query builder grows is refused here until someone
|
|
38
|
+
* teaches this walker about it. Over-refusing is a usability bug; under-refusing
|
|
39
|
+
* is a data leak.
|
|
40
|
+
*/
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.PII_GUARD_MAX_DEPTH = exports.RELATION_FILTER_WRAPPERS = void 0;
|
|
43
|
+
exports.assertNoPiiPredicates = assertNoPiiPredicates;
|
|
44
|
+
const filters_js_1 = require("../query/filters.js");
|
|
45
|
+
const utils_js_1 = require("../query/utils.js");
|
|
46
|
+
/**
|
|
47
|
+
* Relation-filter wrappers whose body is a clause against the relation's target.
|
|
48
|
+
*
|
|
49
|
+
* THREE copies of this list exist in the tree, and they have to agree: a wrapper
|
|
50
|
+
* the compiler understands and this guard does not is a wrapper that reaches the
|
|
51
|
+
* builder unguarded.
|
|
52
|
+
*
|
|
53
|
+
* 1. this one, the only NAMED definition;
|
|
54
|
+
* 2. `query/where-compile.ts`, inlined as `'some' in x || 'every' in x || …`;
|
|
55
|
+
* 3. `prisma-compat.ts`, SPLIT across `RELATION_QUANTIFIERS`
|
|
56
|
+
* (`some`/`every`/`none`) and two inline `k === 'is' || k === 'isNot'`
|
|
57
|
+
* tests, which is the copy most likely to drift because half of it does
|
|
58
|
+
* not look like a list at all.
|
|
59
|
+
*
|
|
60
|
+
* Copy 2 belongs to the SQL compiler and cannot import this one (a query-path
|
|
61
|
+
* module must not depend on `cli/`), so the deduplication has to go the other
|
|
62
|
+
* way: the list wants to live in `query/filters.ts` and be imported here, the
|
|
63
|
+
* way {@link COLUMN_REF_OPERATORS} above now is. Until that export exists this
|
|
64
|
+
* comment is the only thing holding the three in step.
|
|
65
|
+
*/
|
|
66
|
+
exports.RELATION_FILTER_WRAPPERS = ['some', 'none', 'every', 'is', 'isNot'];
|
|
67
|
+
const RELATION_FILTER_WRAPPER_SET = new Set(exports.RELATION_FILTER_WRAPPERS);
|
|
68
|
+
/**
|
|
69
|
+
* Recursion bound for the guard walk.
|
|
70
|
+
*
|
|
71
|
+
* This number is NOT the security boundary: reaching it REFUSES the request
|
|
72
|
+
* (`refuseDepth`). It only bounds the walk on a pathological payload. Returning
|
|
73
|
+
* quietly at the cap, which both copies of this walker once did, meant padding a
|
|
74
|
+
* payload with enough nested `NOT` wrappers walked the guard off the end of its
|
|
75
|
+
* own recursion and then handed the untouched predicate to the builder. It sits
|
|
76
|
+
* well above the query builder's own depth-10 relation cap
|
|
77
|
+
* (`CircularRelationError`) and far above any hand-composed boolean nesting, so
|
|
78
|
+
* nothing the builder would accept is refused here for depth alone.
|
|
79
|
+
*/
|
|
80
|
+
exports.PII_GUARD_MAX_DEPTH = 32;
|
|
81
|
+
/**
|
|
82
|
+
* Query-level arg keys whose value is legitimately an object or an array.
|
|
83
|
+
*
|
|
84
|
+
* Everything else on `FindManyArgs` / `WithOptions` is a scalar (`limit`,
|
|
85
|
+
* `offset`, `take`, `timeout`, `relationLoadStrategy`, `stableRelationOrder`,
|
|
86
|
+
* `warnOnUnlimited`, `forceCustomPlan`, `includePii`), so an object-valued key
|
|
87
|
+
* outside this set is a structure the walker has never seen and is refused.
|
|
88
|
+
* `includePii` / `skipGlobalFilters` are gated by the `UNSAFE` symbol, which
|
|
89
|
+
* `JSON.parse` cannot produce, so neither can be forged over the wire.
|
|
90
|
+
*/
|
|
91
|
+
const LEVEL_OBJECT_KEYS = new Set([
|
|
92
|
+
'where',
|
|
93
|
+
'orderBy',
|
|
94
|
+
'cursor',
|
|
95
|
+
'distinct',
|
|
96
|
+
'with',
|
|
97
|
+
'select',
|
|
98
|
+
'omit',
|
|
99
|
+
'skipGlobalFilters',
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* Every key a pick-row ordering (`RelationPickOrderBy`) may carry. Used only to
|
|
103
|
+
* recognize the shape, mirroring `isRelationPickOrderBy` in `query/filters.ts`,
|
|
104
|
+
* which is what the builder itself branches on.
|
|
105
|
+
*/
|
|
106
|
+
const RELATION_PICK_KEYS = new Set(['pick', 'by', 'direction', 'nulls', 'plan']);
|
|
107
|
+
/** An object or array value, i.e. something that can carry a nested name. */
|
|
108
|
+
function isObjectLike(value) {
|
|
109
|
+
return value !== null && typeof value === 'object';
|
|
110
|
+
}
|
|
111
|
+
/** A non-array object: the shape every clause / spec node in the arg tree uses. */
|
|
112
|
+
function isPlainObject(value) {
|
|
113
|
+
return isObjectLike(value) && !Array.isArray(value);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The depth one element of an array is visited at: unchanged for an ordinary
|
|
117
|
+
* element, one level deeper for an element that is ITSELF an array.
|
|
118
|
+
*
|
|
119
|
+
* The distinction is the whole fix, and it is not the same as "arrays cost a
|
|
120
|
+
* level". Both array branches below deliberately did not increment, and the
|
|
121
|
+
* reason was right: an `OR: [a, b, c]` (or a Prisma-style `orderBy: [{…}, {…}]`)
|
|
122
|
+
* is ONE logical level however long the list is, and its elements are SIBLINGS,
|
|
123
|
+
* each visited from the same stack frame, so width costs no recursion. Charging
|
|
124
|
+
* a level per element would have refused a legal 40-condition `OR`.
|
|
125
|
+
*
|
|
126
|
+
* What that missed is that the array branch also recurses into an element that
|
|
127
|
+
* is another ARRAY, and there the frames DO stack. `{"orderBy":[[[…]]]}` walks
|
|
128
|
+
* one frame per bracket with the logical depth pinned at its starting value, so
|
|
129
|
+
* the cap never fires: measured, 1,000 levels (a 2 KB body) was allowed outright
|
|
130
|
+
* and 10,000 (20 KB) raised `RangeError: Maximum call stack size exceeded`
|
|
131
|
+
* inside the guard. That is the exact class the module header says was fixed by
|
|
132
|
+
* making the cap REFUSE instead of returning quietly, reintroduced through the
|
|
133
|
+
* one branch that never reaches the cap.
|
|
134
|
+
*
|
|
135
|
+
* So: nesting counts, iteration does not. No legal arg shape puts an array
|
|
136
|
+
* directly inside an array (`orderBy` is `X | X[]`, the combinators take `X[]`),
|
|
137
|
+
* so nothing the builder would accept is refused for depth by this rule, and it
|
|
138
|
+
* needs no second budget to reason about, the existing cap now simply sees the
|
|
139
|
+
* nesting it was already meant to be counting.
|
|
140
|
+
*/
|
|
141
|
+
function arrayItemDepth(item, depth) {
|
|
142
|
+
return Array.isArray(item) ? depth + 1 : depth;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Is this relation-keyed value a pick-row ordering rather than a relation filter
|
|
146
|
+
* or a to-one column ordering?
|
|
147
|
+
*
|
|
148
|
+
* Deliberately a MIRROR of `isRelationPickOrderBy` (`query/filters.ts`), down to
|
|
149
|
+
* requiring `pick` to be an object carrying `orderBy`: the builder branches on
|
|
150
|
+
* that predicate, so a guard that answered it differently would guard a
|
|
151
|
+
* different query than the one that runs. It matters in both directions. A
|
|
152
|
+
* target table with columns literally named `pick` and `by` produces
|
|
153
|
+
* `{ rel: { pick: 'asc', by: 'desc' } }`, which is an ordinary two-column to-one
|
|
154
|
+
* ordering to the builder and must be walked as two COLUMN names here. And a
|
|
155
|
+
* value with an object-valued `pick` that is not a full pick spec is not
|
|
156
|
+
* special-cased at all: it falls through to the fail-closed branch.
|
|
157
|
+
*/
|
|
158
|
+
function isPickShape(node) {
|
|
159
|
+
if (!Object.hasOwn(node, 'pick') || !Object.hasOwn(node, 'by'))
|
|
160
|
+
return false;
|
|
161
|
+
const pick = node.pick;
|
|
162
|
+
if (!isPlainObject(pick) || !Object.hasOwn(pick, 'orderBy'))
|
|
163
|
+
return false;
|
|
164
|
+
return Object.keys(node).every((key) => RELATION_PICK_KEYS.has(key));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Walk one query level (`findMany` args, or one `with` entry's options) and
|
|
168
|
+
* refuse it if it filters, sorts, pages, or de-duplicates on a hidden column, or
|
|
169
|
+
* if it carries a shape that cannot be proven not to.
|
|
170
|
+
*
|
|
171
|
+
* `rootTable` may be `undefined` (the caller's table name did not resolve); the
|
|
172
|
+
* walk then does nothing, because the builder rejects the query a moment later
|
|
173
|
+
* by name and there is no metadata here to judge it against.
|
|
174
|
+
*/
|
|
175
|
+
function assertNoPiiPredicates(args, rootTable, host) {
|
|
176
|
+
const assertWithinDepth = (depth) => {
|
|
177
|
+
if (depth <= exports.PII_GUARD_MAX_DEPTH)
|
|
178
|
+
return;
|
|
179
|
+
host.refuseDepth(exports.PII_GUARD_MAX_DEPTH);
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Check one caller-supplied name against `table`. A predicate may name a
|
|
183
|
+
* column by its camelCase field OR by its real column name; both compile to
|
|
184
|
+
* the same SQL, so both have to resolve to the same check.
|
|
185
|
+
*/
|
|
186
|
+
const checkColumnName = (table, name) => {
|
|
187
|
+
const column = (0, utils_js_1.ownLookup)(table.columnMap, name) ?? name;
|
|
188
|
+
const reason = host.hiddenReason(table, column);
|
|
189
|
+
if (reason)
|
|
190
|
+
host.refuseColumn(table, column, reason);
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* A column-keyed operator object (`{ gt: 5 }`, `{ sort, nulls }`, a JSON path,
|
|
194
|
+
* a vector distance) holds values, not names, with ONE exception: the
|
|
195
|
+
* comparison operators also accept `{ col: 'otherField' }`, which compiles to
|
|
196
|
+
* a column-to-column comparison in the same table. Scan for that.
|
|
197
|
+
*
|
|
198
|
+
* The operator list is IMPORTED from `query/filters.ts` rather than copied:
|
|
199
|
+
* that is the set the SQL compiler itself branches on, and a guard that
|
|
200
|
+
* disagreed with it would be guarding a different query than the one that
|
|
201
|
+
* runs. A copy that drifted short by one operator reopens the operand-position
|
|
202
|
+
* channel this scan exists to close, silently.
|
|
203
|
+
*/
|
|
204
|
+
const checkColumnRefs = (table, operatorNode) => {
|
|
205
|
+
for (const opKey of filters_js_1.COLUMN_REF_OPERATORS) {
|
|
206
|
+
const operand = (0, utils_js_1.ownLookup)(operatorNode, opKey);
|
|
207
|
+
if (!isPlainObject(operand))
|
|
208
|
+
continue;
|
|
209
|
+
const ref = operand.col;
|
|
210
|
+
if (typeof ref === 'string')
|
|
211
|
+
checkColumnName(table, ref);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* One `[key, value]` entry of a clause node.
|
|
216
|
+
*
|
|
217
|
+
* `scope` says which key vocabulary applies. `'clause'` is a where / orderBy /
|
|
218
|
+
* cursor object resolved against a table: combinators, relation names, column
|
|
219
|
+
* names. `'relation'` is the value SITTING UNDER a relation name, which also
|
|
220
|
+
* accepts the cardinality wrappers, and is where the fail-closed rule bites:
|
|
221
|
+
* an unrecognized key carrying a structure is refused rather than checked as
|
|
222
|
+
* if it were a column name and waved through when it is not one.
|
|
223
|
+
*
|
|
224
|
+
* `_count` needs no case of its own. It is walked as a column name, which
|
|
225
|
+
* clears it on every real schema, refuses it on the pathological one that has
|
|
226
|
+
* a hidden column with that name, and, since its value is a bare direction
|
|
227
|
+
* token, never reaches the fail-closed branch.
|
|
228
|
+
*/
|
|
229
|
+
const visitEntry = (table, key, value, depth, scope) => {
|
|
230
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
231
|
+
visitClause(value, table, depth + 1);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// `{ user: { is: {...} } }` / `{ posts: { some: {...} } }`: the wrapper's
|
|
235
|
+
// body is a clause against the SAME target. Handing the wrapper to the
|
|
236
|
+
// column walker instead walked `is` as if it were a column of the target,
|
|
237
|
+
// so the inner clause was never visited at all.
|
|
238
|
+
if (scope === 'relation' && RELATION_FILTER_WRAPPER_SET.has(key)) {
|
|
239
|
+
visitClause(value, table, depth + 1);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const relation = (0, utils_js_1.ownLookup)(table.relations, key);
|
|
243
|
+
if (relation) {
|
|
244
|
+
const target = (0, utils_js_1.ownLookup)(host.metadata.tables, relation.to);
|
|
245
|
+
// A relation whose target is not in the metadata cannot be walked, and a
|
|
246
|
+
// walk that cannot see the target cannot clear it.
|
|
247
|
+
if (!target)
|
|
248
|
+
host.refuseShape(table, key);
|
|
249
|
+
visitRelationValue(value, target, depth + 1);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
checkColumnName(table, key);
|
|
253
|
+
if (!isObjectLike(value))
|
|
254
|
+
return;
|
|
255
|
+
const column = (0, utils_js_1.ownLookup)(table.columnMap, key) ?? key;
|
|
256
|
+
// FAIL CLOSED. Under a relation, a key that is neither a known relation-value
|
|
257
|
+
// keyword nor a real column of the target, yet carries an object or array, is
|
|
258
|
+
// a shape this walker has never been taught. At clause scope the same key is
|
|
259
|
+
// rejected by the builder by name (E003, "unknown column"), which keeps a
|
|
260
|
+
// plain typo reading like a typo.
|
|
261
|
+
if (scope === 'relation' && !table.allColumns.includes(column))
|
|
262
|
+
host.refuseShape(table, key);
|
|
263
|
+
if (isPlainObject(value))
|
|
264
|
+
checkColumnRefs(table, value);
|
|
265
|
+
};
|
|
266
|
+
/** A where / orderBy / cursor object (or a Prisma-style array of them). */
|
|
267
|
+
const visitClause = (node, table, depth) => {
|
|
268
|
+
assertWithinDepth(depth);
|
|
269
|
+
if (!table || !isObjectLike(node))
|
|
270
|
+
return;
|
|
271
|
+
// `orderBy` accepts a Prisma-style array of single-key objects, and so does
|
|
272
|
+
// a `NOT` list. ITERATING that list carries no nesting, so a list of any
|
|
273
|
+
// WIDTH stays at one depth. NESTING it does: see arrayItemDepth.
|
|
274
|
+
if (Array.isArray(node)) {
|
|
275
|
+
for (const item of node)
|
|
276
|
+
visitClause(item, table, arrayItemDepth(item, depth));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
for (const [key, value] of Object.entries(node))
|
|
280
|
+
visitEntry(table, key, value, depth, 'clause');
|
|
281
|
+
};
|
|
282
|
+
/** The value sitting under a relation name, in a where OR an orderBy. */
|
|
283
|
+
const visitRelationValue = (value, target, depth) => {
|
|
284
|
+
assertWithinDepth(depth);
|
|
285
|
+
if (!target || !isObjectLike(value))
|
|
286
|
+
return;
|
|
287
|
+
if (Array.isArray(value)) {
|
|
288
|
+
for (const item of value)
|
|
289
|
+
visitRelationValue(item, target, arrayItemDepth(item, depth));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const node = value;
|
|
293
|
+
// Pick-row ordering: `{ rel: { pick: { orderBy, where }, by, … } }`. THREE
|
|
294
|
+
// column-naming positions, none of them a key of the relation value, which
|
|
295
|
+
// is why key-only walking never saw any of them: `pick.orderBy` and
|
|
296
|
+
// `pick.where` are clauses two levels down, and `by` names the target column
|
|
297
|
+
// whose value the parents are sorted by, in its VALUE.
|
|
298
|
+
if (isPickShape(node)) {
|
|
299
|
+
visitPick(node.pick, target, depth + 1);
|
|
300
|
+
visitPickBy(node.by, target);
|
|
301
|
+
// `direction` / `nulls` / `plan` are single tokens. A token position
|
|
302
|
+
// holding a structure is not a token, and is refused rather than assumed
|
|
303
|
+
// inert.
|
|
304
|
+
for (const key of ['direction', 'nulls', 'plan']) {
|
|
305
|
+
if (isObjectLike((0, utils_js_1.ownLookup)(node, key)))
|
|
306
|
+
host.refuseShape(target, key);
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (const [key, member] of Object.entries(node))
|
|
311
|
+
visitEntry(target, key, member, depth, 'relation');
|
|
312
|
+
};
|
|
313
|
+
/** `pick: { orderBy, where }`: both are clauses against the relation target. */
|
|
314
|
+
const visitPick = (value, target, depth) => {
|
|
315
|
+
assertWithinDepth(depth);
|
|
316
|
+
if (!isPlainObject(value))
|
|
317
|
+
return;
|
|
318
|
+
for (const [key, member] of Object.entries(value)) {
|
|
319
|
+
if (key === 'orderBy' || key === 'where') {
|
|
320
|
+
visitClause(member, target, depth);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (isObjectLike(member))
|
|
324
|
+
host.refuseShape(target, `pick.${key}`);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* `by`: the value read off the picked row. Either a bare target column name,
|
|
329
|
+
* or `{ field, path }` extracting a JSON path out of a json/jsonb target
|
|
330
|
+
* column. Both name a column in the VALUE position, which is why key-only
|
|
331
|
+
* checking never saw them.
|
|
332
|
+
*/
|
|
333
|
+
const visitPickBy = (value, target) => {
|
|
334
|
+
if (typeof value === 'string') {
|
|
335
|
+
checkColumnName(target, value);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (isPlainObject(value)) {
|
|
339
|
+
const field = value.field;
|
|
340
|
+
if (typeof field === 'string') {
|
|
341
|
+
checkColumnName(target, field);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
host.refuseShape(target, 'by');
|
|
345
|
+
}
|
|
346
|
+
// Any other `by` (number, null, array) names no column and the builder
|
|
347
|
+
// rejects the shape.
|
|
348
|
+
};
|
|
349
|
+
/** Field-name lists (`distinct`) name columns directly rather than in a clause. */
|
|
350
|
+
const visitFieldList = (value, table) => {
|
|
351
|
+
if (!Array.isArray(value))
|
|
352
|
+
return;
|
|
353
|
+
for (const field of value) {
|
|
354
|
+
if (typeof field === 'string')
|
|
355
|
+
checkColumnName(table, field);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const visitLevel = (level, table, depth) => {
|
|
359
|
+
assertWithinDepth(depth);
|
|
360
|
+
if (!table)
|
|
361
|
+
return;
|
|
362
|
+
visitClause(level.where, table, depth);
|
|
363
|
+
visitClause(level.orderBy, table, depth);
|
|
364
|
+
// `cursor` is a flat `{ field: value }` seek key that the builder turns into
|
|
365
|
+
// a WHERE range comparison against the sort key, so it reads exactly like a
|
|
366
|
+
// where on the same column.
|
|
367
|
+
visitClause(level.cursor, table, depth);
|
|
368
|
+
visitFieldList(level.distinct, table);
|
|
369
|
+
// FAIL CLOSED one level up: a query-level arg this walker does not know,
|
|
370
|
+
// carrying a structure, could name columns the same way `orderBy` does.
|
|
371
|
+
for (const [key, value] of Object.entries(level)) {
|
|
372
|
+
if (LEVEL_OBJECT_KEYS.has(key))
|
|
373
|
+
continue;
|
|
374
|
+
if (isObjectLike(value))
|
|
375
|
+
host.refuseShape(table, key);
|
|
376
|
+
}
|
|
377
|
+
const withClause = level.with;
|
|
378
|
+
if (!isPlainObject(withClause))
|
|
379
|
+
return;
|
|
380
|
+
for (const [relName, spec] of Object.entries(withClause)) {
|
|
381
|
+
const relation = (0, utils_js_1.ownLookup)(table.relations, relName);
|
|
382
|
+
// `_count` and an unknown relation name are not levels; the builder
|
|
383
|
+
// decides whether they are valid, and neither carries a column name.
|
|
384
|
+
if (!relation || spec === true || !isPlainObject(spec))
|
|
385
|
+
continue;
|
|
386
|
+
visitLevel(spec, (0, utils_js_1.ownLookup)(host.metadata.tables, relation.to), depth + 1);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
visitLevel(args, rootTable, 0);
|
|
390
|
+
}
|
|
@@ -62,13 +62,24 @@ function tableCandidates(modelName) {
|
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
// Field/column resolution
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The database column a Prisma field maps to (`@map` wins, else the field name).
|
|
67
|
+
*
|
|
68
|
+
* BOTH `@map("legacy_name")` and `@map(name: "legacy_name")` are valid Prisma
|
|
69
|
+
* and mean the same thing; the model-level `@@map` handler in prisma-schema.ts
|
|
70
|
+
* has always accepted both. Reading only the positional form here discarded the
|
|
71
|
+
* column name on every named-argument field, and the resolver then fell back to
|
|
72
|
+
* the Prisma FIELD name, which in a legacy database is frequently a real column
|
|
73
|
+
* of its own: the resolution reported CLEAN while every read returned another
|
|
74
|
+
* column's data, every write landed in the wrong column, and the Prisma-owned
|
|
75
|
+
* column was never touched again.
|
|
76
|
+
*/
|
|
66
77
|
function fieldColumn(model, fieldName) {
|
|
67
78
|
const f = model.fields.find((x) => x.name === fieldName);
|
|
68
79
|
if (!f)
|
|
69
80
|
return fieldName;
|
|
70
81
|
const mapAttr = f.attrs.find((a) => a.name === 'map');
|
|
71
|
-
const arg = mapAttr?.args.find((a) => a.key === undefined);
|
|
82
|
+
const arg = mapAttr?.args.find((a) => a.key === undefined || a.key === 'name');
|
|
72
83
|
return arg?.kind === 'string' && arg.value ? arg.value : f.name;
|
|
73
84
|
}
|
|
74
85
|
/** True when a field's type names a parsed model (so it is a relation/object field). */
|
|
@@ -171,9 +182,51 @@ function resolvePrismaSchema(ast, schema, options = {}) {
|
|
|
171
182
|
if (!noDb && r.status === 'resolved' && r.turbineName)
|
|
172
183
|
result.map.enums[en.name] = r.turbineName;
|
|
173
184
|
}
|
|
185
|
+
flagDuplicateTables(result);
|
|
174
186
|
result.hasUnresolved = computeHasUnresolved(result);
|
|
175
187
|
return result;
|
|
176
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Two Prisma models must not resolve to ONE table.
|
|
191
|
+
*
|
|
192
|
+
* Nothing downstream can survive it: `prisma-compat` builds its
|
|
193
|
+
* table-to-model index last-writer-wins, so nested relation reshaping silently
|
|
194
|
+
* picks whichever model happened to be declared last, and the two models'
|
|
195
|
+
* field maps are different. The two ways to get here are a `@@map` typo (two
|
|
196
|
+
* models pointing at the same table by accident) and a genuine multi-schema
|
|
197
|
+
* setup, where `@@schema` is what distinguishes `auth.users` from
|
|
198
|
+
* `public.users` and this parser does not represent it.
|
|
199
|
+
*
|
|
200
|
+
* Both models are marked UNRESOLVED and dropped from the map. Guessing between
|
|
201
|
+
* them is not available: the resolver has no evidence for which one the call
|
|
202
|
+
* site meant, and a wrong pick reads and writes another model's columns.
|
|
203
|
+
*/
|
|
204
|
+
function flagDuplicateTables(result) {
|
|
205
|
+
const byTable = new Map();
|
|
206
|
+
for (const m of result.models) {
|
|
207
|
+
if (m.status !== 'resolved' || !m.table)
|
|
208
|
+
continue;
|
|
209
|
+
const list = byTable.get(m.table);
|
|
210
|
+
if (list)
|
|
211
|
+
list.push(m);
|
|
212
|
+
else
|
|
213
|
+
byTable.set(m.table, [m]);
|
|
214
|
+
}
|
|
215
|
+
for (const [table, models] of byTable) {
|
|
216
|
+
if (models.length < 2)
|
|
217
|
+
continue;
|
|
218
|
+
const names = models.map((m) => m.prismaName).join(', ');
|
|
219
|
+
for (const m of models) {
|
|
220
|
+
m.status = 'unresolved';
|
|
221
|
+
m.reason =
|
|
222
|
+
`${models.length} models resolve to table "${table}" (${names}). ` +
|
|
223
|
+
`The compat layer indexes relation targets BY TABLE, so it cannot tell them apart. ` +
|
|
224
|
+
`Fix the @@map, or, if these models live in different Postgres schemas (@@schema), ` +
|
|
225
|
+
`run migrate-from-prisma once per schema: this parser resolves one namespace at a time.`;
|
|
226
|
+
delete result.map.models[m.prismaName];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
177
230
|
// ---------------------------------------------------------------------------
|
|
178
231
|
// Per-construct resolvers
|
|
179
232
|
// ---------------------------------------------------------------------------
|
|
@@ -336,8 +389,26 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
|
|
|
336
389
|
const fk = Array.isArray(def.foreignKey) ? def.foreignKey : [def.foreignKey];
|
|
337
390
|
return [...fk].sort().join(',') === want;
|
|
338
391
|
});
|
|
339
|
-
if (byFk.length
|
|
340
|
-
|
|
392
|
+
if (byFk.length === 0) {
|
|
393
|
+
// The schema PINNED the foreign-key columns and no relation in the
|
|
394
|
+
// database uses them. That is positive evidence of DISAGREEMENT, not a
|
|
395
|
+
// missing hint: falling through to "there is one candidate, take it" bound
|
|
396
|
+
// the field to an unrelated relation and reported the whole model clean.
|
|
397
|
+
// The shape is common: `relationMode = "prisma"` declares relations Prisma
|
|
398
|
+
// enforces in the client with no database constraint behind them, so a
|
|
399
|
+
// model can name FK columns that the catalog has never heard of, while a
|
|
400
|
+
// DIFFERENT column on the same table does carry a real FK. `include:
|
|
401
|
+
// { editor: true }` then returned the author.
|
|
402
|
+
return {
|
|
403
|
+
...base,
|
|
404
|
+
reason: `@relation(fields: [${fkColumns.join(', ')}]) names foreign-key column(s) that no relation on table ` +
|
|
405
|
+
`"${tableMeta.name}" uses. Candidates in the database: ` +
|
|
406
|
+
`${candidates.map((d) => `${d.name} (${Array.isArray(d.foreignKey) ? d.foreignKey.join('+') : d.foreignKey})`).join(', ') || '(none)'}. ` +
|
|
407
|
+
`If this relation is enforced only in Prisma (relationMode = "prisma"), the database has no foreign key ` +
|
|
408
|
+
`to resolve it against; add one, or map this field by hand.`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
picked = byFk;
|
|
341
412
|
}
|
|
342
413
|
if (picked.length === 1) {
|
|
343
414
|
const def = picked[0];
|
|
@@ -32,6 +32,16 @@ export interface PrismaAttrArg {
|
|
|
32
32
|
export interface PrismaAttr {
|
|
33
33
|
/** Attribute name without the leading `@`/`@@` (e.g. `map`, `relation`, `id`, `unique`). */
|
|
34
34
|
name: string;
|
|
35
|
+
/**
|
|
36
|
+
* The part after the dot in a namespaced attribute: `VarChar` for
|
|
37
|
+
* `@db.VarChar(320)`, `Text` for `@db.Text`.
|
|
38
|
+
*
|
|
39
|
+
* Kept because the head alone throws the native type away, and the arguments
|
|
40
|
+
* do not carry it back: `@db.VarChar(320)` retained only `320`, and
|
|
41
|
+
* `@db.Text` retained nothing at all, so `Text`, `Uuid`, `Money`, and
|
|
42
|
+
* `Citext` were indistinguishable from each other and from a bare `String`.
|
|
43
|
+
*/
|
|
44
|
+
nativeType?: string;
|
|
35
45
|
/** Parsed argument list (empty when the attribute took no parens). */
|
|
36
46
|
args: PrismaAttrArg[];
|
|
37
47
|
/** True for a block attribute (`@@name`), false for a field attribute (`@name`). */
|
|
@@ -43,8 +53,14 @@ export interface PrismaAttr {
|
|
|
43
53
|
export interface PrismaField {
|
|
44
54
|
/** Field name as declared (the Prisma API name). */
|
|
45
55
|
name: string;
|
|
46
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Base type with `[]` / `?` stripped (a scalar, enum, or model name).
|
|
58
|
+
* `Unsupported("...")` is normalized to the literal `Unsupported`, with the
|
|
59
|
+
* database type kept in {@link PrismaField.unsupported}.
|
|
60
|
+
*/
|
|
47
61
|
type: string;
|
|
62
|
+
/** The database type inside `Unsupported("...")`, when the field used that form. */
|
|
63
|
+
unsupported?: string;
|
|
48
64
|
/** Trailing `?` - the field is optional/nullable. */
|
|
49
65
|
optional: boolean;
|
|
50
66
|
/** Trailing `[]` - the field is a list. */
|