turbine-orm 0.63.0 → 0.64.1
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/dist/cjs/powql.d.ts +9 -0
- package/dist/cjs/powql.js +16 -2
- package/dist/cjs/query/relations.d.ts +42 -3
- package/dist/cjs/query/relations.js +88 -48
- package/dist/cjs/query/utils.d.ts +16 -0
- package/dist/cjs/query/utils.js +25 -0
- package/dist/powql.d.ts +9 -0
- package/dist/powql.js +17 -3
- package/dist/query/relations.d.ts +42 -3
- package/dist/query/relations.js +88 -49
- package/dist/query/utils.d.ts +16 -0
- package/dist/query/utils.js +24 -0
- package/package.json +1 -1
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -307,6 +307,15 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
307
307
|
* `includePii` is true; an explicit `select` naming a PII column IS the opt-in
|
|
308
308
|
* and returns it regardless. Untagged tables project exactly as before.
|
|
309
309
|
*/
|
|
310
|
+
/**
|
|
311
|
+
* Resolve one projection field name, or throw. PowQL already refused an
|
|
312
|
+
* unresolvable name here (via {@link column}), unlike the SQL engines' join
|
|
313
|
+
* path, which filtered it out silently until 0.64. What this adds is the
|
|
314
|
+
* RELATION case: naming a relation inside `select` is a Prisma habit rather
|
|
315
|
+
* than a typo, and the generic "unknown column" text sends the reader looking
|
|
316
|
+
* for a misspelling that is not there. Same message as the SQL engines.
|
|
317
|
+
*/
|
|
318
|
+
private projectionColumn;
|
|
310
319
|
private projectedColumns;
|
|
311
320
|
/**
|
|
312
321
|
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
package/dist/cjs/powql.js
CHANGED
|
@@ -908,6 +908,20 @@ class PowqlInterface {
|
|
|
908
908
|
* `includePii` is true; an explicit `select` naming a PII column IS the opt-in
|
|
909
909
|
* and returns it regardless. Untagged tables project exactly as before.
|
|
910
910
|
*/
|
|
911
|
+
/**
|
|
912
|
+
* Resolve one projection field name, or throw. PowQL already refused an
|
|
913
|
+
* unresolvable name here (via {@link column}), unlike the SQL engines' join
|
|
914
|
+
* path, which filtered it out silently until 0.64. What this adds is the
|
|
915
|
+
* RELATION case: naming a relation inside `select` is a Prisma habit rather
|
|
916
|
+
* than a typo, and the generic "unknown column" text sends the reader looking
|
|
917
|
+
* for a misspelling that is not there. Same message as the SQL engines.
|
|
918
|
+
*/
|
|
919
|
+
projectionColumn(field, clause) {
|
|
920
|
+
if ((0, utils_js_1.ownLookup)(this.meta.relations, field)) {
|
|
921
|
+
throw new errors_js_1.ValidationError((0, utils_js_1.relationInProjectionMessage)(this.table, field, clause));
|
|
922
|
+
}
|
|
923
|
+
return this.column(field).name;
|
|
924
|
+
}
|
|
911
925
|
projectedColumns(select, omit, includePii) {
|
|
912
926
|
const pk = new Set(this.meta.primaryKey);
|
|
913
927
|
let cols = this.meta.columns.map((c) => c.name);
|
|
@@ -915,7 +929,7 @@ class PowqlInterface {
|
|
|
915
929
|
if (hasSelect) {
|
|
916
930
|
const picked = new Set(Object.entries(select)
|
|
917
931
|
.filter(([, v]) => v)
|
|
918
|
-
.map(([k]) => this.
|
|
932
|
+
.map(([k]) => this.projectionColumn(k, 'select')));
|
|
919
933
|
// Always keep the PK so reselect / relation stitching has a key to work with.
|
|
920
934
|
for (const key of pk)
|
|
921
935
|
picked.add(key);
|
|
@@ -936,7 +950,7 @@ class PowqlInterface {
|
|
|
936
950
|
if (omit && Object.keys(omit).length) {
|
|
937
951
|
const dropped = new Set(Object.entries(omit)
|
|
938
952
|
.filter(([, v]) => v)
|
|
939
|
-
.map(([k]) => this.
|
|
953
|
+
.map(([k]) => this.projectionColumn(k, 'omit')));
|
|
940
954
|
// The PK survives `omit` too. This filter ran unconditionally and after
|
|
941
955
|
// the `select` branch's force-add, so `omit: { id: true }` undid the very
|
|
942
956
|
// guarantee that force-add exists to provide: the m2m loader keys its
|
|
@@ -34,8 +34,41 @@ export interface RelationShape {
|
|
|
34
34
|
cardinality: 'many' | 'one';
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Resolve select/omit
|
|
38
|
-
*
|
|
37
|
+
* Resolve `select` / `omit` into a list of snake_case column names, for the
|
|
38
|
+
* query's own table and for a relation target alike. `null` means "no
|
|
39
|
+
* projection", i.e. all columns, which keeps the `*` fast path.
|
|
40
|
+
*
|
|
41
|
+
* ## Why this is one function
|
|
42
|
+
*
|
|
43
|
+
* It used to be two, `resolveColumns` for the top level and
|
|
44
|
+
* `resolveTargetColumns` for a relation target, doing the same job against
|
|
45
|
+
* different metadata. They drifted, and the drift was invisible from either
|
|
46
|
+
* side: the top level resolved every name through a throwing lookup, while the
|
|
47
|
+
* relation side filtered unresolvable names out and emitted SQL for whatever
|
|
48
|
+
* survived. So the SAME key in the SAME query threw at the top and was silently
|
|
49
|
+
* ignored one level down, where `select: { titel: true }` returned `{}` rows
|
|
50
|
+
* and `omit: { titel: true }` returned the column it was asked to hide.
|
|
51
|
+
*
|
|
52
|
+
* It was worse than an inconsistency between depths. The batched loader runs
|
|
53
|
+
* each relation as a real query against the target table, so it went through
|
|
54
|
+
* the THROWING path, while the join plan went through the silent one. The two
|
|
55
|
+
* strategies therefore disagreed about whether the query was even valid, and
|
|
56
|
+
* under `relationLoadStrategy: 'auto'` which one runs is decided by a cost
|
|
57
|
+
* heuristic reading index coverage and table size. The same code threw on one
|
|
58
|
+
* table and quietly returned the wrong shape on another.
|
|
59
|
+
*
|
|
60
|
+
* Merging them is the fix that outlives this bug. Two functions that must agree
|
|
61
|
+
* are kept in step by whoever remembers; one function cannot disagree with
|
|
62
|
+
* itself. This mirrors `walkWhere` in where-compile.ts, which exists for the
|
|
63
|
+
* same reason after the top-level and relation-scoped WHERE walkers drifted
|
|
64
|
+
* twice, and it is why a new projection site is safe by default: PII exclusion,
|
|
65
|
+
* the `*` fast path and name resolution all live here, so reimplementing the
|
|
66
|
+
* name handling would mean reimplementing those too.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveProjection(qi: BuilderCtx, table: string, meta: TableMetadata, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
|
|
69
|
+
/**
|
|
70
|
+
* The query's own table. Thin wrapper over {@link resolveProjection} kept for
|
|
71
|
+
* the existing call sites in builder.ts.
|
|
39
72
|
*/
|
|
40
73
|
export declare function resolveColumns(qi: BuilderCtx, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
|
|
41
74
|
/**
|
|
@@ -254,8 +287,14 @@ export declare function parseNestedRow(qi: BuilderCtx, row: Record<string, unkno
|
|
|
254
287
|
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
255
288
|
* Shared by {@link buildRelationSubquery} (json order) and
|
|
256
289
|
* {@link buildRelationShape} (decode key order) so they can never diverge.
|
|
290
|
+
*
|
|
291
|
+
* A relation always projects SOMETHING, so the `null` that
|
|
292
|
+
* {@link resolveProjection} uses for the top level's `SELECT *` fast path
|
|
293
|
+
* becomes the target's full column list here. That is the only difference
|
|
294
|
+
* between the two, and it is why this is a four-line wrapper rather than a
|
|
295
|
+
* second implementation.
|
|
257
296
|
*/
|
|
258
|
-
export declare function resolveTargetColumns(qi: BuilderCtx, spec: true | WithOptions, targetMeta: TableMetadata, includePii?: boolean): string[];
|
|
297
|
+
export declare function resolveTargetColumns(qi: BuilderCtx, spec: true | WithOptions, targetMeta: TableMetadata, includePii?: boolean, targetTable?: string): string[];
|
|
259
298
|
/**
|
|
260
299
|
* Render a single relation row's JSON: a keyed object (`'object'`) or a
|
|
261
300
|
* positional array (`'positional'`). The array drops the keys but keeps the
|
|
@@ -47,6 +47,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
47
47
|
};
|
|
48
48
|
})();
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.resolveProjection = resolveProjection;
|
|
50
51
|
exports.resolveColumns = resolveColumns;
|
|
51
52
|
exports.withFingerprint = withFingerprint;
|
|
52
53
|
exports.collectWithParams = collectWithParams;
|
|
@@ -100,10 +101,58 @@ const warn_registry_js_1 = require("./warn-registry.js");
|
|
|
100
101
|
const whereMod = __importStar(require("./where.js"));
|
|
101
102
|
const writesMod = __importStar(require("./writes.js"));
|
|
102
103
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
104
|
+
* Turn ONE caller-supplied projection field name into a column, or throw.
|
|
105
|
+
*
|
|
106
|
+
* The whole point of this function is that it has no third outcome. The
|
|
107
|
+
* relation-side projection used to filter unresolvable names out
|
|
108
|
+
* (`.filter((col) => allColumns.includes(col))`) instead of rejecting them, and
|
|
109
|
+
* a filter that discards is exactly how a name typed by a human becomes SQL
|
|
110
|
+
* that no longer reflects what was asked for.
|
|
105
111
|
*/
|
|
106
|
-
function
|
|
112
|
+
function projectionColumn(table, meta, field, clause) {
|
|
113
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, field);
|
|
114
|
+
if (column)
|
|
115
|
+
return column;
|
|
116
|
+
// A relation named in a projection is a habit, not a typo, so it gets its own
|
|
117
|
+
// message pointing at `with`. Checked BEFORE the generic throw because the
|
|
118
|
+
// generic one degrades into "Did you mean <exactly what you typed>?".
|
|
119
|
+
if ((0, utils_js_1.ownLookup)(meta.relations, field))
|
|
120
|
+
throw new errors_js_1.ValidationError((0, utils_js_1.relationInProjectionMessage)(table, field, clause));
|
|
121
|
+
throw new errors_js_1.ValidationError((0, utils_js_1.unknownFieldMessage)(table, field, meta));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Resolve `select` / `omit` into a list of snake_case column names, for the
|
|
125
|
+
* query's own table and for a relation target alike. `null` means "no
|
|
126
|
+
* projection", i.e. all columns, which keeps the `*` fast path.
|
|
127
|
+
*
|
|
128
|
+
* ## Why this is one function
|
|
129
|
+
*
|
|
130
|
+
* It used to be two, `resolveColumns` for the top level and
|
|
131
|
+
* `resolveTargetColumns` for a relation target, doing the same job against
|
|
132
|
+
* different metadata. They drifted, and the drift was invisible from either
|
|
133
|
+
* side: the top level resolved every name through a throwing lookup, while the
|
|
134
|
+
* relation side filtered unresolvable names out and emitted SQL for whatever
|
|
135
|
+
* survived. So the SAME key in the SAME query threw at the top and was silently
|
|
136
|
+
* ignored one level down, where `select: { titel: true }` returned `{}` rows
|
|
137
|
+
* and `omit: { titel: true }` returned the column it was asked to hide.
|
|
138
|
+
*
|
|
139
|
+
* It was worse than an inconsistency between depths. The batched loader runs
|
|
140
|
+
* each relation as a real query against the target table, so it went through
|
|
141
|
+
* the THROWING path, while the join plan went through the silent one. The two
|
|
142
|
+
* strategies therefore disagreed about whether the query was even valid, and
|
|
143
|
+
* under `relationLoadStrategy: 'auto'` which one runs is decided by a cost
|
|
144
|
+
* heuristic reading index coverage and table size. The same code threw on one
|
|
145
|
+
* table and quietly returned the wrong shape on another.
|
|
146
|
+
*
|
|
147
|
+
* Merging them is the fix that outlives this bug. Two functions that must agree
|
|
148
|
+
* are kept in step by whoever remembers; one function cannot disagree with
|
|
149
|
+
* itself. This mirrors `walkWhere` in where-compile.ts, which exists for the
|
|
150
|
+
* same reason after the top-level and relation-scoped WHERE walkers drifted
|
|
151
|
+
* twice, and it is why a new projection site is safe by default: PII exclusion,
|
|
152
|
+
* the `*` fast path and name resolution all live here, so reimplementing the
|
|
153
|
+
* name handling would mean reimplementing those too.
|
|
154
|
+
*/
|
|
155
|
+
function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
107
156
|
if (select) {
|
|
108
157
|
// An array here means a caller wrote `select: ['id', 'name']` (Drizzle/SQL
|
|
109
158
|
// style) instead of the object shape. Object.entries() would iterate the
|
|
@@ -117,12 +166,12 @@ function resolveColumns(qi, select, omit, includePii) {
|
|
|
117
166
|
// PII column IS the opt-in: it comes back regardless of `includePii`.
|
|
118
167
|
return Object.entries(select)
|
|
119
168
|
.filter(([, v]) => v)
|
|
120
|
-
.map(([k]) =>
|
|
169
|
+
.map(([k]) => projectionColumn(table, meta, k, 'select'));
|
|
121
170
|
}
|
|
122
171
|
// Default / omit-only projection: PII-tagged columns are excluded unless the
|
|
123
172
|
// caller opted in with `includePii: UNSAFE`. An empty set (untagged schema) keeps the
|
|
124
173
|
// `null`/`*` fast path so the emitted SQL is byte-identical to before.
|
|
125
|
-
const piiCols = includePii ? undefined : writesMod.piiColumns(qi,
|
|
174
|
+
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, meta);
|
|
126
175
|
const hasPii = piiCols !== undefined && piiCols.size > 0;
|
|
127
176
|
if (omit) {
|
|
128
177
|
if (Array.isArray(omit)) {
|
|
@@ -131,14 +180,21 @@ function resolveColumns(qi, select, omit, includePii) {
|
|
|
131
180
|
// Include all columns except those where value is true (and PII columns).
|
|
132
181
|
const omitCols = new Set(Object.entries(omit)
|
|
133
182
|
.filter(([, v]) => v)
|
|
134
|
-
.map(([k]) =>
|
|
135
|
-
return
|
|
183
|
+
.map(([k]) => projectionColumn(table, meta, k, 'omit')));
|
|
184
|
+
return meta.allColumns.filter((col) => !omitCols.has(col) && !(hasPii && piiCols.has(col)));
|
|
136
185
|
}
|
|
137
186
|
if (hasPii) {
|
|
138
|
-
return
|
|
187
|
+
return meta.allColumns.filter((col) => !piiCols.has(col));
|
|
139
188
|
}
|
|
140
189
|
return null;
|
|
141
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The query's own table. Thin wrapper over {@link resolveProjection} kept for
|
|
193
|
+
* the existing call sites in builder.ts.
|
|
194
|
+
*/
|
|
195
|
+
function resolveColumns(qi, select, omit, includePii) {
|
|
196
|
+
return resolveProjection(qi, qi.table, qi.tableMeta, select, omit, includePii);
|
|
197
|
+
}
|
|
142
198
|
/**
|
|
143
199
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
144
200
|
* buildSelectWithRelations / buildRelationSubquery.
|
|
@@ -403,20 +459,17 @@ function orderByEntryFingerprint(qi, d, targetTable) {
|
|
|
403
459
|
return String(d);
|
|
404
460
|
}
|
|
405
461
|
function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
462
|
+
// There used to be a dev-only pre-scan here that printed `Unknown orderBy
|
|
463
|
+
// field "x" for table "y". This will cause a runtime error.` and then let
|
|
464
|
+
// compilation continue into the code below, which throws for the same key
|
|
465
|
+
// with a better message (it names the table, suggests the closest column and
|
|
466
|
+
// lists the valid relations). Every unknown-key shape was measured: plain
|
|
467
|
+
// direction, OrderBySpec, JSON path, both relation-shaped values, and array
|
|
468
|
+
// form. All six warn-and-then-throw; none reaches the end of this function.
|
|
469
|
+
// A warning whose entire content is a prediction of the exception on the next
|
|
470
|
+
// line is noise in dev logs and a second place to keep the key-resolution
|
|
471
|
+
// rules in step, so it is gone. See orderby-unknown-field.test.ts, which pins
|
|
472
|
+
// the refusal itself across that surface.
|
|
420
473
|
const meta = qi.schema.tables[qi.table];
|
|
421
474
|
let relOrdCounter = 0;
|
|
422
475
|
return (0, filters_js_1.orderByEntries)(orderBy)
|
|
@@ -1210,30 +1263,17 @@ function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1210
1263
|
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
1211
1264
|
* Shared by {@link buildRelationSubquery} (json order) and
|
|
1212
1265
|
* {@link buildRelationShape} (decode key order) so they can never diverge.
|
|
1266
|
+
*
|
|
1267
|
+
* A relation always projects SOMETHING, so the `null` that
|
|
1268
|
+
* {@link resolveProjection} uses for the top level's `SELECT *` fast path
|
|
1269
|
+
* becomes the target's full column list here. That is the only difference
|
|
1270
|
+
* between the two, and it is why this is a four-line wrapper rather than a
|
|
1271
|
+
* second implementation.
|
|
1213
1272
|
*/
|
|
1214
|
-
function resolveTargetColumns(qi, spec, targetMeta, includePii) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
const selectedFields = Object.entries(spec.select)
|
|
1219
|
-
.filter(([, v]) => v)
|
|
1220
|
-
.map(([k]) => (0, utils_js_1.ownLookup)(targetMeta.columnMap, k) ?? (0, schema_js_1.camelToSnake)(k));
|
|
1221
|
-
return selectedFields.filter((col) => targetMeta.allColumns.includes(col));
|
|
1222
|
-
}
|
|
1223
|
-
// Default / omit-only relation projection: PII columns are excluded unless
|
|
1224
|
-
// the query opted in via `includePii`.
|
|
1225
|
-
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, targetMeta);
|
|
1226
|
-
const hasPii = piiCols !== undefined && piiCols.size > 0;
|
|
1227
|
-
if (spec !== true && spec.omit) {
|
|
1228
|
-
const omittedFields = new Set(Object.entries(spec.omit)
|
|
1229
|
-
.filter(([, v]) => v)
|
|
1230
|
-
.map(([k]) => (0, utils_js_1.ownLookup)(targetMeta.columnMap, k) ?? (0, schema_js_1.camelToSnake)(k)));
|
|
1231
|
-
return targetMeta.allColumns.filter((col) => !omittedFields.has(col) && !(hasPii && piiCols.has(col)));
|
|
1232
|
-
}
|
|
1233
|
-
if (hasPii) {
|
|
1234
|
-
return targetMeta.allColumns.filter((col) => !piiCols.has(col));
|
|
1235
|
-
}
|
|
1236
|
-
return targetMeta.allColumns;
|
|
1273
|
+
function resolveTargetColumns(qi, spec, targetMeta, includePii, targetTable = targetMeta.name) {
|
|
1274
|
+
const select = spec === true ? undefined : spec.select;
|
|
1275
|
+
const omit = spec === true ? undefined : spec.omit;
|
|
1276
|
+
return resolveProjection(qi, targetTable, targetMeta, select, omit, includePii) ?? targetMeta.allColumns;
|
|
1237
1277
|
}
|
|
1238
1278
|
/**
|
|
1239
1279
|
* Render a single relation row's JSON: a keyed object (`'object'`) or a
|
|
@@ -1384,7 +1424,7 @@ function buildRelationShape(qi, relDef, spec, parentMeta, includePii) {
|
|
|
1384
1424
|
const targetMeta = qi.schema.tables[relDef.to];
|
|
1385
1425
|
if (!targetMeta)
|
|
1386
1426
|
return { keys: [], nested: {}, cardinality: 'many' };
|
|
1387
|
-
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii);
|
|
1427
|
+
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
1388
1428
|
const keys = targetColumns.map((col) => targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col));
|
|
1389
1429
|
const nested = {};
|
|
1390
1430
|
if (spec !== true && spec.with) {
|
|
@@ -1642,7 +1682,7 @@ function planFlattenNode(qi, counter, relName, relDef, spec, depth, path, includ
|
|
|
1642
1682
|
}
|
|
1643
1683
|
}
|
|
1644
1684
|
const alias = `${FLATTEN_ALIAS_PREFIX}${counter.n++}`;
|
|
1645
|
-
const cols = resolveTargetColumns(qi, spec, targetMeta, includePii);
|
|
1685
|
+
const cols = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
1646
1686
|
const discAlias = `${alias}__${FLATTEN_DISCRIMINATOR}`;
|
|
1647
1687
|
const node = {
|
|
1648
1688
|
relName,
|
|
@@ -2212,7 +2252,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2212
2252
|
// `includePii` opt-in). Shared with the positional-shape builder so the
|
|
2213
2253
|
// emitted json_build_array column order and the decode-side key order can
|
|
2214
2254
|
// never drift apart.
|
|
2215
|
-
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii);
|
|
2255
|
+
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
2216
2256
|
// Engine override seam (additive): a dialect whose JSON-aggregation shape does
|
|
2217
2257
|
// not map onto buildJsonObject/buildJsonArrayAgg (SQL Server FOR JSON PATH) owns
|
|
2218
2258
|
// the WHOLE subquery. Absent for PG/MySQL/SQLite → the native path below runs
|
|
@@ -451,3 +451,19 @@ export declare function unknownFieldMessage(table: string, field: string, meta:
|
|
|
451
451
|
columnMap: Record<string, string>;
|
|
452
452
|
relations?: Record<string, unknown>;
|
|
453
453
|
}): string;
|
|
454
|
+
/**
|
|
455
|
+
* The error text for a RELATION named inside `select` / `omit`.
|
|
456
|
+
*
|
|
457
|
+
* Separate from {@link unknownFieldMessage} because the generic text degrades
|
|
458
|
+
* into nonsense here: `closestName` matches an exactly-spelled relation name at
|
|
459
|
+
* distance zero, so the message would read `Unknown field "comments". Did you
|
|
460
|
+
* mean "comments" (a relation)?`, which answers a question nobody asked and
|
|
461
|
+
* hides the actual fix.
|
|
462
|
+
*
|
|
463
|
+
* It is worth its own message for a second reason: this is not really a typo,
|
|
464
|
+
* it is a habit. Prisma nests a relation inside `select`, so writing
|
|
465
|
+
* `select: { comments: true }` is the natural first guess, and in Turbine a
|
|
466
|
+
* relation is loaded by `with`, which sits BESIDE `select` rather than inside
|
|
467
|
+
* it. Naming the fix costs one sentence and saves a search.
|
|
468
|
+
*/
|
|
469
|
+
export declare function relationInProjectionMessage(table: string, field: string, clause: 'select' | 'omit'): string;
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -36,6 +36,7 @@ exports.coerceJsonWireValue = coerceJsonWireValue;
|
|
|
36
36
|
exports.closestName = closestName;
|
|
37
37
|
exports.suggestKey = suggestKey;
|
|
38
38
|
exports.unknownFieldMessage = unknownFieldMessage;
|
|
39
|
+
exports.relationInProjectionMessage = relationInProjectionMessage;
|
|
39
40
|
const pg_1 = __importDefault(require("pg"));
|
|
40
41
|
const schema_js_1 = require("../schema.js");
|
|
41
42
|
const warn_registry_js_1 = require("./warn-registry.js");
|
|
@@ -886,3 +887,27 @@ function unknownFieldMessage(table, field, meta) {
|
|
|
886
887
|
` Known columns: ${columns.join(', ') || '(none)'}.` +
|
|
887
888
|
(relations.length ? ` Known relations (valid in \`where\` and \`with\`): ${relations.join(', ')}.` : ''));
|
|
888
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* The error text for a RELATION named inside `select` / `omit`.
|
|
892
|
+
*
|
|
893
|
+
* Separate from {@link unknownFieldMessage} because the generic text degrades
|
|
894
|
+
* into nonsense here: `closestName` matches an exactly-spelled relation name at
|
|
895
|
+
* distance zero, so the message would read `Unknown field "comments". Did you
|
|
896
|
+
* mean "comments" (a relation)?`, which answers a question nobody asked and
|
|
897
|
+
* hides the actual fix.
|
|
898
|
+
*
|
|
899
|
+
* It is worth its own message for a second reason: this is not really a typo,
|
|
900
|
+
* it is a habit. Prisma nests a relation inside `select`, so writing
|
|
901
|
+
* `select: { comments: true }` is the natural first guess, and in Turbine a
|
|
902
|
+
* relation is loaded by `with`, which sits BESIDE `select` rather than inside
|
|
903
|
+
* it. Naming the fix costs one sentence and saves a search.
|
|
904
|
+
*/
|
|
905
|
+
function relationInProjectionMessage(table, field, clause) {
|
|
906
|
+
const head = `[turbine] "${field}" is a relation on table "${table}", not a column, so it cannot be named in \`${clause}\`.`;
|
|
907
|
+
return clause === 'select'
|
|
908
|
+
? `${head} Load it with \`with: { ${field}: true }\`, which is a sibling of \`select\`, not a member of it.` +
|
|
909
|
+
" To narrow the relation's own columns, put a `select` inside that relation's options:" +
|
|
910
|
+
` \`with: { ${field}: { select: { … } } }\`.`
|
|
911
|
+
: `${head} A relation is only present when you ask for it in \`with\`, so leave it out of \`with\` to leave it` +
|
|
912
|
+
' out of the result.';
|
|
913
|
+
}
|
package/dist/powql.d.ts
CHANGED
|
@@ -307,6 +307,15 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
307
307
|
* `includePii` is true; an explicit `select` naming a PII column IS the opt-in
|
|
308
308
|
* and returns it regardless. Untagged tables project exactly as before.
|
|
309
309
|
*/
|
|
310
|
+
/**
|
|
311
|
+
* Resolve one projection field name, or throw. PowQL already refused an
|
|
312
|
+
* unresolvable name here (via {@link column}), unlike the SQL engines' join
|
|
313
|
+
* path, which filtered it out silently until 0.64. What this adds is the
|
|
314
|
+
* RELATION case: naming a relation inside `select` is a Prisma habit rather
|
|
315
|
+
* than a typo, and the generic "unknown column" text sends the reader looking
|
|
316
|
+
* for a misspelling that is not there. Same message as the SQL engines.
|
|
317
|
+
*/
|
|
318
|
+
private projectionColumn;
|
|
310
319
|
private projectedColumns;
|
|
311
320
|
/**
|
|
312
321
|
* The snake_case names of this table's PII-tagged columns. Empty for a table
|
package/dist/powql.js
CHANGED
|
@@ -45,7 +45,7 @@ import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/fil
|
|
|
45
45
|
// are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
|
|
46
46
|
// engines, so a spread request body cannot turn either on here either.
|
|
47
47
|
import { assertDirectionToken, resolveUnsafeFlag, UNSAFE } from './query/types.js';
|
|
48
|
-
import { escapeLike } from './query/utils.js';
|
|
48
|
+
import { escapeLike, ownLookup, relationInProjectionMessage } from './query/utils.js';
|
|
49
49
|
import { assertJsonFilterKeys, jsonStringEntries } from './query/where.js';
|
|
50
50
|
import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
51
51
|
/**
|
|
@@ -872,6 +872,20 @@ export class PowqlInterface {
|
|
|
872
872
|
* `includePii` is true; an explicit `select` naming a PII column IS the opt-in
|
|
873
873
|
* and returns it regardless. Untagged tables project exactly as before.
|
|
874
874
|
*/
|
|
875
|
+
/**
|
|
876
|
+
* Resolve one projection field name, or throw. PowQL already refused an
|
|
877
|
+
* unresolvable name here (via {@link column}), unlike the SQL engines' join
|
|
878
|
+
* path, which filtered it out silently until 0.64. What this adds is the
|
|
879
|
+
* RELATION case: naming a relation inside `select` is a Prisma habit rather
|
|
880
|
+
* than a typo, and the generic "unknown column" text sends the reader looking
|
|
881
|
+
* for a misspelling that is not there. Same message as the SQL engines.
|
|
882
|
+
*/
|
|
883
|
+
projectionColumn(field, clause) {
|
|
884
|
+
if (ownLookup(this.meta.relations, field)) {
|
|
885
|
+
throw new ValidationError(relationInProjectionMessage(this.table, field, clause));
|
|
886
|
+
}
|
|
887
|
+
return this.column(field).name;
|
|
888
|
+
}
|
|
875
889
|
projectedColumns(select, omit, includePii) {
|
|
876
890
|
const pk = new Set(this.meta.primaryKey);
|
|
877
891
|
let cols = this.meta.columns.map((c) => c.name);
|
|
@@ -879,7 +893,7 @@ export class PowqlInterface {
|
|
|
879
893
|
if (hasSelect) {
|
|
880
894
|
const picked = new Set(Object.entries(select)
|
|
881
895
|
.filter(([, v]) => v)
|
|
882
|
-
.map(([k]) => this.
|
|
896
|
+
.map(([k]) => this.projectionColumn(k, 'select')));
|
|
883
897
|
// Always keep the PK so reselect / relation stitching has a key to work with.
|
|
884
898
|
for (const key of pk)
|
|
885
899
|
picked.add(key);
|
|
@@ -900,7 +914,7 @@ export class PowqlInterface {
|
|
|
900
914
|
if (omit && Object.keys(omit).length) {
|
|
901
915
|
const dropped = new Set(Object.entries(omit)
|
|
902
916
|
.filter(([, v]) => v)
|
|
903
|
-
.map(([k]) => this.
|
|
917
|
+
.map(([k]) => this.projectionColumn(k, 'omit')));
|
|
904
918
|
// The PK survives `omit` too. This filter ran unconditionally and after
|
|
905
919
|
// the `select` branch's force-add, so `omit: { id: true }` undid the very
|
|
906
920
|
// guarantee that force-add exists to provide: the m2m loader keys its
|
|
@@ -34,8 +34,41 @@ export interface RelationShape {
|
|
|
34
34
|
cardinality: 'many' | 'one';
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Resolve select/omit
|
|
38
|
-
*
|
|
37
|
+
* Resolve `select` / `omit` into a list of snake_case column names, for the
|
|
38
|
+
* query's own table and for a relation target alike. `null` means "no
|
|
39
|
+
* projection", i.e. all columns, which keeps the `*` fast path.
|
|
40
|
+
*
|
|
41
|
+
* ## Why this is one function
|
|
42
|
+
*
|
|
43
|
+
* It used to be two, `resolveColumns` for the top level and
|
|
44
|
+
* `resolveTargetColumns` for a relation target, doing the same job against
|
|
45
|
+
* different metadata. They drifted, and the drift was invisible from either
|
|
46
|
+
* side: the top level resolved every name through a throwing lookup, while the
|
|
47
|
+
* relation side filtered unresolvable names out and emitted SQL for whatever
|
|
48
|
+
* survived. So the SAME key in the SAME query threw at the top and was silently
|
|
49
|
+
* ignored one level down, where `select: { titel: true }` returned `{}` rows
|
|
50
|
+
* and `omit: { titel: true }` returned the column it was asked to hide.
|
|
51
|
+
*
|
|
52
|
+
* It was worse than an inconsistency between depths. The batched loader runs
|
|
53
|
+
* each relation as a real query against the target table, so it went through
|
|
54
|
+
* the THROWING path, while the join plan went through the silent one. The two
|
|
55
|
+
* strategies therefore disagreed about whether the query was even valid, and
|
|
56
|
+
* under `relationLoadStrategy: 'auto'` which one runs is decided by a cost
|
|
57
|
+
* heuristic reading index coverage and table size. The same code threw on one
|
|
58
|
+
* table and quietly returned the wrong shape on another.
|
|
59
|
+
*
|
|
60
|
+
* Merging them is the fix that outlives this bug. Two functions that must agree
|
|
61
|
+
* are kept in step by whoever remembers; one function cannot disagree with
|
|
62
|
+
* itself. This mirrors `walkWhere` in where-compile.ts, which exists for the
|
|
63
|
+
* same reason after the top-level and relation-scoped WHERE walkers drifted
|
|
64
|
+
* twice, and it is why a new projection site is safe by default: PII exclusion,
|
|
65
|
+
* the `*` fast path and name resolution all live here, so reimplementing the
|
|
66
|
+
* name handling would mean reimplementing those too.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveProjection(qi: BuilderCtx, table: string, meta: TableMetadata, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
|
|
69
|
+
/**
|
|
70
|
+
* The query's own table. Thin wrapper over {@link resolveProjection} kept for
|
|
71
|
+
* the existing call sites in builder.ts.
|
|
39
72
|
*/
|
|
40
73
|
export declare function resolveColumns(qi: BuilderCtx, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
|
|
41
74
|
/**
|
|
@@ -254,8 +287,14 @@ export declare function parseNestedRow(qi: BuilderCtx, row: Record<string, unkno
|
|
|
254
287
|
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
255
288
|
* Shared by {@link buildRelationSubquery} (json order) and
|
|
256
289
|
* {@link buildRelationShape} (decode key order) so they can never diverge.
|
|
290
|
+
*
|
|
291
|
+
* A relation always projects SOMETHING, so the `null` that
|
|
292
|
+
* {@link resolveProjection} uses for the top level's `SELECT *` fast path
|
|
293
|
+
* becomes the target's full column list here. That is the only difference
|
|
294
|
+
* between the two, and it is why this is a four-line wrapper rather than a
|
|
295
|
+
* second implementation.
|
|
257
296
|
*/
|
|
258
|
-
export declare function resolveTargetColumns(qi: BuilderCtx, spec: true | WithOptions, targetMeta: TableMetadata, includePii?: boolean): string[];
|
|
297
|
+
export declare function resolveTargetColumns(qi: BuilderCtx, spec: true | WithOptions, targetMeta: TableMetadata, includePii?: boolean, targetTable?: string): string[];
|
|
259
298
|
/**
|
|
260
299
|
* Render a single relation row's JSON: a keyed object (`'object'`) or a
|
|
261
300
|
* positional array (`'positional'`). The array drops the keys but keeps the
|
package/dist/query/relations.js
CHANGED
|
@@ -18,15 +18,63 @@ import { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
|
18
18
|
import { resolveCountRelations } from './batched-loader.js';
|
|
19
19
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
20
20
|
import { assertDirectionToken, assertOrderDirection } from './types.js';
|
|
21
|
-
import { ownLookup } from './utils.js';
|
|
21
|
+
import { ownLookup, relationInProjectionMessage, resolveColumnName, unknownFieldMessage } from './utils.js';
|
|
22
22
|
import { hasWarnedOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
23
23
|
import * as whereMod from './where.js';
|
|
24
24
|
import * as writesMod from './writes.js';
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Turn ONE caller-supplied projection field name into a column, or throw.
|
|
27
|
+
*
|
|
28
|
+
* The whole point of this function is that it has no third outcome. The
|
|
29
|
+
* relation-side projection used to filter unresolvable names out
|
|
30
|
+
* (`.filter((col) => allColumns.includes(col))`) instead of rejecting them, and
|
|
31
|
+
* a filter that discards is exactly how a name typed by a human becomes SQL
|
|
32
|
+
* that no longer reflects what was asked for.
|
|
28
33
|
*/
|
|
29
|
-
|
|
34
|
+
function projectionColumn(table, meta, field, clause) {
|
|
35
|
+
const column = resolveColumnName(meta, field);
|
|
36
|
+
if (column)
|
|
37
|
+
return column;
|
|
38
|
+
// A relation named in a projection is a habit, not a typo, so it gets its own
|
|
39
|
+
// message pointing at `with`. Checked BEFORE the generic throw because the
|
|
40
|
+
// generic one degrades into "Did you mean <exactly what you typed>?".
|
|
41
|
+
if (ownLookup(meta.relations, field))
|
|
42
|
+
throw new ValidationError(relationInProjectionMessage(table, field, clause));
|
|
43
|
+
throw new ValidationError(unknownFieldMessage(table, field, meta));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve `select` / `omit` into a list of snake_case column names, for the
|
|
47
|
+
* query's own table and for a relation target alike. `null` means "no
|
|
48
|
+
* projection", i.e. all columns, which keeps the `*` fast path.
|
|
49
|
+
*
|
|
50
|
+
* ## Why this is one function
|
|
51
|
+
*
|
|
52
|
+
* It used to be two, `resolveColumns` for the top level and
|
|
53
|
+
* `resolveTargetColumns` for a relation target, doing the same job against
|
|
54
|
+
* different metadata. They drifted, and the drift was invisible from either
|
|
55
|
+
* side: the top level resolved every name through a throwing lookup, while the
|
|
56
|
+
* relation side filtered unresolvable names out and emitted SQL for whatever
|
|
57
|
+
* survived. So the SAME key in the SAME query threw at the top and was silently
|
|
58
|
+
* ignored one level down, where `select: { titel: true }` returned `{}` rows
|
|
59
|
+
* and `omit: { titel: true }` returned the column it was asked to hide.
|
|
60
|
+
*
|
|
61
|
+
* It was worse than an inconsistency between depths. The batched loader runs
|
|
62
|
+
* each relation as a real query against the target table, so it went through
|
|
63
|
+
* the THROWING path, while the join plan went through the silent one. The two
|
|
64
|
+
* strategies therefore disagreed about whether the query was even valid, and
|
|
65
|
+
* under `relationLoadStrategy: 'auto'` which one runs is decided by a cost
|
|
66
|
+
* heuristic reading index coverage and table size. The same code threw on one
|
|
67
|
+
* table and quietly returned the wrong shape on another.
|
|
68
|
+
*
|
|
69
|
+
* Merging them is the fix that outlives this bug. Two functions that must agree
|
|
70
|
+
* are kept in step by whoever remembers; one function cannot disagree with
|
|
71
|
+
* itself. This mirrors `walkWhere` in where-compile.ts, which exists for the
|
|
72
|
+
* same reason after the top-level and relation-scoped WHERE walkers drifted
|
|
73
|
+
* twice, and it is why a new projection site is safe by default: PII exclusion,
|
|
74
|
+
* the `*` fast path and name resolution all live here, so reimplementing the
|
|
75
|
+
* name handling would mean reimplementing those too.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
30
78
|
if (select) {
|
|
31
79
|
// An array here means a caller wrote `select: ['id', 'name']` (Drizzle/SQL
|
|
32
80
|
// style) instead of the object shape. Object.entries() would iterate the
|
|
@@ -40,12 +88,12 @@ export function resolveColumns(qi, select, omit, includePii) {
|
|
|
40
88
|
// PII column IS the opt-in: it comes back regardless of `includePii`.
|
|
41
89
|
return Object.entries(select)
|
|
42
90
|
.filter(([, v]) => v)
|
|
43
|
-
.map(([k]) =>
|
|
91
|
+
.map(([k]) => projectionColumn(table, meta, k, 'select'));
|
|
44
92
|
}
|
|
45
93
|
// Default / omit-only projection: PII-tagged columns are excluded unless the
|
|
46
94
|
// caller opted in with `includePii: UNSAFE`. An empty set (untagged schema) keeps the
|
|
47
95
|
// `null`/`*` fast path so the emitted SQL is byte-identical to before.
|
|
48
|
-
const piiCols = includePii ? undefined : writesMod.piiColumns(qi,
|
|
96
|
+
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, meta);
|
|
49
97
|
const hasPii = piiCols !== undefined && piiCols.size > 0;
|
|
50
98
|
if (omit) {
|
|
51
99
|
if (Array.isArray(omit)) {
|
|
@@ -54,14 +102,21 @@ export function resolveColumns(qi, select, omit, includePii) {
|
|
|
54
102
|
// Include all columns except those where value is true (and PII columns).
|
|
55
103
|
const omitCols = new Set(Object.entries(omit)
|
|
56
104
|
.filter(([, v]) => v)
|
|
57
|
-
.map(([k]) =>
|
|
58
|
-
return
|
|
105
|
+
.map(([k]) => projectionColumn(table, meta, k, 'omit')));
|
|
106
|
+
return meta.allColumns.filter((col) => !omitCols.has(col) && !(hasPii && piiCols.has(col)));
|
|
59
107
|
}
|
|
60
108
|
if (hasPii) {
|
|
61
|
-
return
|
|
109
|
+
return meta.allColumns.filter((col) => !piiCols.has(col));
|
|
62
110
|
}
|
|
63
111
|
return null;
|
|
64
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* The query's own table. Thin wrapper over {@link resolveProjection} kept for
|
|
115
|
+
* the existing call sites in builder.ts.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveColumns(qi, select, omit, includePii) {
|
|
118
|
+
return resolveProjection(qi, qi.table, qi.tableMeta, select, omit, includePii);
|
|
119
|
+
}
|
|
65
120
|
/**
|
|
66
121
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
67
122
|
* buildSelectWithRelations / buildRelationSubquery.
|
|
@@ -326,20 +381,17 @@ export function orderByEntryFingerprint(qi, d, targetTable) {
|
|
|
326
381
|
return String(d);
|
|
327
382
|
}
|
|
328
383
|
export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
384
|
+
// There used to be a dev-only pre-scan here that printed `Unknown orderBy
|
|
385
|
+
// field "x" for table "y". This will cause a runtime error.` and then let
|
|
386
|
+
// compilation continue into the code below, which throws for the same key
|
|
387
|
+
// with a better message (it names the table, suggests the closest column and
|
|
388
|
+
// lists the valid relations). Every unknown-key shape was measured: plain
|
|
389
|
+
// direction, OrderBySpec, JSON path, both relation-shaped values, and array
|
|
390
|
+
// form. All six warn-and-then-throw; none reaches the end of this function.
|
|
391
|
+
// A warning whose entire content is a prediction of the exception on the next
|
|
392
|
+
// line is noise in dev logs and a second place to keep the key-resolution
|
|
393
|
+
// rules in step, so it is gone. See orderby-unknown-field.test.ts, which pins
|
|
394
|
+
// the refusal itself across that surface.
|
|
343
395
|
const meta = qi.schema.tables[qi.table];
|
|
344
396
|
let relOrdCounter = 0;
|
|
345
397
|
return orderByEntries(orderBy)
|
|
@@ -1133,30 +1185,17 @@ export function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1133
1185
|
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
1134
1186
|
* Shared by {@link buildRelationSubquery} (json order) and
|
|
1135
1187
|
* {@link buildRelationShape} (decode key order) so they can never diverge.
|
|
1188
|
+
*
|
|
1189
|
+
* A relation always projects SOMETHING, so the `null` that
|
|
1190
|
+
* {@link resolveProjection} uses for the top level's `SELECT *` fast path
|
|
1191
|
+
* becomes the target's full column list here. That is the only difference
|
|
1192
|
+
* between the two, and it is why this is a four-line wrapper rather than a
|
|
1193
|
+
* second implementation.
|
|
1136
1194
|
*/
|
|
1137
|
-
export function resolveTargetColumns(qi, spec, targetMeta, includePii) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const selectedFields = Object.entries(spec.select)
|
|
1142
|
-
.filter(([, v]) => v)
|
|
1143
|
-
.map(([k]) => ownLookup(targetMeta.columnMap, k) ?? camelToSnake(k));
|
|
1144
|
-
return selectedFields.filter((col) => targetMeta.allColumns.includes(col));
|
|
1145
|
-
}
|
|
1146
|
-
// Default / omit-only relation projection: PII columns are excluded unless
|
|
1147
|
-
// the query opted in via `includePii`.
|
|
1148
|
-
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, targetMeta);
|
|
1149
|
-
const hasPii = piiCols !== undefined && piiCols.size > 0;
|
|
1150
|
-
if (spec !== true && spec.omit) {
|
|
1151
|
-
const omittedFields = new Set(Object.entries(spec.omit)
|
|
1152
|
-
.filter(([, v]) => v)
|
|
1153
|
-
.map(([k]) => ownLookup(targetMeta.columnMap, k) ?? camelToSnake(k)));
|
|
1154
|
-
return targetMeta.allColumns.filter((col) => !omittedFields.has(col) && !(hasPii && piiCols.has(col)));
|
|
1155
|
-
}
|
|
1156
|
-
if (hasPii) {
|
|
1157
|
-
return targetMeta.allColumns.filter((col) => !piiCols.has(col));
|
|
1158
|
-
}
|
|
1159
|
-
return targetMeta.allColumns;
|
|
1195
|
+
export function resolveTargetColumns(qi, spec, targetMeta, includePii, targetTable = targetMeta.name) {
|
|
1196
|
+
const select = spec === true ? undefined : spec.select;
|
|
1197
|
+
const omit = spec === true ? undefined : spec.omit;
|
|
1198
|
+
return resolveProjection(qi, targetTable, targetMeta, select, omit, includePii) ?? targetMeta.allColumns;
|
|
1160
1199
|
}
|
|
1161
1200
|
/**
|
|
1162
1201
|
* Render a single relation row's JSON: a keyed object (`'object'`) or a
|
|
@@ -1307,7 +1346,7 @@ export function buildRelationShape(qi, relDef, spec, parentMeta, includePii) {
|
|
|
1307
1346
|
const targetMeta = qi.schema.tables[relDef.to];
|
|
1308
1347
|
if (!targetMeta)
|
|
1309
1348
|
return { keys: [], nested: {}, cardinality: 'many' };
|
|
1310
|
-
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii);
|
|
1349
|
+
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
1311
1350
|
const keys = targetColumns.map((col) => targetMeta.reverseColumnMap[col] ?? snakeToCamel(col));
|
|
1312
1351
|
const nested = {};
|
|
1313
1352
|
if (spec !== true && spec.with) {
|
|
@@ -1565,7 +1604,7 @@ function planFlattenNode(qi, counter, relName, relDef, spec, depth, path, includ
|
|
|
1565
1604
|
}
|
|
1566
1605
|
}
|
|
1567
1606
|
const alias = `${FLATTEN_ALIAS_PREFIX}${counter.n++}`;
|
|
1568
|
-
const cols = resolveTargetColumns(qi, spec, targetMeta, includePii);
|
|
1607
|
+
const cols = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
1569
1608
|
const discAlias = `${alias}__${FLATTEN_DISCRIMINATOR}`;
|
|
1570
1609
|
const node = {
|
|
1571
1610
|
relName,
|
|
@@ -2135,7 +2174,7 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2135
2174
|
// `includePii` opt-in). Shared with the positional-shape builder so the
|
|
2136
2175
|
// emitted json_build_array column order and the decode-side key order can
|
|
2137
2176
|
// never drift apart.
|
|
2138
|
-
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii);
|
|
2177
|
+
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
2139
2178
|
// Engine override seam (additive): a dialect whose JSON-aggregation shape does
|
|
2140
2179
|
// not map onto buildJsonObject/buildJsonArrayAgg (SQL Server FOR JSON PATH) owns
|
|
2141
2180
|
// the WHOLE subquery. Absent for PG/MySQL/SQLite → the native path below runs
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -451,3 +451,19 @@ export declare function unknownFieldMessage(table: string, field: string, meta:
|
|
|
451
451
|
columnMap: Record<string, string>;
|
|
452
452
|
relations?: Record<string, unknown>;
|
|
453
453
|
}): string;
|
|
454
|
+
/**
|
|
455
|
+
* The error text for a RELATION named inside `select` / `omit`.
|
|
456
|
+
*
|
|
457
|
+
* Separate from {@link unknownFieldMessage} because the generic text degrades
|
|
458
|
+
* into nonsense here: `closestName` matches an exactly-spelled relation name at
|
|
459
|
+
* distance zero, so the message would read `Unknown field "comments". Did you
|
|
460
|
+
* mean "comments" (a relation)?`, which answers a question nobody asked and
|
|
461
|
+
* hides the actual fix.
|
|
462
|
+
*
|
|
463
|
+
* It is worth its own message for a second reason: this is not really a typo,
|
|
464
|
+
* it is a habit. Prisma nests a relation inside `select`, so writing
|
|
465
|
+
* `select: { comments: true }` is the natural first guess, and in Turbine a
|
|
466
|
+
* relation is loaded by `with`, which sits BESIDE `select` rather than inside
|
|
467
|
+
* it. Naming the fix costs one sentence and saves a search.
|
|
468
|
+
*/
|
|
469
|
+
export declare function relationInProjectionMessage(table: string, field: string, clause: 'select' | 'omit'): string;
|
package/dist/query/utils.js
CHANGED
|
@@ -852,3 +852,27 @@ export function unknownFieldMessage(table, field, meta) {
|
|
|
852
852
|
` Known columns: ${columns.join(', ') || '(none)'}.` +
|
|
853
853
|
(relations.length ? ` Known relations (valid in \`where\` and \`with\`): ${relations.join(', ')}.` : ''));
|
|
854
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* The error text for a RELATION named inside `select` / `omit`.
|
|
857
|
+
*
|
|
858
|
+
* Separate from {@link unknownFieldMessage} because the generic text degrades
|
|
859
|
+
* into nonsense here: `closestName` matches an exactly-spelled relation name at
|
|
860
|
+
* distance zero, so the message would read `Unknown field "comments". Did you
|
|
861
|
+
* mean "comments" (a relation)?`, which answers a question nobody asked and
|
|
862
|
+
* hides the actual fix.
|
|
863
|
+
*
|
|
864
|
+
* It is worth its own message for a second reason: this is not really a typo,
|
|
865
|
+
* it is a habit. Prisma nests a relation inside `select`, so writing
|
|
866
|
+
* `select: { comments: true }` is the natural first guess, and in Turbine a
|
|
867
|
+
* relation is loaded by `with`, which sits BESIDE `select` rather than inside
|
|
868
|
+
* it. Naming the fix costs one sentence and saves a search.
|
|
869
|
+
*/
|
|
870
|
+
export function relationInProjectionMessage(table, field, clause) {
|
|
871
|
+
const head = `[turbine] "${field}" is a relation on table "${table}", not a column, so it cannot be named in \`${clause}\`.`;
|
|
872
|
+
return clause === 'select'
|
|
873
|
+
? `${head} Load it with \`with: { ${field}: true }\`, which is a sibling of \`select\`, not a member of it.` +
|
|
874
|
+
" To narrow the relation's own columns, put a `select` inside that relation's options:" +
|
|
875
|
+
` \`with: { ${field}: { select: { … } } }\`.`
|
|
876
|
+
: `${head} A relation is only present when you ask for it in \`with\`, so leave it out of \`with\` to leave it` +
|
|
877
|
+
' out of the result.';
|
|
878
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.1",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|