turbine-orm 0.76.0 → 0.77.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/dist/cjs/cli/index.js +2 -2
- package/dist/cjs/cli/migrate.js +8 -8
- package/dist/cjs/cli/studio.js +6 -3
- package/dist/cjs/client.js +10 -10
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/errors.d.ts +2 -2
- package/dist/cjs/errors.js +21 -25
- package/dist/cjs/generate.js +1 -1
- package/dist/cjs/introspect.js +5 -5
- package/dist/cjs/mssql.js +12 -12
- package/dist/cjs/mysql.js +5 -5
- package/dist/cjs/nested-write.js +31 -31
- package/dist/cjs/observe.js +1 -1
- package/dist/cjs/pipeline-submittable.js +5 -1
- package/dist/cjs/pipeline.js +8 -0
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +4 -4
- package/dist/cjs/powdb-shared.d.ts +348 -0
- package/dist/cjs/powdb-shared.js +587 -0
- package/dist/cjs/powdb.d.ts +12 -299
- package/dist/cjs/powdb.js +106 -567
- package/dist/cjs/powql.d.ts +1 -1
- package/dist/cjs/powql.js +89 -89
- package/dist/cjs/prisma-compat.js +16 -16
- package/dist/cjs/query/aggregates.js +19 -20
- package/dist/cjs/query/batched-loader.js +8 -8
- package/dist/cjs/query/builder.js +8 -8
- package/dist/cjs/query/compound-unique.js +2 -2
- package/dist/cjs/query/filters.js +1 -1
- package/dist/cjs/query/relations.js +60 -48
- package/dist/cjs/query/types.js +4 -4
- package/dist/cjs/query/utils.js +5 -5
- package/dist/cjs/query/warn-registry.d.ts +7 -0
- package/dist/cjs/query/warn-registry.js +7 -0
- package/dist/cjs/query/where-compile.js +1 -1
- package/dist/cjs/query/where.js +30 -31
- package/dist/cjs/query/writes.js +7 -7
- package/dist/cjs/realtime.js +4 -4
- package/dist/cjs/schema-metadata.js +1 -1
- package/dist/cjs/schema-sql.js +3 -3
- package/dist/cjs/seed.js +1 -1
- package/dist/cjs/sqlite.js +7 -7
- package/dist/cjs/typed-sql.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/migrate.js +8 -8
- package/dist/cli/studio.js +6 -3
- package/dist/client.js +10 -10
- package/dist/dialect.js +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +21 -25
- package/dist/generate.js +1 -1
- package/dist/introspect.js +5 -5
- package/dist/mssql.js +12 -12
- package/dist/mysql.js +5 -5
- package/dist/nested-write.js +31 -31
- package/dist/observe.js +1 -1
- package/dist/pipeline-submittable.js +6 -2
- package/dist/pipeline.js +9 -1
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +2 -2
- package/dist/powdb-shared.d.ts +348 -0
- package/dist/powdb-shared.js +570 -0
- package/dist/powdb.d.ts +12 -299
- package/dist/powdb.js +71 -534
- package/dist/powql.d.ts +1 -1
- package/dist/powql.js +43 -43
- package/dist/prisma-compat.js +16 -16
- package/dist/query/aggregates.js +19 -20
- package/dist/query/batched-loader.js +8 -8
- package/dist/query/builder.js +8 -8
- package/dist/query/compound-unique.js +2 -2
- package/dist/query/filters.js +1 -1
- package/dist/query/relations.js +60 -48
- package/dist/query/types.js +4 -4
- package/dist/query/utils.js +5 -5
- package/dist/query/warn-registry.d.ts +7 -0
- package/dist/query/warn-registry.js +7 -0
- package/dist/query/where-compile.js +1 -1
- package/dist/query/where.js +30 -31
- package/dist/query/writes.js +7 -7
- package/dist/realtime.js +4 -4
- package/dist/schema-metadata.js +1 -1
- package/dist/schema-sql.js +3 -3
- package/dist/seed.js +1 -1
- package/dist/sqlite.js +7 -7
- package/dist/typed-sql.js +1 -1
- package/package.json +15 -7
package/dist/query/relations.js
CHANGED
|
@@ -112,8 +112,7 @@ export function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
112
112
|
// numeric indices and throw a cryptic `Unknown field "0"`, catch it early
|
|
113
113
|
// with an actionable message.
|
|
114
114
|
if (Array.isArray(select)) {
|
|
115
|
-
throw new ValidationError(`
|
|
116
|
-
`(e.g. { id: true, name: true }), not an array.`);
|
|
115
|
+
throw new ValidationError(`"select" must be an object mapping field names to true ` + `(e.g. { id: true, name: true }), not an array.`);
|
|
117
116
|
}
|
|
118
117
|
// Only include columns where value is true. An explicit `select` naming a
|
|
119
118
|
// PII column IS the opt-in: it comes back regardless of `includePii`.
|
|
@@ -155,7 +154,7 @@ export function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
155
154
|
const hasPii = piiCols !== undefined && piiCols.size > 0;
|
|
156
155
|
if (omit) {
|
|
157
156
|
if (Array.isArray(omit)) {
|
|
158
|
-
throw new ValidationError(`
|
|
157
|
+
throw new ValidationError(`"omit" must be an object mapping field names to true ` + `(e.g. { createdAt: true }), not an array.`);
|
|
159
158
|
}
|
|
160
159
|
// Include all columns except those where value is true (and PII columns).
|
|
161
160
|
const omitCols = new Set(Object.entries(omit)
|
|
@@ -506,7 +505,7 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
506
505
|
if (meta)
|
|
507
506
|
resolveOrderByColumn(qi, qi.table, meta, key);
|
|
508
507
|
if (!params) {
|
|
509
|
-
throw new ValidationError(`
|
|
508
|
+
throw new ValidationError(`Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
|
|
510
509
|
}
|
|
511
510
|
const rawColumn = qi.toColumn(key);
|
|
512
511
|
const operator = whereMod.vectorOperator(qi, key, rawColumn, value.distance.metric);
|
|
@@ -586,7 +585,7 @@ export function nullsSuffix(qi, nulls) {
|
|
|
586
585
|
export function resolveOrderByColumn(_qi, table, meta, key) {
|
|
587
586
|
const col = resolveColumnName(meta, key);
|
|
588
587
|
if (col === undefined) {
|
|
589
|
-
throw new ValidationError(`
|
|
588
|
+
throw new ValidationError(`Unknown field "${key}" in orderBy on table "${table}". ` +
|
|
590
589
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
591
590
|
}
|
|
592
591
|
return col;
|
|
@@ -602,12 +601,12 @@ export function validateJsonPathOrderBy(qi, table, meta, field, spec) {
|
|
|
602
601
|
const col = resolveOrderByColumn(qi, table, meta, field);
|
|
603
602
|
if (spec.path.length === 0 ||
|
|
604
603
|
spec.path.some((el) => typeof el !== 'string' && !(typeof el === 'number' && Number.isFinite(el)))) {
|
|
605
|
-
throw new ValidationError(`
|
|
604
|
+
throw new ValidationError(`JSON-path orderBy on "${field}" (table "${table}") requires a non-empty \`path\` array ` +
|
|
606
605
|
`of keys/indexes (e.g. { path: ['weight'], direction: 'asc' }).`);
|
|
607
606
|
}
|
|
608
607
|
const colType = whereMod.pgTypeForColumn(qi, meta, col);
|
|
609
608
|
if (!whereMod.isJsonColumnType(qi, colType)) {
|
|
610
|
-
throw new ValidationError(`
|
|
609
|
+
throw new ValidationError(`JSON-path orderBy on "${field}": column "${col}" on table "${table}" is not a JSON column ` +
|
|
611
610
|
`(actual type: ${colType}).`);
|
|
612
611
|
}
|
|
613
612
|
return col;
|
|
@@ -624,7 +623,7 @@ export function validateJsonPathOrderBy(qi, table, meta, field, spec) {
|
|
|
624
623
|
export function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, params) {
|
|
625
624
|
const col = validateJsonPathOrderBy(qi, table, meta, field, spec);
|
|
626
625
|
if (!params) {
|
|
627
|
-
throw new ValidationError(`
|
|
626
|
+
throw new ValidationError(`JSON-path ordering on "${field}" is not supported in this orderBy context.`);
|
|
628
627
|
}
|
|
629
628
|
params.push(whereMod.jsonPathParam(qi, spec.path));
|
|
630
629
|
const extract = qi.dialect.buildJsonPathExtract(`${prefix}${qi.q(col)}`, qi.p(params.length));
|
|
@@ -676,14 +675,14 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
|
676
675
|
// relation object. Bounded by the same depth cap as nested `with`.
|
|
677
676
|
for (;;) {
|
|
678
677
|
if (!currentMeta)
|
|
679
|
-
throw new RelationError(`
|
|
678
|
+
throw new RelationError(`Unknown relation target in orderBy chain "${path.join('.')}"`);
|
|
680
679
|
const relDef = ownLookup(currentMeta.relations, relName);
|
|
681
680
|
if (!relDef) {
|
|
682
|
-
throw new ValidationError(`
|
|
681
|
+
throw new ValidationError(`Unknown relation "${relName}" in orderBy on relation "${path.join('.')}" ` +
|
|
683
682
|
`(table "${currentMeta.name}"). Available: ${Object.keys(currentMeta.relations).join(', ') || '(none)'}.`);
|
|
684
683
|
}
|
|
685
684
|
if (relDef.type !== 'belongsTo' && relDef.type !== 'hasOne') {
|
|
686
|
-
throw new ValidationError(`
|
|
685
|
+
throw new ValidationError(`orderBy cannot traverse the to-many relation "${relName}" on "${currentMeta.name}" ` +
|
|
687
686
|
`(path "${path.concat(relName).join('.')}"): a to-many relation has no single value to order by. ` +
|
|
688
687
|
`Use a pick-row ordering ({ pick, by }) at the top level, or order by "_count".`);
|
|
689
688
|
}
|
|
@@ -692,7 +691,7 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
|
692
691
|
}
|
|
693
692
|
const nextMeta = qi.schema.tables[relDef.to];
|
|
694
693
|
if (!nextMeta)
|
|
695
|
-
throw new RelationError(`
|
|
694
|
+
throw new RelationError(`Unknown relation target "${relDef.to}" in orderBy`);
|
|
696
695
|
const nextAlias = `${head.alias}c${hop}`;
|
|
697
696
|
const on = relDef.type === 'belongsTo'
|
|
698
697
|
? qi.dialect.buildCorrelation(nextAlias, relDef.referenceKey, currentAlias, relDef.foreignKey)
|
|
@@ -710,7 +709,7 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
|
710
709
|
// Exactly one key per level, as with the single-hop form.
|
|
711
710
|
const entries = Object.entries(value);
|
|
712
711
|
if (entries.length !== 1) {
|
|
713
|
-
throw new ValidationError(`
|
|
712
|
+
throw new ValidationError(`orderBy on relation "${path.join('.')}" needs exactly one key per level ` +
|
|
714
713
|
`(got: ${entries.map(([k]) => k).join(', ') || '(empty)'}).`);
|
|
715
714
|
}
|
|
716
715
|
const [key, entryValue] = entries[0];
|
|
@@ -726,7 +725,7 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
|
726
725
|
// Terminal: a column on the last table in the chain.
|
|
727
726
|
const snakeCol = resolveColumnName(currentMeta, key);
|
|
728
727
|
if (snakeCol === undefined) {
|
|
729
|
-
throw new ValidationError(`
|
|
728
|
+
throw new ValidationError(`Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
|
|
730
729
|
}
|
|
731
730
|
assertOrderDirection(entryValue, `orderBy on relation path "${path.join('.')}"`);
|
|
732
731
|
const { dir, nulls } = normalizeOrderBy(entryValue);
|
|
@@ -774,9 +773,9 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
774
773
|
// missing relation.
|
|
775
774
|
const isColumn = resolveColumnName(ownerMeta, relName) !== undefined;
|
|
776
775
|
throw new RelationError(isColumn
|
|
777
|
-
? `
|
|
776
|
+
? `orderBy on "${ownerTable}.${relName}" got a relation-shaped value, but "${relName}" is a ` +
|
|
778
777
|
`column. Order a column with 'asc' / 'desc' (or { sort, nulls }); the object form is for relations.`
|
|
779
|
-
: `
|
|
778
|
+
: `Unknown relation "${relName}" in orderBy on table "${ownerTable}". ` +
|
|
780
779
|
availableClause(Object.keys(ownerMeta.relations), `"${ownerTable}" has no relations.`));
|
|
781
780
|
}
|
|
782
781
|
// Pick-row ordering (`{ pick, by }`): order by a value from ONE related
|
|
@@ -792,7 +791,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
792
791
|
if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
793
792
|
const keys = Object.keys(value);
|
|
794
793
|
if (keys.length !== 1 || keys[0] !== '_count') {
|
|
795
|
-
throw new ValidationError(`
|
|
794
|
+
throw new ValidationError(`orderBy on to-many relation "${relName}" only supports "_count" ` +
|
|
796
795
|
`or a pick-row ordering ({ pick, by }) (got: ${keys.join(', ') || '(empty)'}).`);
|
|
797
796
|
}
|
|
798
797
|
assertOrderDirection(value._count, `orderBy { ${relName}: { _count } }`);
|
|
@@ -802,7 +801,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
802
801
|
// To-one: each entry orders by a correlated scalar subquery on a target column.
|
|
803
802
|
const targetMeta = qi.schema.tables[relDef.to];
|
|
804
803
|
if (!targetMeta)
|
|
805
|
-
throw new RelationError(`
|
|
804
|
+
throw new RelationError(`Unknown relation target "${relDef.to}"`);
|
|
806
805
|
const qTarget = qi.q(relDef.to);
|
|
807
806
|
const qParent = qi.q(parentRef);
|
|
808
807
|
// belongsTo: alias.referenceKey = parent.foreignKey; hasOne: reversed.
|
|
@@ -811,7 +810,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
811
810
|
: qi.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
|
|
812
811
|
const entries = Object.entries(value);
|
|
813
812
|
if (entries.length === 0) {
|
|
814
|
-
throw new ValidationError(`
|
|
813
|
+
throw new ValidationError(`orderBy on to-one relation "${relName}" needs at least one target column.`);
|
|
815
814
|
}
|
|
816
815
|
return entries
|
|
817
816
|
.map(([col, dirValue]) => {
|
|
@@ -831,7 +830,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
831
830
|
? ` "${col}" is a relation on "${relDef.to}": order by one of ITS columns, e.g. ` +
|
|
832
831
|
`{ ${relName}: { ${col}: { <column>: 'asc' } } }.`
|
|
833
832
|
: '';
|
|
834
|
-
throw new ValidationError(`
|
|
833
|
+
throw new ValidationError(`Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").${relationHint}`);
|
|
835
834
|
}
|
|
836
835
|
assertOrderDirection(dirValue, `orderBy "${col}" on relation "${relName}"`);
|
|
837
836
|
const { dir, nulls } = normalizeOrderBy(dirValue);
|
|
@@ -862,7 +861,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
862
861
|
* - `by` must be a target column name or a `{ field, path }` JSON-path spec.
|
|
863
862
|
*/
|
|
864
863
|
export function pickOrderNestedError(_qi, relName) {
|
|
865
|
-
return new ValidationError(`
|
|
864
|
+
return new ValidationError(`Pick-row ordering on relation "${relName}" is only supported in a top-level ` +
|
|
866
865
|
'findMany orderBy: nested `with` orderBy does not support it.');
|
|
867
866
|
}
|
|
868
867
|
export function validatePickOrderBy(qi, relName, relDef, spec, nested) {
|
|
@@ -870,10 +869,10 @@ export function validatePickOrderBy(qi, relName, relDef, spec, nested) {
|
|
|
870
869
|
throw pickOrderNestedError(qi, relName);
|
|
871
870
|
}
|
|
872
871
|
if (relDef.type === 'manyToMany') {
|
|
873
|
-
throw new ValidationError(`
|
|
872
|
+
throw new ValidationError(`Pick-row ordering is not supported on manyToMany relation "${relName}": hasMany relations only.`);
|
|
874
873
|
}
|
|
875
874
|
if (relDef.type !== 'hasMany') {
|
|
876
|
-
throw new ValidationError(`
|
|
875
|
+
throw new ValidationError(`Pick-row ordering is only for to-many (hasMany) relations; "${relName}" is ${relDef.type}. ` +
|
|
877
876
|
`Order by the target column directly instead ({ ${relName}: { <column>: 'asc' } }).`);
|
|
878
877
|
}
|
|
879
878
|
const pickOrder = spec.pick?.orderBy;
|
|
@@ -882,20 +881,20 @@ export function validatePickOrderBy(qi, relName, relDef, spec, nested) {
|
|
|
882
881
|
typeof pickOrder !== 'object' ||
|
|
883
882
|
pickOrder === null ||
|
|
884
883
|
Object.keys(pickOrder).length === 0) {
|
|
885
|
-
throw new ValidationError(`
|
|
884
|
+
throw new ValidationError(`Pick-row ordering on relation "${relName}" requires \`pick.orderBy\` to choose ONE ` +
|
|
886
885
|
"related row deterministically (e.g. pick: { orderBy: { createdAt: 'desc' } }).");
|
|
887
886
|
}
|
|
888
887
|
const by = spec.by;
|
|
889
888
|
const validJsonBy = typeof by === 'object' && by !== null && typeof by.field === 'string' && Array.isArray(by.path);
|
|
890
889
|
if (typeof by !== 'string' && !validJsonBy) {
|
|
891
|
-
throw new ValidationError(`
|
|
890
|
+
throw new ValidationError(`Pick-row ordering on relation "${relName}" requires \`by\`: a target column name ` +
|
|
892
891
|
"or a JSON-path spec ({ field: 'data', path: ['title'] }).");
|
|
893
892
|
}
|
|
894
893
|
// Physical plan gate. A typo like `plan: 'latreal'` must never silently run
|
|
895
894
|
// the subquery plan (a silent plan change wearing a validation gap). Shared
|
|
896
895
|
// by build and cache-hit collect so a warmed cache throws identically.
|
|
897
896
|
if (spec.plan !== undefined && spec.plan !== 'subquery' && spec.plan !== 'lateral') {
|
|
898
|
-
throw new ValidationError(`
|
|
897
|
+
throw new ValidationError(`Pick-row ordering on relation "${relName}" has an invalid \`plan\`: ` +
|
|
899
898
|
`${JSON.stringify(spec.plan)}. Use 'subquery' (default) or 'lateral'.`);
|
|
900
899
|
}
|
|
901
900
|
if (spec.plan === 'lateral') {
|
|
@@ -906,7 +905,7 @@ export function validatePickOrderBy(qi, relName, relDef, spec, nested) {
|
|
|
906
905
|
// parent column with that exact name would make the unqualified WHERE
|
|
907
906
|
// reference ambiguous once the join is in scope; refuse it explicitly.
|
|
908
907
|
if (qi.tableMeta.allColumns.includes('__turbine_pick')) {
|
|
909
|
-
throw new ValidationError(`
|
|
908
|
+
throw new ValidationError(`Pick-row ordering with plan: 'lateral' cannot be used: table "${qi.tableMeta.name}" ` +
|
|
910
909
|
'has a column named "__turbine_pick", which the lateral join output reserves.');
|
|
911
910
|
}
|
|
912
911
|
}
|
|
@@ -930,11 +929,11 @@ export function validatePickOrderBy(qi, relName, relDef, spec, nested) {
|
|
|
930
929
|
*/
|
|
931
930
|
export function buildRelationPickOrderBy(qi, relName, relDef, spec, alias, parentRef, params, lateralSink) {
|
|
932
931
|
if (!params) {
|
|
933
|
-
throw new ValidationError(`
|
|
932
|
+
throw new ValidationError(`Pick-row ordering on relation "${relName}" is only supported in a top-level findMany orderBy.`);
|
|
934
933
|
}
|
|
935
934
|
const targetMeta = qi.schema.tables[relDef.to];
|
|
936
935
|
if (!targetMeta)
|
|
937
|
-
throw new RelationError(`
|
|
936
|
+
throw new RelationError(`Unknown relation target "${relDef.to}"`);
|
|
938
937
|
assertDirectionToken(spec.direction, `pick-row orderBy on relation "${relName}"`);
|
|
939
938
|
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
940
939
|
const limitOne = qi.buildPagination('1', undefined, true);
|
|
@@ -959,7 +958,7 @@ export function buildRelationPickOrderBy(qi, relName, relDef, spec, alias, paren
|
|
|
959
958
|
// context and hard-fails rather than silently emitting a subquery.
|
|
960
959
|
if (spec.plan === 'lateral') {
|
|
961
960
|
if (!lateralSink) {
|
|
962
|
-
throw new ValidationError(`
|
|
961
|
+
throw new ValidationError(`Pick-row ordering with plan: 'lateral' on relation "${relName}" is only supported ` +
|
|
963
962
|
'in a top-level findMany orderBy.');
|
|
964
963
|
}
|
|
965
964
|
const childAlias = `${alias}i`;
|
|
@@ -1026,7 +1025,7 @@ export function collectRelationPickOrderParams(qi, relName, relDef, spec, params
|
|
|
1026
1025
|
validatePickOrderBy(qi, relName, relDef, spec, false);
|
|
1027
1026
|
const targetMeta = qi.schema.tables[relDef.to];
|
|
1028
1027
|
if (!targetMeta)
|
|
1029
|
-
throw new RelationError(`
|
|
1028
|
+
throw new RelationError(`Unknown relation target "${relDef.to}"`);
|
|
1030
1029
|
if (typeof spec.by === 'string') {
|
|
1031
1030
|
resolveOrderByColumn(qi, relDef.to, targetMeta, spec.by);
|
|
1032
1031
|
}
|
|
@@ -1071,7 +1070,7 @@ export function buildRelationOrderClause(qi, targetTable, targetMeta, alias, ord
|
|
|
1071
1070
|
const orders = orderEntries
|
|
1072
1071
|
.map(([key, dirValue]) => {
|
|
1073
1072
|
if (isVectorOrderBy(dirValue)) {
|
|
1074
|
-
throw new ValidationError(`
|
|
1073
|
+
throw new ValidationError(`Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
|
|
1075
1074
|
}
|
|
1076
1075
|
if (isJsonPathOrderBy(dirValue)) {
|
|
1077
1076
|
return buildJsonPathOrderEntry(qi, targetTable, targetMeta, key, dirValue, `${alias}.`, params);
|
|
@@ -1097,7 +1096,7 @@ export function buildRelationOrderClause(qi, targetTable, targetMeta, alias, ord
|
|
|
1097
1096
|
export function collectRelationOrderParams(qi, targetTable, targetMeta, orderEntries, params) {
|
|
1098
1097
|
for (const [key, dirValue] of orderEntries) {
|
|
1099
1098
|
if (isVectorOrderBy(dirValue)) {
|
|
1100
|
-
throw new ValidationError(`
|
|
1099
|
+
throw new ValidationError(`Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
|
|
1101
1100
|
}
|
|
1102
1101
|
if (isJsonPathOrderBy(dirValue)) {
|
|
1103
1102
|
validateJsonPathOrderBy(qi, targetTable, targetMeta, key, dirValue);
|
|
@@ -1141,7 +1140,7 @@ export function buildRelationCountExpr(qi, relDef, parentRef, alias, params) {
|
|
|
1141
1140
|
const count = qi.castAgg('COUNT(*)', 'int');
|
|
1142
1141
|
if (relDef.type === 'manyToMany') {
|
|
1143
1142
|
if (!relDef.through) {
|
|
1144
|
-
throw new ValidationError(`
|
|
1143
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}" is missing its \`through\` junction.`);
|
|
1145
1144
|
}
|
|
1146
1145
|
const qJ = qi.q(relDef.through.table);
|
|
1147
1146
|
const jalias = `${alias}j`;
|
|
@@ -1325,9 +1324,22 @@ export function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1325
1324
|
parsed[relName] = jsonVal;
|
|
1326
1325
|
}
|
|
1327
1326
|
}
|
|
1328
|
-
catch {
|
|
1329
|
-
|
|
1330
|
-
|
|
1327
|
+
catch (err) {
|
|
1328
|
+
// A relation column that does not parse is a bug in the emitted SQL or
|
|
1329
|
+
// a driver-level corruption, not a recoverable condition. Handing back
|
|
1330
|
+
// the raw string satisfied the assignment and violated the generated
|
|
1331
|
+
// type: callers got a `string` where `Post[]` was promised, and found
|
|
1332
|
+
// out at the first `.map`. Throw instead, with the context needed to
|
|
1333
|
+
// find it.
|
|
1334
|
+
//
|
|
1335
|
+
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1336
|
+
// per-relation loop: one 10,000-row page was 10,000 console.warn calls.
|
|
1337
|
+
if (process.env.NODE_ENV !== 'production' &&
|
|
1338
|
+
shouldWarnOnce(WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1339
|
+
console.warn(`[turbine] Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1340
|
+
`This is a bug; please report it with the emitted SQL.`);
|
|
1341
|
+
}
|
|
1342
|
+
throw new ValidationError(`Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1331
1343
|
}
|
|
1332
1344
|
}
|
|
1333
1345
|
else if (Array.isArray(rawValue)) {
|
|
@@ -2158,7 +2170,7 @@ export function makeFlattenParser(qi, plan) {
|
|
|
2158
2170
|
export function buildSelectWithRelations(qi, table, withClause, params, columnsList, depth, path, includePii, flatten) {
|
|
2159
2171
|
const meta = qi.schema.tables[table];
|
|
2160
2172
|
if (!meta)
|
|
2161
|
-
throw new ValidationError(`
|
|
2173
|
+
throw new ValidationError(`Unknown table "${table}"`);
|
|
2162
2174
|
// Positional JSON encoding is Postgres-only in v1. Gate here, the single
|
|
2163
2175
|
// entry point for every `with` clause, so no engine ever emits the
|
|
2164
2176
|
// json_build_array shape its dialect can't produce (and mssql's FOR JSON
|
|
@@ -2177,7 +2189,7 @@ export function buildSelectWithRelations(qi, table, withClause, params, columnsL
|
|
|
2177
2189
|
continue;
|
|
2178
2190
|
const relDef = ownLookup(meta.relations, relName);
|
|
2179
2191
|
if (!relDef) {
|
|
2180
|
-
throw new RelationError(`
|
|
2192
|
+
throw new RelationError(`Unknown relation "${relName}" on table "${table}". ` +
|
|
2181
2193
|
availableClause(Object.keys(meta.relations), `"${table}" has no relations.`));
|
|
2182
2194
|
}
|
|
2183
2195
|
// `relationLoadStrategy: 'flatten'`: an eligible to-one relation becomes a
|
|
@@ -2311,7 +2323,7 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2311
2323
|
}
|
|
2312
2324
|
const targetMeta = qi.schema.tables[targetTable];
|
|
2313
2325
|
if (!targetMeta)
|
|
2314
|
-
throw new RelationError(`
|
|
2326
|
+
throw new RelationError(`Unknown relation target "${targetTable}"`);
|
|
2315
2327
|
// `true` IS `{}`; below this line there is no `true` case, which is what
|
|
2316
2328
|
// keeps this walk and collectRelationSubqueryParams reading one shape.
|
|
2317
2329
|
// The raw `spec` is still what crosses the dialect seam below, since
|
|
@@ -2416,7 +2428,7 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2416
2428
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
2417
2429
|
const nestedRelDef = ownLookup(targetMeta.relations, nestedRelName);
|
|
2418
2430
|
if (!nestedRelDef) {
|
|
2419
|
-
throw new RelationError(`
|
|
2431
|
+
throw new RelationError(`Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2420
2432
|
availableClause(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2421
2433
|
}
|
|
2422
2434
|
// Recursively build nested subquery, passing THIS alias as the parent reference
|
|
@@ -2490,7 +2502,7 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2490
2502
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
2491
2503
|
const nestedRelDef = ownLookup(targetMeta.relations, nestedRelName);
|
|
2492
2504
|
if (!nestedRelDef) {
|
|
2493
|
-
throw new RelationError(`
|
|
2505
|
+
throw new RelationError(`Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2494
2506
|
availableClause(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2495
2507
|
}
|
|
2496
2508
|
const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
|
@@ -2534,7 +2546,7 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
2534
2546
|
*/
|
|
2535
2547
|
export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCounter, currentDepth, currentPath, talias, targetMeta, targetColumns, includePii) {
|
|
2536
2548
|
if (!relDef.through) {
|
|
2537
|
-
throw new ValidationError(`
|
|
2549
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
2538
2550
|
}
|
|
2539
2551
|
// `true` IS `{}`; see {@link relationOptions}. Same reading as
|
|
2540
2552
|
// buildRelationSubquery and collectRelationSubqueryParams' m2m branch.
|
|
@@ -2550,12 +2562,12 @@ export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, ali
|
|
|
2550
2562
|
// introspected PK means we cannot know what to JOIN on, fail loudly rather than
|
|
2551
2563
|
// silently guessing `id` and generating a wrong JOIN.
|
|
2552
2564
|
if (targetMeta.primaryKey.length === 0) {
|
|
2553
|
-
throw new ValidationError(`
|
|
2565
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
|
|
2554
2566
|
`cannot determine the join column. Define a primary key or use an explicit through descriptor.`);
|
|
2555
2567
|
}
|
|
2556
2568
|
const targetPk = targetMeta.primaryKey;
|
|
2557
2569
|
if (targetKeys.length !== targetPk.length) {
|
|
2558
|
-
throw new ValidationError(`
|
|
2570
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}": through.targetKey has ${targetKeys.length} column(s) ` +
|
|
2559
2571
|
`but target "${targetTable}" primary key has ${targetPk.length}. Composite keys must pair positionally.`);
|
|
2560
2572
|
}
|
|
2561
2573
|
const joinOn = targetKeys.map((jcol, i) => `${jalias}.${qi.q(jcol)} = ${talias}.${qi.q(targetPk[i])}`).join(' AND ');
|
|
@@ -2563,7 +2575,7 @@ export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, ali
|
|
|
2563
2575
|
const sourceKeys = normalizeKeyColumns(relDef.through.sourceKey);
|
|
2564
2576
|
const refKeys = normalizeKeyColumns(relDef.referenceKey);
|
|
2565
2577
|
if (sourceKeys.length !== refKeys.length) {
|
|
2566
|
-
throw new ValidationError(`
|
|
2578
|
+
throw new ValidationError(`manyToMany relation "${relDef.name}": through.sourceKey has ${sourceKeys.length} column(s) ` +
|
|
2567
2579
|
`but referenceKey has ${refKeys.length}. Composite keys must pair positionally.`);
|
|
2568
2580
|
}
|
|
2569
2581
|
let whereClause = sourceKeys
|
|
@@ -2609,7 +2621,7 @@ export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, ali
|
|
|
2609
2621
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
2610
2622
|
const nestedRelDef = ownLookup(targetMeta.relations, nestedRelName);
|
|
2611
2623
|
if (!nestedRelDef) {
|
|
2612
|
-
throw new RelationError(`
|
|
2624
|
+
throw new RelationError(`Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2613
2625
|
availableClause(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2614
2626
|
}
|
|
2615
2627
|
const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
|
@@ -2629,7 +2641,7 @@ export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, ali
|
|
|
2629
2641
|
for (const [nestedRelName, nestedSpec] of sortedEntries(opts.with)) {
|
|
2630
2642
|
const nestedRelDef = ownLookup(targetMeta.relations, nestedRelName);
|
|
2631
2643
|
if (!nestedRelDef) {
|
|
2632
|
-
throw new RelationError(`
|
|
2644
|
+
throw new RelationError(`Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2633
2645
|
availableClause(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2634
2646
|
}
|
|
2635
2647
|
const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, talias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
package/dist/query/types.js
CHANGED
|
@@ -66,7 +66,7 @@ import { ValidationError } from '../errors.js';
|
|
|
66
66
|
export const UNSAFE = Symbol.for('turbine-orm.UNSAFE');
|
|
67
67
|
/** The one message shape every privilege refusal uses. */
|
|
68
68
|
function privilegeRefusal(option, received, extra = '') {
|
|
69
|
-
return new ValidationError(
|
|
69
|
+
return new ValidationError(`\`${option}\` must be the \`UNSAFE\` symbol, received ${describeValue(received)}. ` +
|
|
70
70
|
`${option} is a privilege option: it removes a safety boundary (a tenant filter, the PII ` +
|
|
71
71
|
'projection, or the empty-`where` guard), so it cannot be enabled by a plain value that ' +
|
|
72
72
|
'JSON.parse can produce. Import the sentinel and pass it explicitly: ' +
|
|
@@ -138,12 +138,12 @@ export function resolveSkipGlobalFilters(value) {
|
|
|
138
138
|
// `[UNSAFE, ...tables]` ESCALATE to a global skip whenever `tables` happens
|
|
139
139
|
// to come back empty, which is the same accident with a much worse outcome.
|
|
140
140
|
if (tables.length === 0) {
|
|
141
|
-
throw new ValidationError('
|
|
141
|
+
throw new ValidationError('`skipGlobalFilters: [UNSAFE]` names no table, so it would skip nothing. ' +
|
|
142
142
|
'Pass the tables to skip (`[UNSAFE, "posts"]`), or the bare sentinel (`UNSAFE`) to skip every table.');
|
|
143
143
|
}
|
|
144
144
|
for (const t of tables) {
|
|
145
145
|
if (typeof t !== 'string') {
|
|
146
|
-
throw new ValidationError('
|
|
146
|
+
throw new ValidationError('`skipGlobalFilters: [UNSAFE, ...]` takes table accessor NAMES after the sentinel, ' +
|
|
147
147
|
`received ${describeValue(t)}.`);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
@@ -205,7 +205,7 @@ export function assertDirectionToken(value, context) {
|
|
|
205
205
|
if (lowered === 'asc' || lowered === 'desc')
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
|
-
throw new ValidationError(`
|
|
208
|
+
throw new ValidationError(`Invalid orderBy direction ${describeValue(value)} for ${context}. ` +
|
|
209
209
|
"Use 'asc' or 'desc'. An unrecognized direction used to sort ASCENDING silently, " +
|
|
210
210
|
'which returns a correct-looking page in the wrong order.');
|
|
211
211
|
}
|
package/dist/query/utils.js
CHANGED
|
@@ -1415,7 +1415,7 @@ export function unknownFieldMessage(table, field, meta) {
|
|
|
1415
1415
|
const didYouMean = suggestion
|
|
1416
1416
|
? ` Did you mean "${suggestion}"${relations.includes(suggestion) ? ' (a relation)' : ''}?`
|
|
1417
1417
|
: '';
|
|
1418
|
-
return (`
|
|
1418
|
+
return (`Unknown field "${field}" on table "${table}".${didYouMean}` +
|
|
1419
1419
|
` Known columns: ${columns.join(', ') || '(none)'}.` +
|
|
1420
1420
|
(relations.length ? ` Known relations (valid in \`where\` and \`with\`): ${relations.join(', ')}.` : ''));
|
|
1421
1421
|
}
|
|
@@ -1450,7 +1450,7 @@ export function availableClause(names, emptySentence) {
|
|
|
1450
1450
|
* it. Naming the fix costs one sentence and saves a search.
|
|
1451
1451
|
*/
|
|
1452
1452
|
export function relationInProjectionMessage(table, field, clause) {
|
|
1453
|
-
const head = `
|
|
1453
|
+
const head = `"${field}" is a relation on table "${table}", not a column, so it cannot be named in \`${clause}\`.`;
|
|
1454
1454
|
return clause === 'select'
|
|
1455
1455
|
? `${head} Load it with \`with: { ${field}: true }\`, which is a sibling of \`select\`, not a member of it.` +
|
|
1456
1456
|
" To narrow the relation's own columns, put a `select` inside that relation's options:" +
|
|
@@ -1501,11 +1501,11 @@ export function warnRedundantSortTerm(table, clause, dropped) {
|
|
|
1501
1501
|
* which is exactly the class 0.64/0.65 exist to kill.
|
|
1502
1502
|
*/
|
|
1503
1503
|
export function selectNamesNothingMessage(table) {
|
|
1504
|
-
return (`
|
|
1504
|
+
return (`"select" names no fields (on table "${table}"): every value is false or it is empty. ` +
|
|
1505
1505
|
`Pass at least one field as true, or drop "select" to get the default projection.`);
|
|
1506
1506
|
}
|
|
1507
1507
|
export function selectOmitExclusiveMessage(table) {
|
|
1508
|
-
return (`
|
|
1508
|
+
return (`"select" and "omit" are mutually exclusive (on table "${table}"). ` +
|
|
1509
1509
|
`A select already lists exactly the fields you want.`);
|
|
1510
1510
|
}
|
|
1511
1511
|
/**
|
|
@@ -1559,7 +1559,7 @@ export function normalizePagination(args) {
|
|
|
1559
1559
|
function assertAliasAgrees(alias, aliasValue, native, nativeValue) {
|
|
1560
1560
|
if (nativeValue === undefined || nativeValue === aliasValue)
|
|
1561
1561
|
return;
|
|
1562
|
-
throw new ValidationError(`
|
|
1562
|
+
throw new ValidationError(`"${alias}" and "${native}" are the same option and were given different values ` +
|
|
1563
1563
|
`(${alias}: ${String(aliasValue)}, ${native}: ${String(nativeValue)}). ` +
|
|
1564
1564
|
`"${alias}" is Prisma's spelling of "${native}"; pass one of them.`);
|
|
1565
1565
|
}
|
|
@@ -127,4 +127,11 @@ export declare const WARN_NS: {
|
|
|
127
127
|
* Keyed on the requested mode.
|
|
128
128
|
*/
|
|
129
129
|
readonly planCacheModeIgnored: "planCacheModeIgnored";
|
|
130
|
+
/**
|
|
131
|
+
* A relation's JSON payload could not be parsed in `parseNestedRow`
|
|
132
|
+
* (relations.ts). Keyed on `table.relation`, so a page of 10,000 rows whose
|
|
133
|
+
* relation column is malformed says it once rather than once per row: the
|
|
134
|
+
* warn sits in the per-relation loop, which runs once per row per relation.
|
|
135
|
+
*/
|
|
136
|
+
readonly relationParseFailure: "relationParseFailure";
|
|
130
137
|
};
|
|
@@ -162,4 +162,11 @@ export const WARN_NS = {
|
|
|
162
162
|
* Keyed on the requested mode.
|
|
163
163
|
*/
|
|
164
164
|
planCacheModeIgnored: 'planCacheModeIgnored',
|
|
165
|
+
/**
|
|
166
|
+
* A relation's JSON payload could not be parsed in `parseNestedRow`
|
|
167
|
+
* (relations.ts). Keyed on `table.relation`, so a page of 10,000 rows whose
|
|
168
|
+
* relation column is malformed says it once rather than once per row: the
|
|
169
|
+
* warn sits in the per-relation loop, which runs once per row per relation.
|
|
170
|
+
*/
|
|
171
|
+
relationParseFailure: 'relationParseFailure',
|
|
165
172
|
};
|
|
@@ -68,7 +68,7 @@ export const MAX_WHERE_DEPTH = 32;
|
|
|
68
68
|
export function assertWhereDepth(depth, clause = 'where') {
|
|
69
69
|
if (depth <= MAX_WHERE_DEPTH)
|
|
70
70
|
return;
|
|
71
|
-
throw new ValidationError(
|
|
71
|
+
throw new ValidationError(`\`${clause}\` clause nests more than ${MAX_WHERE_DEPTH} levels of AND / OR / NOT ` +
|
|
72
72
|
`(or relation filters) deep. That is far past anything a real predicate needs, and an unbounded ` +
|
|
73
73
|
`walk over caller-supplied nesting is a stack-overflow surface, so it is refused. If this is a ` +
|
|
74
74
|
`generated predicate, flatten it: a single \`AND\` / \`OR\` array of N conditions is one level, ` +
|