turbine-orm 0.60.1 → 0.62.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 +71 -27
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +374 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +248 -11
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline-submittable.js +26 -3
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +149 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +373 -27
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +250 -13
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline-submittable.js +26 -3
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +148 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
package/dist/prisma-compat.js
CHANGED
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
* by this layer, and a key that is neither a Prisma arg nor a turbine option
|
|
41
41
|
* gets a dev-mode warning instead of vanishing (see
|
|
42
42
|
* {@link PRISMA_ARG_KEYS} and `warnUnknownQueryOptions`).
|
|
43
|
+
* The three PRIVILEGE options among them (`skipGlobalFilters`, `includePii`,
|
|
44
|
+
* `allowFullTableScan`) are forwarded VERBATIM, which is what makes them safe
|
|
45
|
+
* here: core accepts only the `UNSAFE` sentinel, so a Prisma-shaped args
|
|
46
|
+
* object carrying `includePii: true` reaches core and is refused there, and
|
|
47
|
+
* this layer never needs its own copy of that rule.
|
|
43
48
|
*
|
|
44
49
|
* ## What it deliberately does NOT do (documented divergences)
|
|
45
50
|
*
|
|
@@ -75,11 +80,16 @@
|
|
|
75
80
|
* - **`limit` on `updateMany` / `deleteMany`** (Prisma 6.7+) throws. Turbine has
|
|
76
81
|
* no row-bounded mass mutation, and dropping a SAFETY BOUND with a warning
|
|
77
82
|
* would turn "change at most 10 rows" into "change every matching row".
|
|
78
|
-
* - **Write projections
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
+
* - **Write projections**: `select` / `omit` on create/update/delete/upsert
|
|
84
|
+
* narrow the RETURNED OBJECT (see {@link resolveWriteProjection}); they do
|
|
85
|
+
* NOT narrow the statement's `RETURNING` list, so the column still crosses
|
|
86
|
+
* the wire, and `pii: true` remains the SQL-level control. A relation key in
|
|
87
|
+
* either (or any `include` on a write) THROWS, since a write returns its own
|
|
88
|
+
* row only. `omit` on READS is translated to core's own `omit`, see
|
|
89
|
+
* {@link translateOmit}.
|
|
90
|
+
* - **`select` on `count`** and **`orderBy` / `cursor` / `take` / `skip` on
|
|
91
|
+
* `aggregate`** are accepted and IGNORED (they are legitimate Prisma, so they
|
|
92
|
+
* never warn); a plain number / the aggregate comes back.
|
|
83
93
|
*
|
|
84
94
|
* ## Type dependencies (0.41.0)
|
|
85
95
|
*
|
|
@@ -112,7 +122,7 @@
|
|
|
112
122
|
import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
|
|
113
123
|
import { createManyShapeRuns } from './nested-write.js';
|
|
114
124
|
import { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
115
|
-
import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
|
|
125
|
+
import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UNSAFE, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
|
|
116
126
|
import { suggestKey } from './query/utils.js';
|
|
117
127
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
118
128
|
// ---------------------------------------------------------------------------
|
|
@@ -601,16 +611,53 @@ function mapTake(take) {
|
|
|
601
611
|
return take;
|
|
602
612
|
}
|
|
603
613
|
/**
|
|
604
|
-
* Translate Prisma `
|
|
605
|
-
*
|
|
606
|
-
*
|
|
614
|
+
* Translate a Prisma `omit` (scalar field names → true) into Turbine's `omit`.
|
|
615
|
+
*
|
|
616
|
+
* This used to be DROPPED silently, which made it a data-exposure bug rather
|
|
617
|
+
* than an ergonomics one: `omit` is the idiom for a sensitive-but-untagged
|
|
618
|
+
* column (`passwordHash`, `resetToken`), so a caller who asked for it got the
|
|
619
|
+
* column back anyway, in a response they believed was already filtered. Turbine
|
|
620
|
+
* core has supported `omit` on reads all along; only this translation was
|
|
621
|
+
* missing.
|
|
622
|
+
*
|
|
623
|
+
* A relation name is refused rather than forwarded: core's `omit` narrows the
|
|
624
|
+
* SCALAR projection, so `omit: { posts: true }` would have compiled to nothing
|
|
625
|
+
* and dropped the caller's intent a second time. Prisma rejects it too.
|
|
626
|
+
*/
|
|
627
|
+
function translateOmit(ctx, mm, omit) {
|
|
628
|
+
const out = {};
|
|
629
|
+
let any = false;
|
|
630
|
+
for (const [key, val] of Object.entries(omit)) {
|
|
631
|
+
if (val === false || val == null)
|
|
632
|
+
continue;
|
|
633
|
+
if (mm.relations[key]) {
|
|
634
|
+
throw new ValidationError(`[turbine] prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
|
|
635
|
+
'is a relation. Leave the relation out of `include` instead.');
|
|
636
|
+
}
|
|
637
|
+
out[renameField(mm, key)] = true;
|
|
638
|
+
any = true;
|
|
639
|
+
}
|
|
640
|
+
return any ? out : undefined;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Translate Prisma `include` / `select` / `omit` into Turbine
|
|
644
|
+
* `{ select, omit, with }`. `include` keeps all scalars and adds relations;
|
|
645
|
+
* `select` narrows scalars and may also pull relations + `_count`. `include`
|
|
646
|
+
* and `select` are mutually exclusive, and so are `select` and `omit` (a
|
|
647
|
+
* narrowed projection minus fields is ambiguous; Prisma refuses the pair too).
|
|
607
648
|
*/
|
|
608
649
|
function translateProjection(ctx, mm, args) {
|
|
609
650
|
const include = args.include;
|
|
610
651
|
const select = args.select;
|
|
652
|
+
const omitArg = args.omit;
|
|
611
653
|
if (include && select) {
|
|
612
654
|
throw new ValidationError('[turbine] prisma-compat: `include` and `select` are mutually exclusive.');
|
|
613
655
|
}
|
|
656
|
+
if (select && omitArg) {
|
|
657
|
+
throw new ValidationError('[turbine] prisma-compat: `select` and `omit` are mutually exclusive. ' +
|
|
658
|
+
'A `select` already lists exactly the fields you want.');
|
|
659
|
+
}
|
|
660
|
+
const omit = omitArg ? translateOmit(ctx, mm, omitArg) : undefined;
|
|
614
661
|
const withClause = {};
|
|
615
662
|
let hasWith = false;
|
|
616
663
|
if (include) {
|
|
@@ -629,7 +676,7 @@ function translateProjection(ctx, mm, args) {
|
|
|
629
676
|
withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val);
|
|
630
677
|
hasWith = true;
|
|
631
678
|
}
|
|
632
|
-
return { with: hasWith ? withClause : undefined };
|
|
679
|
+
return { omit, with: hasWith ? withClause : undefined };
|
|
633
680
|
}
|
|
634
681
|
if (select) {
|
|
635
682
|
const scalar = {};
|
|
@@ -653,7 +700,130 @@ function translateProjection(ctx, mm, args) {
|
|
|
653
700
|
}
|
|
654
701
|
return { select: hasScalar ? scalar : undefined, with: hasWith ? withClause : undefined };
|
|
655
702
|
}
|
|
656
|
-
return {};
|
|
703
|
+
return { omit };
|
|
704
|
+
}
|
|
705
|
+
const NO_WRITE_PROJECTION = {};
|
|
706
|
+
/**
|
|
707
|
+
* Refuse a write-projection key that names no field on the model.
|
|
708
|
+
*
|
|
709
|
+
* Unlike a READ projection, this one never reaches core: `select` / `omit` on a
|
|
710
|
+
* write are applied to the already-returned object by
|
|
711
|
+
* {@link applyWriteProjection}, so nothing downstream can notice the name is
|
|
712
|
+
* wrong. A typo therefore failed silently in BOTH directions, and the `omit`
|
|
713
|
+
* direction is the one that matters: `omit: { ssn: true }` misspelled as
|
|
714
|
+
* `snn` returned the column, and the call site that asked for it hidden had no
|
|
715
|
+
* signal at all. (`pick` failed the other way, quietly dropping the field from
|
|
716
|
+
* the result.)
|
|
717
|
+
*
|
|
718
|
+
* The read path has never had this hole: core validates every projection key
|
|
719
|
+
* and throws E003 on an unknown one, which is the behaviour a caller has
|
|
720
|
+
* already observed on `findMany` before writing the same clause on `create`.
|
|
721
|
+
* So this is not a new rule, it is the read rule reaching the one surface that
|
|
722
|
+
* bypassed it. Errors match core's shape, including the suggestion.
|
|
723
|
+
*/
|
|
724
|
+
function assertWriteProjectionField(ctx, mm, op, clause, key) {
|
|
725
|
+
if (Object.hasOwn(mm.fields, key))
|
|
726
|
+
return;
|
|
727
|
+
const known = Object.keys(mm.fields);
|
|
728
|
+
const suggestion = suggestKey(key, new Set(known));
|
|
729
|
+
throw new ValidationError(`[turbine] prisma-compat: unknown field "${key}" in \`${clause}\` on ${modelName(ctx, mm)}.${op}().` +
|
|
730
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : '') +
|
|
731
|
+
` Known fields: ${known.join(', ') || '(none)'}.`);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Resolve `select` / `omit` / `include` on create / update / delete / upsert.
|
|
735
|
+
*
|
|
736
|
+
* All three used to be accepted and IGNORED here. For `omit` that is the exact
|
|
737
|
+
* data-exposure bug that was fixed for reads: `omit` is the idiom for a
|
|
738
|
+
* sensitive-but-untagged column, and because it demonstrably WORKS on
|
|
739
|
+
* `findMany`, a caller has positive evidence for assuming it works on `create`
|
|
740
|
+
* too. `select` is the same class in the other spelling (the caller asked for
|
|
741
|
+
* three fields and got the whole row).
|
|
742
|
+
*
|
|
743
|
+
* SCOPE, stated exactly rather than implied: this narrows the OBJECT this
|
|
744
|
+
* adapter returns. Core's write args carry no projection, so the column is
|
|
745
|
+
* still in the statement's `RETURNING` list and still crosses the wire. The
|
|
746
|
+
* SQL-level control is `pii: true` on the column, which is enforced in core and
|
|
747
|
+
* is unaffected by any of this.
|
|
748
|
+
*
|
|
749
|
+
* A relation (or `_count`) key THROWS instead: a write returns its own row and
|
|
750
|
+
* nothing else, so serving it is impossible, and the alternative is handing
|
|
751
|
+
* back a row whose relation property is silently `undefined`, which is the
|
|
752
|
+
* failure mode this whole round exists to remove.
|
|
753
|
+
*/
|
|
754
|
+
function resolveWriteProjection(ctx, mm, op, args) {
|
|
755
|
+
if (!args)
|
|
756
|
+
return NO_WRITE_PROJECTION;
|
|
757
|
+
const include = args.include;
|
|
758
|
+
const select = args.select;
|
|
759
|
+
const omitArg = args.omit;
|
|
760
|
+
if (include && select) {
|
|
761
|
+
throw new ValidationError('[turbine] prisma-compat: `include` and `select` are mutually exclusive.');
|
|
762
|
+
}
|
|
763
|
+
if (select && omitArg) {
|
|
764
|
+
throw new ValidationError('[turbine] prisma-compat: `select` and `omit` are mutually exclusive. ' +
|
|
765
|
+
'A `select` already lists exactly the fields you want.');
|
|
766
|
+
}
|
|
767
|
+
if (include) {
|
|
768
|
+
for (const [key, val] of Object.entries(include)) {
|
|
769
|
+
if (val === false || val == null)
|
|
770
|
+
continue;
|
|
771
|
+
throw new UnsupportedFeatureError(`include ("${key}") on ${op}`, 'prisma-compat', `a write on model "${modelName(ctx, mm)}" returns its own row only; read the relation back with a ` +
|
|
772
|
+
'separate findUnique inside the same $transaction.');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (select) {
|
|
776
|
+
const pick = new Set();
|
|
777
|
+
for (const [key, val] of Object.entries(select)) {
|
|
778
|
+
if (val === false || val == null)
|
|
779
|
+
continue;
|
|
780
|
+
if (key === '_count' || mm.relations[key]) {
|
|
781
|
+
throw new UnsupportedFeatureError(`select of "${key}" on ${op}`, 'prisma-compat', `"${key}" is a relation on model "${modelName(ctx, mm)}"; a write returns its own row only, so read ` +
|
|
782
|
+
'it back with a separate findUnique inside the same $transaction.');
|
|
783
|
+
}
|
|
784
|
+
assertWriteProjectionField(ctx, mm, op, 'select', key);
|
|
785
|
+
pick.add(key);
|
|
786
|
+
}
|
|
787
|
+
// An all-false `select` names nothing. Reads treat that as "no projection"
|
|
788
|
+
// (translateProjection returns select: undefined), so writes do too rather
|
|
789
|
+
// than inventing a second rule for input Prisma rejects anyway.
|
|
790
|
+
return pick.size ? { pick } : NO_WRITE_PROJECTION;
|
|
791
|
+
}
|
|
792
|
+
if (omitArg) {
|
|
793
|
+
const omit = new Set();
|
|
794
|
+
for (const [key, val] of Object.entries(omitArg)) {
|
|
795
|
+
if (val === false || val == null)
|
|
796
|
+
continue;
|
|
797
|
+
if (mm.relations[key]) {
|
|
798
|
+
throw new ValidationError(`[turbine] prisma-compat: \`omit\` on model "${modelName(ctx, mm)}" takes scalar fields, but "${key}" ` +
|
|
799
|
+
'is a relation.');
|
|
800
|
+
}
|
|
801
|
+
assertWriteProjectionField(ctx, mm, op, 'omit', key);
|
|
802
|
+
omit.add(key);
|
|
803
|
+
}
|
|
804
|
+
return omit.size ? { omit } : NO_WRITE_PROJECTION;
|
|
805
|
+
}
|
|
806
|
+
return NO_WRITE_PROJECTION;
|
|
807
|
+
}
|
|
808
|
+
/** Apply a {@link WriteProjection} to one already-reshaped write result row. */
|
|
809
|
+
function applyWriteProjection(proj, row) {
|
|
810
|
+
if (!isPlainObject(row))
|
|
811
|
+
return row;
|
|
812
|
+
if (proj.pick) {
|
|
813
|
+
const out = {};
|
|
814
|
+
for (const key of proj.pick) {
|
|
815
|
+
if (key in row)
|
|
816
|
+
out[key] = row[key];
|
|
817
|
+
}
|
|
818
|
+
return out;
|
|
819
|
+
}
|
|
820
|
+
if (proj.omit) {
|
|
821
|
+
const out = { ...row };
|
|
822
|
+
for (const key of proj.omit)
|
|
823
|
+
delete out[key];
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
return row;
|
|
657
827
|
}
|
|
658
828
|
/** Translate a Prisma relation include payload into a Turbine `WithOptions`. */
|
|
659
829
|
function translateWithOption(ctx, mm, turbineRel, val) {
|
|
@@ -672,10 +842,14 @@ function translateWithOption(ctx, mm, turbineRel, val) {
|
|
|
672
842
|
if (val.skip !== undefined) {
|
|
673
843
|
throw new UnsupportedFeatureError('skip (offset) on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no offset, page the relation with a separate query.");
|
|
674
844
|
}
|
|
675
|
-
if (target && (val.select !== undefined || val.include !== undefined)) {
|
|
845
|
+
if (target && (val.select !== undefined || val.include !== undefined || val.omit !== undefined)) {
|
|
676
846
|
const proj = translateProjection(ctx, target, val);
|
|
677
847
|
if (proj.select)
|
|
678
848
|
opt.select = proj.select;
|
|
849
|
+
// Same silent drop as the top level: a nested `omit` names the sensitive
|
|
850
|
+
// column on the CHILD rows, which are just as exposed.
|
|
851
|
+
if (proj.omit)
|
|
852
|
+
opt.omit = proj.omit;
|
|
679
853
|
if (proj.with)
|
|
680
854
|
opt.with = proj.with;
|
|
681
855
|
}
|
|
@@ -722,6 +896,8 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
|
|
|
722
896
|
const proj = translateProjection(ctx, mm, prismaArgs);
|
|
723
897
|
if (proj.select)
|
|
724
898
|
t.select = proj.select;
|
|
899
|
+
if (proj.omit)
|
|
900
|
+
t.omit = proj.omit;
|
|
725
901
|
if (proj.with)
|
|
726
902
|
t.with = proj.with;
|
|
727
903
|
if (Array.isArray(prismaArgs.distinct)) {
|
|
@@ -1483,15 +1659,22 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1483
1659
|
findUnique: (args) => defer('findUnique', args, () => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique'), 'unique'), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
|
|
1484
1660
|
findFirstOrThrow: (args = {}) => defer('findFirstOrThrow', args, () => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
1485
1661
|
findUniqueOrThrow: (args) => defer('findUniqueOrThrow', args, () => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow'), 'unique'), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
1486
|
-
create: (args) =>
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1662
|
+
create: (args) => {
|
|
1663
|
+
// Resolved inside the translate thunk below (so it validates before the
|
|
1664
|
+
// write runs and stays deferred), read back by every result path.
|
|
1665
|
+
let proj = NO_WRITE_PROJECTION;
|
|
1666
|
+
const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
|
|
1667
|
+
return defer('create', args, () => {
|
|
1668
|
+
proj = resolveWriteProjection(ctx, mm, 'create', args);
|
|
1669
|
+
const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
|
|
1670
|
+
applyNativeOptions(CREATE_OPTIONS, args, t);
|
|
1671
|
+
return t;
|
|
1672
|
+
}, (qi, t) => qi.create(t).then(shape), {
|
|
1673
|
+
build: (qi, t) => qi.buildCreate(t),
|
|
1674
|
+
reshape: shape,
|
|
1675
|
+
nested: (t) => hasNestedKeys(ctx, mm, t.data),
|
|
1676
|
+
});
|
|
1677
|
+
},
|
|
1495
1678
|
createMany: (args) => defer('createMany', args, () => {
|
|
1496
1679
|
const data = args.data;
|
|
1497
1680
|
const rows = Array.isArray(data)
|
|
@@ -1519,28 +1702,33 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1519
1702
|
nested: (t) => createManyRunsOf(t).length > 1,
|
|
1520
1703
|
execInTx: (table, t) => createManyByRun(table(mm.table), t, createManyRunsOf(t)),
|
|
1521
1704
|
}),
|
|
1522
|
-
update: (args) =>
|
|
1523
|
-
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
// and break on any model whose column is `@map`ped.
|
|
1532
|
-
if (isPlainObject(a.optimisticLock)) {
|
|
1533
|
-
t.optimisticLock = {
|
|
1534
|
-
...a.optimisticLock,
|
|
1535
|
-
field: renameField(mm, String(a.optimisticLock.field)),
|
|
1705
|
+
update: (args) => {
|
|
1706
|
+
let proj = NO_WRITE_PROJECTION;
|
|
1707
|
+
const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
|
|
1708
|
+
return defer('update', args, () => {
|
|
1709
|
+
const a = requireWhere(args, 'update');
|
|
1710
|
+
proj = resolveWriteProjection(ctx, mm, 'update', a);
|
|
1711
|
+
const t = {
|
|
1712
|
+
where: translateWhere(ctx, mm, a.where),
|
|
1713
|
+
data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
|
|
1536
1714
|
};
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1715
|
+
applyNativeOptions(UPDATE_OPTIONS, a, t);
|
|
1716
|
+
// `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
|
|
1717
|
+
// copied: a blind passthrough would send the Prisma spelling into core
|
|
1718
|
+
// and break on any model whose column is `@map`ped.
|
|
1719
|
+
if (isPlainObject(a.optimisticLock)) {
|
|
1720
|
+
t.optimisticLock = {
|
|
1721
|
+
...a.optimisticLock,
|
|
1722
|
+
field: renameField(mm, String(a.optimisticLock.field)),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
return t;
|
|
1726
|
+
}, (qi, t) => qi.update(t).then(shape), {
|
|
1727
|
+
build: (qi, t) => qi.buildUpdate(t),
|
|
1728
|
+
reshape: shape,
|
|
1729
|
+
nested: (t) => hasNestedKeys(ctx, mm, t.data),
|
|
1730
|
+
});
|
|
1731
|
+
},
|
|
1544
1732
|
updateMany: (args) => defer('updateMany', args, () => {
|
|
1545
1733
|
const a = args;
|
|
1546
1734
|
refuseRowLimit(modelName(ctx, mm), 'updateMany', a);
|
|
@@ -1553,15 +1741,20 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1553
1741
|
// row, and an explicit `allowFullTableScan: false` must not be able to
|
|
1554
1742
|
// turn that parity into a thrown empty-where guard.
|
|
1555
1743
|
if (a.where === undefined)
|
|
1556
|
-
t.allowFullTableScan =
|
|
1744
|
+
t.allowFullTableScan = UNSAFE;
|
|
1557
1745
|
return t;
|
|
1558
1746
|
}, (qi, t) => qi.updateMany(t), { build: (qi, t) => qi.buildUpdateMany(t), reshape: (raw) => raw }),
|
|
1559
|
-
delete: (args) =>
|
|
1560
|
-
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1747
|
+
delete: (args) => {
|
|
1748
|
+
let proj = NO_WRITE_PROJECTION;
|
|
1749
|
+
const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
|
|
1750
|
+
return defer('delete', args, () => {
|
|
1751
|
+
const a = requireWhere(args, 'delete');
|
|
1752
|
+
proj = resolveWriteProjection(ctx, mm, 'delete', a);
|
|
1753
|
+
const t = { where: translateWhere(ctx, mm, a.where) };
|
|
1754
|
+
applyNativeOptions(DELETE_OPTIONS, a, t);
|
|
1755
|
+
return t;
|
|
1756
|
+
}, (qi, t) => qi.delete(t).then(shape), { build: (qi, t) => qi.buildDelete(t), reshape: shape });
|
|
1757
|
+
},
|
|
1565
1758
|
deleteMany: (args = {}) => defer('deleteMany', args, () => {
|
|
1566
1759
|
const a = args;
|
|
1567
1760
|
refuseRowLimit(modelName(ctx, mm), 'deleteMany', a);
|
|
@@ -1569,40 +1762,42 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1569
1762
|
applyNativeOptions(DELETE_MANY_OPTIONS, a, t);
|
|
1570
1763
|
// LAST, so it wins. See the same note on updateMany.
|
|
1571
1764
|
if (a.where === undefined)
|
|
1572
|
-
t.allowFullTableScan =
|
|
1765
|
+
t.allowFullTableScan = UNSAFE;
|
|
1573
1766
|
return t;
|
|
1574
1767
|
}, (qi, t) => qi.deleteMany(t), { build: (qi, t) => qi.buildDeleteMany(t), reshape: (raw) => raw }),
|
|
1575
|
-
upsert: (args) =>
|
|
1576
|
-
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const row = await upsertLookupFirst(table(mm.table), t);
|
|
1593
|
-
return reshapeRow(ctx, mm, row);
|
|
1594
|
-
});
|
|
1595
|
-
}, {
|
|
1596
|
-
build: (qi, t) => qi.buildUpsert(t),
|
|
1597
|
-
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1598
|
-
nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
|
|
1599
|
-
execInTx: async (table, t) => {
|
|
1768
|
+
upsert: (args) => {
|
|
1769
|
+
let proj = NO_WRITE_PROJECTION;
|
|
1770
|
+
const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
|
|
1771
|
+
return defer('upsert', args, () => {
|
|
1772
|
+
const a = requireWhere(args, 'upsert');
|
|
1773
|
+
proj = resolveWriteProjection(ctx, mm, 'upsert', a);
|
|
1774
|
+
const t = {
|
|
1775
|
+
where: translateWhere(ctx, mm, a.where),
|
|
1776
|
+
create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
|
|
1777
|
+
update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
|
|
1778
|
+
};
|
|
1779
|
+
applyNativeOptions(UPSERT_OPTIONS, a, t);
|
|
1780
|
+
return t;
|
|
1781
|
+
}, (qi, t) => {
|
|
1782
|
+
// Native ON CONFLICT upsert is only Prisma-equivalent when the where
|
|
1783
|
+
// key values equal the create values AND no nested write data is
|
|
1784
|
+
// present; otherwise emulate Prisma's lookup-first atomically.
|
|
1600
1785
|
if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
|
|
1601
|
-
return
|
|
1786
|
+
return qi.upsert(t).then(shape);
|
|
1602
1787
|
}
|
|
1603
|
-
return
|
|
1604
|
-
},
|
|
1605
|
-
|
|
1788
|
+
return runInTx(async (table) => shape(await upsertLookupFirst(table(mm.table), t)));
|
|
1789
|
+
}, {
|
|
1790
|
+
build: (qi, t) => qi.buildUpsert(t),
|
|
1791
|
+
reshape: shape,
|
|
1792
|
+
nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
|
|
1793
|
+
execInTx: async (table, t) => {
|
|
1794
|
+
if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
|
|
1795
|
+
return shape(await table(mm.table).upsert(t));
|
|
1796
|
+
}
|
|
1797
|
+
return shape(await upsertLookupFirst(table(mm.table), t));
|
|
1798
|
+
},
|
|
1799
|
+
});
|
|
1800
|
+
},
|
|
1606
1801
|
count: (args = {}) => defer('count', args, () => {
|
|
1607
1802
|
const t = {};
|
|
1608
1803
|
applyNativeOptions(COUNT_OPTIONS, args, t);
|
|
@@ -18,7 +18,7 @@ import type { BuilderCtx } from './where.js';
|
|
|
18
18
|
* projection, and a value-returning aggregate is a projection by another name:
|
|
19
19
|
* `groupBy({ by: ['email'] })` emits one row per distinct plaintext email, and
|
|
20
20
|
* `_min`/`_max` return a stored cell verbatim. Both therefore REQUIRE the same
|
|
21
|
-
* `includePii
|
|
21
|
+
* `includePii` opt-in (the UNSAFE sentinel) that reads use.
|
|
22
22
|
*
|
|
23
23
|
* Deliberately NOT gated: `_count` (a count, never a value), `_sum` / `_avg`
|
|
24
24
|
* (a computed total across many rows, not a stored cell), and `where` /
|
package/dist/query/aggregates.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
12
12
|
import { snakeToCamel } from '../schema.js';
|
|
13
13
|
import { isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, } from './filters.js';
|
|
14
|
+
import { assertOrderDirection, resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
|
|
14
15
|
import { isTemporalInfinity, ownLookup } from './utils.js';
|
|
15
16
|
import * as whereMod from './where.js';
|
|
16
17
|
/**
|
|
@@ -19,7 +20,7 @@ import * as whereMod from './where.js';
|
|
|
19
20
|
* projection, and a value-returning aggregate is a projection by another name:
|
|
20
21
|
* `groupBy({ by: ['email'] })` emits one row per distinct plaintext email, and
|
|
21
22
|
* `_min`/`_max` return a stored cell verbatim. Both therefore REQUIRE the same
|
|
22
|
-
* `includePii
|
|
23
|
+
* `includePii` opt-in (the UNSAFE sentinel) that reads use.
|
|
23
24
|
*
|
|
24
25
|
* Deliberately NOT gated: `_count` (a count, never a value), `_sum` / `_avg`
|
|
25
26
|
* (a computed total across many rows, not a stored cell), and `where` /
|
|
@@ -37,7 +38,8 @@ export function assertAggregatePiiOptIn(table, meta, field, column, usage, inclu
|
|
|
37
38
|
return;
|
|
38
39
|
throw new ValidationError(`[turbine] ${usage} on column "${field}" of table "${table}" is refused: that column is ` +
|
|
39
40
|
'PII-tagged (`pii: true`), and this aggregate returns its stored values, which are excluded ' +
|
|
40
|
-
'from every default projection. Pass `includePii:
|
|
41
|
+
'from every default projection. Pass `includePii: UNSAFE` on this call to opt in ' +
|
|
42
|
+
"(import { UNSAFE } from 'turbine-orm'). " +
|
|
41
43
|
'`_count` over a PII column (a count, not a value) and `where` / `orderBy` / `having` on PII ' +
|
|
42
44
|
'columns need no opt-in.');
|
|
43
45
|
}
|
|
@@ -50,7 +52,11 @@ export function buildGroupBy(qi, args) {
|
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
qi.currentSkip = args.skipGlobalFilters;
|
|
55
|
+
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
56
|
+
// Resolve the PII opt-in ONCE, here, so the sentinel check runs on every
|
|
57
|
+
// groupBy (including one whose `by` names no PII column) rather than only on
|
|
58
|
+
// the paths that happen to consult it.
|
|
59
|
+
const includePii = resolveUnsafeFlag(args.includePii, 'includePii');
|
|
54
60
|
const gbWhere = whereMod.mergeGlobalFilter(qi, args.where);
|
|
55
61
|
const { sql: whereSql, params } = gbWhere
|
|
56
62
|
? whereMod.buildWhere(qi, gbWhere)
|
|
@@ -100,7 +106,7 @@ export function buildGroupBy(qi, args) {
|
|
|
100
106
|
for (const entry of args.by) {
|
|
101
107
|
if (typeof entry === 'string') {
|
|
102
108
|
const col = qi.toColumn(entry);
|
|
103
|
-
assertAggregatePiiOptIn(qi.table, meta, entry, col, 'groupBy `by` key',
|
|
109
|
+
assertAggregatePiiOptIn(qi.table, meta, entry, col, 'groupBy `by` key', includePii);
|
|
104
110
|
claimResultKey(entry, `column "${col}"`);
|
|
105
111
|
// The emitted output column is the snake_case name; claim it too (when
|
|
106
112
|
// it differs from the result key) so a JSON alias like 'created_at'
|
|
@@ -115,7 +121,7 @@ export function buildGroupBy(qi, args) {
|
|
|
115
121
|
}
|
|
116
122
|
else {
|
|
117
123
|
const col = resolveJsonPathTarget(qi, 'group key', entry.field, entry.path);
|
|
118
|
-
assertAggregatePiiOptIn(qi.table, meta, entry.field, col, 'groupBy JSON `by` key',
|
|
124
|
+
assertAggregatePiiOptIn(qi.table, meta, entry.field, col, 'groupBy JSON `by` key', includePii);
|
|
119
125
|
params.push(whereMod.jsonPathParam(qi, entry.path));
|
|
120
126
|
const extract = qi.dialect.buildJsonPathExtract(qi.q(col), qi.p(params.length));
|
|
121
127
|
const alias = entry.alias ?? String(entry.path[entry.path.length - 1]);
|
|
@@ -183,7 +189,7 @@ export function buildGroupBy(qi, args) {
|
|
|
183
189
|
if (target === true) {
|
|
184
190
|
const col = qi.toColumn(key);
|
|
185
191
|
if (aggKey === '_min' || aggKey === '_max') {
|
|
186
|
-
assertAggregatePiiOptIn(qi.table, meta, key, col, `groupBy ${aggKey}`,
|
|
192
|
+
assertAggregatePiiOptIn(qi.table, meta, key, col, `groupBy ${aggKey}`, includePii);
|
|
187
193
|
}
|
|
188
194
|
// Aggregate output aliases share the same output-name namespace as
|
|
189
195
|
// the group keys: `_sum: { totalPrice: true, total_price: {json} }`
|
|
@@ -197,7 +203,7 @@ export function buildGroupBy(qi, args) {
|
|
|
197
203
|
}
|
|
198
204
|
const col = resolveJsonPathTarget(qi, `${aggKey} target "${key}"`, target.field, target.path);
|
|
199
205
|
if (aggKey === '_min' || aggKey === '_max') {
|
|
200
|
-
assertAggregatePiiOptIn(qi.table, meta, target.field, col, `groupBy ${aggKey} JSON target`,
|
|
206
|
+
assertAggregatePiiOptIn(qi.table, meta, target.field, col, `groupBy ${aggKey} JSON target`, includePii);
|
|
201
207
|
}
|
|
202
208
|
const alwaysNumeric = aggKey === '_sum' || aggKey === '_avg';
|
|
203
209
|
if (alwaysNumeric && target.type === 'text') {
|
|
@@ -369,6 +375,9 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
369
375
|
throw new ValidationError(`[turbine] Cannot order groupBy by "_count" on table "${qi.table}": _count is not selected. ` +
|
|
370
376
|
`Orderable keys: ${validKeys()}.`);
|
|
371
377
|
}
|
|
378
|
+
// Refuse a direction that is neither asc nor desc BEFORE normalizeOrderBy,
|
|
379
|
+
// whose `=== 'desc' ? DESC : ASC` would silently sort ascending.
|
|
380
|
+
assertOrderDirection(value, `groupBy orderBy "_count" on table "${qi.table}"`);
|
|
372
381
|
const { dir, nulls } = normalizeOrderBy(value);
|
|
373
382
|
parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
374
383
|
continue;
|
|
@@ -386,6 +395,7 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
386
395
|
throw new ValidationError(`[turbine] Cannot order groupBy by "${key}.${field}" on table "${qi.table}": ` +
|
|
387
396
|
`that aggregate is not requested in this call. Orderable keys: ${validKeys()}.`);
|
|
388
397
|
}
|
|
398
|
+
assertOrderDirection(dirSpec, `groupBy orderBy "${key}.${field}" on table "${qi.table}"`);
|
|
389
399
|
const { dir, nulls } = normalizeOrderBy(dirSpec);
|
|
390
400
|
parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
391
401
|
}
|
|
@@ -397,6 +407,7 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
397
407
|
throw new ValidationError(`[turbine] Unknown field "${key}" in groupBy orderBy on table "${qi.table}". ` +
|
|
398
408
|
`Orderable keys: ${validKeys()}.`);
|
|
399
409
|
}
|
|
410
|
+
assertOrderDirection(value, `groupBy orderBy "${key}" on table "${qi.table}"`);
|
|
400
411
|
const { dir, nulls } = normalizeOrderBy(value);
|
|
401
412
|
parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
402
413
|
}
|
|
@@ -465,6 +476,7 @@ export function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
|
|
|
465
476
|
'sort specs, and JSON-path orderings only.');
|
|
466
477
|
}
|
|
467
478
|
const col = qi.resolveOrderByColumn(qi.table, qi.tableMeta, key);
|
|
479
|
+
assertOrderDirection(value, `groupBy distinctOn.orderBy "${key}" on table "${qi.table}"`);
|
|
468
480
|
const { dir, nulls } = normalizeOrderBy(value);
|
|
469
481
|
orderParts.push(`${qi.q(col)} ${dir}${qi.nullsSuffix(nulls)}`);
|
|
470
482
|
}
|
|
@@ -760,7 +772,9 @@ function temporalAggValue(qi, col, value) {
|
|
|
760
772
|
return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
|
|
761
773
|
}
|
|
762
774
|
export function buildAggregate(qi, args) {
|
|
763
|
-
qi.currentSkip = args.skipGlobalFilters;
|
|
775
|
+
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
776
|
+
// Resolved once, up front: see buildGroupBy.
|
|
777
|
+
const includePii = resolveUnsafeFlag(args.includePii, 'includePii');
|
|
764
778
|
const aggWhere = whereMod.mergeGlobalFilter(qi, args.where);
|
|
765
779
|
const { sql: whereSql, params } = aggWhere
|
|
766
780
|
? whereMod.buildWhere(qi, aggWhere)
|
|
@@ -831,7 +845,7 @@ export function buildAggregate(qi, args) {
|
|
|
831
845
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
832
846
|
if (enabled) {
|
|
833
847
|
const col = qi.toColumn(field);
|
|
834
|
-
assertAggregatePiiOptIn(qi.table, meta, field, col, 'aggregate _min',
|
|
848
|
+
assertAggregatePiiOptIn(qi.table, meta, field, col, 'aggregate _min', includePii);
|
|
835
849
|
selectExprs.push(`MIN(${qi.q(col)}) AS ${qi.q(`_min_${col}`)}`);
|
|
836
850
|
}
|
|
837
851
|
}
|
|
@@ -841,7 +855,7 @@ export function buildAggregate(qi, args) {
|
|
|
841
855
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
842
856
|
if (enabled) {
|
|
843
857
|
const col = qi.toColumn(field);
|
|
844
|
-
assertAggregatePiiOptIn(qi.table, meta, field, col, 'aggregate _max',
|
|
858
|
+
assertAggregatePiiOptIn(qi.table, meta, field, col, 'aggregate _max', includePii);
|
|
845
859
|
selectExprs.push(`MAX(${qi.q(col)}) AS ${qi.q(`_max_${col}`)}`);
|
|
846
860
|
}
|
|
847
861
|
}
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
import type pg from 'pg';
|
|
57
57
|
import { type RelationDef, type SchemaMetadata, type TableMetadata } from '../schema.js';
|
|
58
58
|
import type { ReselectExecutor } from './builder.js';
|
|
59
|
-
import type { SkipGlobalFilters, WithClause, WithCount } from './types.js';
|
|
59
|
+
import type { SkipGlobalFilters, Unsafe, WithClause, WithCount } from './types.js';
|
|
60
60
|
/**
|
|
61
61
|
* A DeferredQuery, minimally typed for what the loader consumes. Kept local to
|
|
62
62
|
* avoid a value import of builder.ts (which imports this module).
|
|
@@ -104,9 +104,14 @@ export interface RelationLoadContext {
|
|
|
104
104
|
/**
|
|
105
105
|
* The query's `includePii` opt-in, threaded onto every child `buildFindMany`
|
|
106
106
|
* so a batched relation load excludes (or includes) PII-tagged columns exactly
|
|
107
|
-
* as the join strategy does at every nested level.
|
|
107
|
+
* as the join strategy does at every nested level. Absent by default.
|
|
108
|
+
*
|
|
109
|
+
* Typed as the SENTINEL, not a boolean, and that is a contract not a style
|
|
110
|
+
* choice: this value is copied verbatim onto child `FindManyArgs`, where a
|
|
111
|
+
* plain `true` is refused as a privilege escalation. A boolean here would
|
|
112
|
+
* type-check and then throw on the first relation follow-up.
|
|
108
113
|
*/
|
|
109
|
-
includePii?:
|
|
114
|
+
includePii?: Unsafe;
|
|
110
115
|
/**
|
|
111
116
|
* Render `table`'s global filter against `alias` for a raw follow-up query
|
|
112
117
|
* (the batched `_count`), numbering its `$n` placeholders AFTER
|
|
@@ -128,7 +133,7 @@ export interface RelationLoadContext {
|
|
|
128
133
|
* keep the `select: undefined, omit: undefined` fast path and the emitted SQL
|
|
129
134
|
* stays byte-identical.
|
|
130
135
|
*/
|
|
131
|
-
export declare function defaultProjectionFields(meta: TableMetadata, includePii: boolean | undefined): {
|
|
136
|
+
export declare function defaultProjectionFields(meta: TableMetadata, includePii: boolean | Unsafe | undefined): {
|
|
132
137
|
hidden: ReadonlySet<string>;
|
|
133
138
|
visible: string[];
|
|
134
139
|
} | undefined;
|
|
@@ -77,7 +77,10 @@ const MAX_DEPTH = 10;
|
|
|
77
77
|
* keep the `select: undefined, omit: undefined` fast path and the emitted SQL
|
|
78
78
|
* stays byte-identical.
|
|
79
79
|
*/
|
|
80
|
-
export function defaultProjectionFields(meta,
|
|
80
|
+
export function defaultProjectionFields(meta,
|
|
81
|
+
// Either the already-resolved boolean (top-level callers) or the raw sentinel
|
|
82
|
+
// (the relation-load context); both are only ever tested for truthiness.
|
|
83
|
+
includePii) {
|
|
81
84
|
if (includePii)
|
|
82
85
|
return undefined;
|
|
83
86
|
const hidden = new Set();
|