A guard shape looks like a description of a query. It is closer to a description of who decides what. Every leaf in it answers one question — does the client choose this value, or does the server?
{ status: true } // the client chooses
{ status: 'published' } // the server chose, at deploy time
{ status: force('published') } // same, spelled so a literal `true` is unambiguous
{ status: (base) => base.max(20) } // the client chooses, within a rule
That is the whole vocabulary. The interesting part is what “the server chose” means at runtime: two different things, depending on where in the shape the forced value sits — and one of them is not what the documentation says.
Examples use an event-ticketing schema:
/// @scope-root
model Organizer {
id String @id @default(cuid())
name String
events Event[]
}
model Event {
id String @id @default(cuid())
title String
venue String
startsAt DateTime
isPublished Boolean @default(false)
isCancelled Boolean @default(false)
organizerId String
organizer Organizer @relation(fields: [organizerId], references: [id])
tickets Ticket[]
}
model Ticket {
id String @id @default(cuid())
priceCents Int
tier String
eventId String
event Event @relation(fields: [eventId], references: [id])
}
Behavior here was run against prisma-guard 1.33.0, zod 4.4.3, Prisma 6.19.3. Every claim is reproducible; the harness is at the end.
1. The four value types
| Written as | Meaning | Client may send it? |
|---|---|---|
true |
client-controlled; @zod chains from the schema apply |
yes |
a bare literal ('published', false, 42) |
forced by the server | depends — §3 |
force(v) |
forced by the server | depends — §3 |
(base) => schema |
client-controlled, with an inline refine replacing @zod |
yes |
force() exists because true is already the sentinel for “client-controlled”, so a shape has no other way to pin a boolean to true. On any other value it is legal but redundant — force('published') and 'published' behave identically. Use it anyway on security-relevant fields, so a reader need not remember which literals are special.
Rule. In a shape, true is never a value. It is a permission.
2. Forced values must live inside an operator
In a where shape, a forced value belongs to an operator, not to the field:
// wrong
where: { isPublished: force(true) }
// right
where: { isPublished: { equals: force(true) } }
The wrong form fails at shape construction:
Operator "value" not supported for type "Boolean"
The message names an operator you never wrote — the guard is treating the wrapper as an operator object — which makes it hard to place. In data shapes the field-level form is correct (data: { isPublished: force(false) }), so the two halves of a config are not symmetric.
Rule. where forces an operator. data forces a field.
3. Where the forced value sits decides whether the client may send it
Among shapes that build, one lenient position and four strict ones. A fifth case does not reach client validation at all: forced conditions under a negative relation operator are rejected when the shape is constructed — §7 covers it.
Lenient: a top-level where field whose predicate is entirely forced
findMany: {
shape: {
default: {
where: { isPublished: { equals: force(true) }, title: { contains: true } },
take: { max: 50, default: 20 },
},
},
}
The field stays in the client-facing schema. Whatever the client sends for it is discarded:
| Client sends | Emitted args |
|---|---|
| nothing | {"take":20,"where":{"isPublished":{"equals":true}}} |
isPublished: { equals: true } |
{"take":20,"where":{"isPublished":{"equals":true}}} |
isPublished: { equals: false } |
{"take":20,"where":{"isPublished":{"equals":true}}} |
isPublished: { not: false } — a different operator |
{"take":20,"where":{"isPublished":{"equals":true}}} |
(take: 20 is the shape’s default, not something the client sent.)
No error in any row, and the same holds for strings and numbers: a shape forcing title: { contains: 'gala' } against a client sending contains: 'anything' emits contains: "gala".
Strict: a forced modifier beside a client-controlled operator
where: { title: { contains: true, mode: 'insensitive' } }
Here contains is the client’s and mode is the server’s, in the same object. The forced key is removed from the client-facing schema, so a client that sends it is rejected:
Invalid query on model "Event": where.title: Unrecognized key(s): mode
Note the flip side: with mode: true instead, a client that forgets mode silently gets case-sensitive search. Neither option is a safe default. Force it when case-insensitivity is part of the endpoint’s contract and no caller should be able to opt out — then make sure no client sends mode, because doing so is a 400. Open it when the caller genuinely chooses, and accept that a caller which omits it gets exact matching.
Strict: anything inside a relation filter
where: { organizer: { is: { id: { equals: force(ctx.organizerId) } } } }
where: { tickets: { some: { tier: { equals: force('vip') } } } }
Invalid query on model "Event": where.organizer.is.id: Unrecognized key(s): equals
Invalid query on model "Event": where.tickets.some.tier: Unrecognized key(s): equals
Rejected whether the client sends the right value or a foreign one. Tenant scoping usually lives here, which is why teams who scope through a relation come away believing forced values are always strict.
Strict: anything in a nested include
include: {
events: {
where: { isCancelled: { equals: force(false) }, title: { contains: true } },
take: { max: 20, default: 10 },
},
}
Invalid query on model "Organizer": include.events.where.isCancelled: Unrecognized key(s): equals
The client may still filter the relation on the fields you opened, and its filter is AND-wrapped with yours:
{ "include": { "events": { "where": { "AND": [ { "title": { "contains": "gala" } },
{ "isCancelled": { "equals": false } } ] }, "take": 10 } } }
Nested bounds hold too — a client sending take: 500 inside the include gets include.events.take: Number must be <= 20.
Strict: anything in a data shape
create: {
shape: {
default: {
data: { title: true, venue: true, startsAt: true, isPublished: force(false) },
},
},
}
Invalid data for create on model "Event": Unrecognized key(s): isPublished
(title, venue and startsAt are required with no default, so a shape omitting them fails §6’s completeness check before any client input is read. organizerId is absent because the scope extension injects it.)
Upsert splits this across its two halves and says which one it means:
Invalid data for upsert (create) on model "Event": Unrecognized key(s): isPublished
The rule, stated once
A wholly-forced scalar predicate at the top level of
whereis merged — the client may send the field and its input is silently discarded. Forced values anywhere else — a modifier beside a client operator, a relation-filter member, a nestedinclude’swhere, adatafield — are removed from the client-facing schema, and sending them is a 400.
There is exactly one lenient position, and it is the one people reach for first when adding a filter to a list endpoint.
Every read case above ran through both entry points — guard.query(...).parse(body) and prisma.event.guard(shape).findMany(body) — and they agree. The data-shape row is delegate-only, since guard.query() accepts read methods only. Four cases elsewhere do not agree, and all are findings rather than defects: three are projection auto-apply, two of those on nested projections, and the fourth is a mutation shape passed to guard.query(). All of them are §8.
What to do with this. Decide, per shape, which behavior you are relying on, and write the test that proves that one.
If your scope is a top-level forced predicate, one specific test shape does not prove it: sending a foreign value and then asserting only that the request succeeded, or asserting against fixtures where both outcomes look alike — an empty second tenant, or one whose rows you never check for. Remove the forcing and that assertion still holds, so it was never testing the forcing.
Make it discriminating and it does bite: with a second tenant that actually has rows, dropping the force sends the client’s foreign id through to Prisma and the foreign row comes back. That is a real isolation test. It costs a database, a server and a seeded dataset per case, and it proves a different thing than an args assertion — outcome-level isolation for this request, rather than the shape of the query the guard emitted. Asserting on the emitted args inspects the mechanism directly and needs neither.
4. The documentation says conflicting client values throw. They do not.
The prisma-guard 1.33.0 README, under “Forced where merge strategy”, says:
Inline merge — if a forced field’s value is a plain operator object and the client also provided an operator object for the same field, the forced operator keys are merged into the client’s operator object. […] Conflicts on the same op key (different values) throw
ShapeError.
Read straight, that sentence promises rejection: force organizerId to org_1, have a client send org_2, get a 400. Tested on four field types, through both entry points:
| Shape | Client sends | Documented | Actual |
|---|---|---|---|
isPublished: { equals: force(true) } |
equals: false |
ShapeError |
{"isPublished":{"equals":true}} |
title: { contains: 'gala' } |
contains: 'anything' |
ShapeError |
{"title":{"contains":"gala"}} |
priceCents: { gte: 100 } |
gte: 5 |
ShapeError |
{"priceCents":{"gte":100}} |
organizerId: { equals: force('org_1') } |
equals: 'org_2' |
ShapeError |
{"organizerId":{"equals":"org_1"}} |
No error in any case. The forced value wins silently.
The check does exist — it runs somewhere else. A conflict inside the shape config, between a top-level force and a force in a combinator, throws at construction:
Conflicting forced where values for "isPublished.equals": shape defines both true and false
So: conflict detection applies to your configuration, not to client input. Client input never competes with a forced value, so it can never conflict with one.
Why this matters more than a doc nit. Someone who reads that line and writes
// this test passes for the wrong reason
await expect(request(app).get('/event').query({ where: { organizerId: { equals: 'org_2' } } }))
.rejects.toThrow()
The request succeeds and returns correctly-scoped rows, so the assertion fails, so the developer “fixes” it by asserting a 200 and moves on believing the scope was tested. It was not.
Worth reporting upstream: either the runtime should reject a conflicting client value, or that README sentence should say the check is shape-only. Until then, treat the runtime as the specification.
5. Where a forced condition lands in the emitted query
Two merge strategies, and which one you get depends on the request, not the shape.
AND-wrap — the client filtered a different field:
{ "where": { "AND": [ { "title": { "contains": "gala" } }, { "isPublished": { "equals": true } } ] }, "take": 20 }
Inline merge — the client touched the same field:
{ "take": 20, "where": { "isPublished": { "equals": true } } }
So the obvious assertion is unstable — expect(args.where.isPublished) is undefined whenever the client filtered anything else. Write the helper once:
export function findPredicate(where: any, field: string): any {
if (!where || typeof where !== 'object') return undefined
if (field in where) return where[field]
for (const key of ['AND', 'OR', 'NOT']) {
const branch = where[key]
if (!branch) continue
for (const entry of Array.isArray(branch) ? branch : [branch]) {
const found = findPredicate(entry, field)
if (found !== undefined) return found
}
}
return undefined
}
expect(findPredicate(args.where, 'isPublished')).toEqual({ equals: true })
Combinators. A forced value written inside AND or OR is lifted out and applied as a top-level AND constraint. Note where the force() sits below: a member of the OR, not a sibling of it.
where: {
OR: {
title: { contains: true },
venue: { contains: true },
isPublished: { equals: force(true) },
},
}
A client sending two OR branches gets them AND-ed with the forced condition, not OR-ed:
{"where":{"AND":[{"OR":[{"title":{"contains":"gala"}},{"venue":{"contains":"hall"}}]},{"isPublished":{"equals":true}}]}}
The proof that it left the combinator is an empty request. An OR branch would make that match anything in the disjunction; instead no OR survives:
{"where":{"isPublished":{"equals":true}}}
Forced values restrict, never broaden. So a genuinely disjunctive rule — “events I organize or events that are public” — cannot be expressed with forced values: lifting turns it into a conjunction that matches almost nothing.
The rule has to live somewhere the guard is not doing the narrowing. Three choices, in the order worth trying them: model the rule as a single scoping column, so the disjunction collapses to an equality the shape can force; write a purpose-built handler that composes and issues the query itself, with the disjunction authored server-side; or enforce the policy in the database. The middle option is the one to be careful with — writing the handler is what enforces the rule, and it is now your code’s job rather than the guard’s.
NOT is the exception to the merge, and it changes shape
A forced NOT and a client NOT are kept as separate branches rather than merged, so Prisma’s NOT semantics survive. What the README does not say is that the emitted form changes depending on whether the client sent one.
With a shape whose NOT declares one forced key and one client-controlled key:
where: { NOT: { isCancelled: { equals: force(true) }, title: { contains: true } }, venue: { contains: true } }
a client that sends nothing gets an object:
{"where":{"NOT":{"isCancelled":{"equals":true}}}}
and a client that sends its own NOT gets an array of two branches — the client’s first, the forced one second:
{"where":{"NOT":[{"title":{"contains":"draft"}},{"isCancelled":{"equals":true}}]}}
The client’s object form and array form both arrive there; NOT: { title: ... } and NOT: [{ title: ... }] emit the same thing. This is the same assertion trap as AND-wrap versus inline merge, one level deeper: a test reading where.NOT.isCancelled passes for as long as no client sends a NOT, and breaks the first time one does — while the guard is behaving correctly. Walk it.
A client NOT is only accepted at all because the shape’s NOT declares a client-controlled key inside it. Against a wholly-forced NOT, the strict rule applies as everywhere else:
Invalid query on model "Event": where.NOT: No matching variant (branch 1: [Unrecognized key(s): venue] | branch 2: [Expected array])
6. Forced values and validation are different layers
A forced value is still validated against the field’s schema. @zod directives and inline refines apply to client-controlled fields; forcing skips the question of who supplies the value, not the question of whether the value is legal.
create: {
shape: {
default: {
data: {
title: true, // @zod chains from the schema apply
venue: (base) => base.max(120), // inline refine replaces @zod for this field
startsAt: true,
isPublished: force(false),
isCancelled: force(false),
},
},
},
}
Failures are reported per field, and the message names the model and operation:
Invalid data for create on model "Event": venue: String must contain at most 120 character(s)
startsAt is listed for a reason that has nothing to do with forcing: a create shape has to be complete. If a required field has no client entry, no schema default, no scope FK injection, no relation write and no @zod .default(...) or .catch(...) directive, the shape is rejected at construction — before any request is examined — with a message that lists the escape hatches:
Required field "venue" on model "Event" is missing from create data shape, has no default,
is not a scope FK, and is not covered by a relation write in the shape
That is a shape bug, not a request bug — it fires before any client is involved.
Forced fields are applied on update too, not only on create. A shape of data: { title: true, isCancelled: force(true) } against a client body of { title: 'x' } emits:
{ "data": { "title": "x", "isCancelled": true }, "where": { "id": "e1" } }
A forced value in an update shape is not a guard rail, it is a side effect: every call through that variant sets the field whether the caller meant to or not. Right for updatedBy, wrong for anything a user would be surprised to have changed. To merely prevent a field being written, leave it out of the shape — omission already denies it.
Upsert forces per half. create and update are separate shapes with separate forced values:
upsert: {
shape: {
where: { id: true },
create: { title: true, venue: true, startsAt: true, isPublished: force(false), isCancelled: force(false) },
update: { title: true },
},
}
A new row is always unpublished; an existing row’s isPublished is untouched because the update half omits it. Forcing in update as well silently unpublishes every event anyone edits.
7. Where forced values do not reach
Four boundaries worth knowing before you rely on one. Three of them are visible in emitted args, so they are shown that way rather than asserted.
Nested reads, to-many. A relation loaded through include/select is not tenant-filtered by the scope extension. For a to-many relation the mitigation is a forced where inside the include shape, and it does what you would hope — the guarded delegate emits it whether or not the client asked:
{"include":{"events":{"where":{"isPublished":{"equals":true}}}}}
It is also a strict position, so the client cannot supply that field itself:
Invalid query on model "Organizer": include.events.where.isPublished: Unrecognized key(s): equals
Nested reads, to-one. Here the mitigation does not exist, and the guard says so outright rather than ignoring the attempt:
Relation "event" on model "Ticket" is to-one. Only "select" and "include" are supported for to-one nested reads, not where/orderBy/cursor/take/skip.
A to-one relation is reached through the foreign key on the row you already returned, so there is no collection to filter — which row comes back is fixed by that key. That is not the same as the access decision being settled: returning the row is still a separate projection, and still a separate authorization decision. What the missing where removes is your ability to make that decision in the shape. The options are to omit the relation, restrict its scalars with a nested select, or enforce the constraint in the database. This message is a ShapeError at 400 and comes from the shape, not the request: a shape written this way fails the first time it is built, not on some later unlucky request.
Relation filters are not lifted — and none is refused outright. A forced value inside some stays inside some; it is not pulled to the top level the way a combinator member is:
{"where":{"tickets":{"some":{"tier":{"equals":"vip"}}}}}
When the client also filters inside that relation, the AND-wrap happens at the relation level, not the top:
{"where":{"tickets":{"some":{"AND":[{"priceCents":{"gte":100}},{"tier":{"equals":"vip"}}]}}}}
That is the behavior you want: a relation-scoped rule stays attached to the relation it scopes.
none behaves differently, and this is the second place in this article where the runtime does not match its own message. A forced condition under none is rejected at shape construction:
Relation filter "tickets.none" on model "Event" mixes client-controlled and forced conditions. Under negative relation operators (none, isNot), merging weakens the filter. Either move forced conditions to a separate top-level "none" branch, or make all conditions under this operator client-controlled or all forced.
The reasoning is sound — under a negation, AND-ing an extra condition into the client’s filter makes the exclusion narrower, which quietly weakens the rule the forced value was there to enforce. But the message describes a mixture, and the check fires without one. A shape whose none contains a single forced condition and no client-controlled key anywhere in it throws the same error, and it throws with an empty request body, so this is shape construction rather than validation. The final clause of the message — “or make all conditions under this operator client-controlled or all forced” — describes a shape that 1.33.0 does not actually accept.
The same forced conditions under some and every build and run normally, which is what tells you the check is specific to negative operators rather than to your shape. The cleanest demonstration is the to-one pair — same relation, same forced condition, opposite operator:
where: { organizer: { is: { name: { equals: force('Acme') } } } } // builds
where: { organizer: { isNot: { name: { equals: force('Acme') } } } } // ShapeError
Relation filter "organizer.isNot" on model "Event" mixes client-controlled and forced conditions. Under negative relation operators (none, isNot), merging weakens the filter. Either move forced conditions to a separate top-level "isNot" branch, or make all conditions under this operator client-controlled or all forced.
A wholly client-controlled isNot builds and runs. So the trigger is not negation, and not mixing — it is a forced condition appearing anywhere under a negative operator. On a to-many relation isNot is not available in the first place:
Operator "isNot" not supported for to-many relation "tickets". Allowed: some, every, none
What to do. Put the forced condition where it will not be negated: a separate top-level predicate, or some/every phrased positively. If the rule is genuinely “no ticket of this tier”, 1.33.0 will not express it declaratively at all — the message’s own suggested remedy, an all-forced block under the operator, is the case that throws. Check this against your own version before designing around it; of everything in this article it is the likeliest to have moved.
Relation writes. Forced values do not describe a connect. The shape configures which relation operations are permitted and what identifies the target:
// wrong
data: { tickets: { connect: true } }
// -> connect config on "Event.tickets" must be an object of unique selectors
// right
data: { tickets: { connect: { id: true } } }
Restricting to connect/disconnect means a client can only reference existing rows — but which rows is not something force() can pin. Ownership of the connected id is a hook’s job.
Scope roots. The @scope-root model is not scoped to itself, and the difference is plain in what the extension injects. A scoped model gets the tenant filter:
{"where":{"organizerId":"org_1"}}
The scope root gets nothing at all — an unfiltered findMany on Organizer stays unfiltered, and a client where passes through untouched:
{}
{"where":{"name":{"contains":"a"}}}
Which is correct: there is no column on Organizer that says which organizer it belongs to. It is also the boundary people are most surprised by, because models that do have a generated scope mapping are filtered automatically and the difference is invisible at the call site. Check the generated SCOPE_MAP rather than assuming. Across the two schemas in the lab it maps three models and no others, which the harness records rather than leaving to the article:
MAP Plant -> [{"fk":"nurseryId","root":"Nursery","relationName":"nursery"}]
MAP Order -> [{"fk":"nurseryId","root":"Nursery","relationName":"nursery"}]
MAP Event -> [{"fk":"organizerId","root":"Organizer","relationName":"organizer"}]
MAP Ticket -> (no scope mapping — unscoped, queries on it are not filtered)
MAP OrderItem -> (no scope mapping — unscoped, queries on it are not filtered)
MAP Customer -> (no scope mapping — unscoped, queries on it are not filtered)
MAP Nursery -> (no scope mapping — unscoped, queries on it are not filtered)
MAP Organizer -> (no scope mapping — unscoped, queries on it are not filtered)
Ticket, OrderItem, and Customer are reached through a scoped parent but carry no mapping of their own, so a query that starts at one of them is unscoped — as is any query on a root. The scope extension adds no filter in those cases. Protect the operation with an explicit route shape, application authorization, or a database policy.
Raw SQL. $queryRaw and $executeRaw are not intercepted by anything in this article. That one is from the prisma-guard README, not the harness — the lab asserts on arguments passed to a fake delegate, and raw SQL never reaches it.
8. Testing a shape without a server
Shapes are data. You can assert on the exact arguments that would have reached Prisma with no HTTP, no database, and no fixtures.
import { guard } from './generated/guard/client'
const args = guard
.query('Event', 'findMany', { '/public/events': publicEventsShape })
.parse(body, { caller: '/public/events' })
expect(findPredicate(args.where, 'isPublished')).toEqual({ equals: true })
expect(args.take).toBeLessThanOrEqual(50)
Three things it catches cheaply: a forced value removed during a refactor, a take bound raised past what the page needs, a new field added to a shared select. Two caveats before building a suite around it.
parse() is reads-only. A mutation shape passed to guard.query() produces Caller key "where" collides with reserved shape config key — the top-level keys get read as variant names. Mutations go through the guarded delegate.
parse() does not show projection. The shape’s select is applied when the query executes:
guard.query('Event', 'findMany', { select: { id: true, title: true }, take: { max: 10 } }).parse({})
// {}
prisma.event.guard({ select: { id: true, title: true }, take: { max: 10 } }).findMany({})
// { select: { id: true, title: true } }
Assert where and take through parse(); assert projection through the delegate. A test suite that checks projection via parse() reports a missing select that is not missing.
Rules
- In a shape,
trueis a permission, not a value. force()is only required for a literaltrue; use it anyway on security-relevant fields for readability.- In
where, force an operator ({ equals: force(x) }); indata, force the field.where: { f: force(x) }fails withOperator "value" not supported. - A wholly-forced predicate at the top level of
whereaccepts client input and discards it silently — this is the only lenient position. - Forced values anywhere else — a modifier beside a client operator, a relation-filter member, a nested
include’swhere, adatafield — are removed, and sending them is a 400. - Do not trust the README line promising
ShapeErroron conflicting client values; the runtime silently overrides. Conflict detection is shape-only. - Against a top-level forced predicate, sending a foreign value and checking only that the request succeeded — or checking a result that looks the same either way — proves nothing. Assert on emitted args with a recursive helper (AND-wrap versus inline merge depends on the request body), or use fixtures that make the outcome discriminating.
- Forced values inside
AND/ORlift to top-level AND, so a disjunctive scope cannot be declarative; nest the force inside the combinator or you are testing a different shape. - Forced
NOTstays its own branch, and the emitted form flips from object to array the moment a client sends its ownNOT— walk it, never index it. - Forced values are still validated; forcing decides who supplies a value, not whether it is legal.
guard.query()is reads-only, and the guarded delegate is the only place projection auto-apply appears. - A create shape must be complete: client field, schema default, scope FK, relation write, or a
@zod .default(...)/.catch(...)directive. - A forced field in an
updateshape writes on every call through that variant — to merely deny a field, omit it instead. Upsert forces per half. - Forced conditions inside
some/everystay at the relation level and AND-wrap there, not at the top. Undernoneand to-oneisNotthey are refused at construction — the message says “mixes” but fires with nothing mixed, so treat negative relation operators as closed to forcing in 1.33.0. - Scope does not descend into nested reads: a forced
wherein the include shape is the only shape-level filtering mitigation, and only for to-many relations — a to-one relation takesselect/includeand nothing else, so omit it, restrict its scalars, or constrain it in the database. - Forced values do not reach relation-write targets, raw SQL, or the scope root, which is never scoped to itself.
Appendix: reproducing every claim here
import { force } from 'prisma-guard'
import { guard } from './generated/guard/client'
const shape = {
where: { isPublished: { equals: force(true) }, title: { contains: true } },
take: { max: 50, default: 20 },
}
console.log(JSON.stringify(guard.query('Event', 'findMany', shape).parse({
where: { isPublished: { equals: false } },
})))
// {"where":{"isPublished":{"equals":true}},"take":20}
For the delegate side — mutations, projection, and the second half of every position case — you do not need a Prisma client. guard(input, caller) resolves its delegate from this.$parent, so any object with the right method names works:
const ext = guard.extension(() => ({ Organizer: 'org_1' }))
const fake = { findMany: async (args: unknown) => args, create: async (args: unknown) => args }
const guarded = (ext.model as any).event.guard.call({ $parent: { event: fake } }, shape)
console.log(await guarded.findMany({ where: { isPublished: { equals: false } } }))
Run both for every read case. Agreement means you have a behavior rather than an artefact of one code path; disagreement is worth writing down, and this article contains four of those — three projection auto-apply differences, two of them on nested projections, plus one mutation shape passed to the read-only entry point. Mutations have only the delegate, so there is nothing to compare — label those cases rather than quietly counting them as agreeing.
The §7 scope claims need one more primitive. Scope injection happens in the extension’s query layer rather than in the shape, so neither parse() nor the guarded delegate shows it — call $allOperations directly and let the query callback hand the args straight back:
const scoped = (model: string, operation: string, args: unknown) =>
(ext.query as any).$allOperations({ model, operation, args, query: async (a: unknown) => a })
console.log(await scoped('Event', 'findMany', {})) // {"where":{"organizerId":"org_1"}}
console.log(await scoped('Organizer', 'findMany', {})) // {}
Three primitives, then: parse(), a fake delegate, and $allOperations. Between them they cover every claim in this article except raw SQL, which by definition never reaches any of them.