From 0ea9a1f94bb8f3b8060e5ab29162f0c0b448b0fb Mon Sep 17 00:00:00 2001 From: Jem Gillam <6413628+jemgillam@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:06:57 +0100 Subject: [PATCH 1/2] Edits for inhibition explanations --- grafast/website/grafast/flow.mdx | 18 ++++++++++++---- .../grafast/plan-resolvers/best-practices.md | 6 +++--- .../grafast/standard-steps/inhibitIf.mdx | 14 +++++++++++-- .../grafast/standard-steps/inhibitOnEmpty.mdx | 9 ++++++++ .../grafast/standard-steps/inhibitOnNull.mdx | 21 +++++++++++++------ 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/grafast/website/grafast/flow.mdx b/grafast/website/grafast/flow.mdx index 55eb47d796..7d34700180 100644 --- a/grafast/website/grafast/flow.mdx +++ b/grafast/website/grafast/flow.mdx @@ -265,6 +265,15 @@ object identifiers ("Node IDs"), optional foreign keys, or other advanced scenarios where you need to suppress downstream work or turn those suppressions back into useful data. +:::danger[Inhibition is not a security boundary] + +Inhibition enables Gra*fast* to optimize away work that is unnecessary for a +particular result. It does not guarantee that the work will not execute. Never +use inhibition for authorization or to prevent security-sensitive operations; +enforce those requirements inside the protected operation or data source. + +::: + If you do reach for them, a common sequence is to guard an input, inhibit downstream work when that guard fails, and optionally trap the inhibition later so the field can return a benign value: @@ -327,8 +336,8 @@ flowchart TD /> Here `inhibitOnNull` marks only the `null` entries as inhibited, so `loadOne` -never attempts to fetch those users while the rest of the batch proceeds as -normal. Other helpers build on the same idea: +can avoid fetching those users while the rest of the batch proceeds as normal. +Other helpers build on the same idea: - [`inhibitOnNull`](./standard-steps/inhibitOnNull.mdx) skips dependent work for `null` inputs while still returning `null` to the caller. @@ -346,5 +355,6 @@ should not depend on the specific step class. In plan diagrams the step is usually absorbed into the dependency edge, so you will see labels such as `rejectNull`, `trapError`, or `onReject="…"` rather than a dedicated node. Crucially, Gra*fast* applies these flags per entry: if one item in a batch is -inhibited or errored it is simply omitted from the `execute()` call while the -rest of the items carry on unhindered. +inhibited or errored, Gra*fast* can omit it from the `execute()` call while the +rest of the items carry on unhindered. This is an optimization and must not be +relied on for correctness or security. diff --git a/grafast/website/grafast/plan-resolvers/best-practices.md b/grafast/website/grafast/plan-resolvers/best-practices.md index 524059071e..9daaf5cf97 100644 --- a/grafast/website/grafast/plan-resolvers/best-practices.md +++ b/grafast/website/grafast/plan-resolvers/best-practices.md @@ -348,7 +348,7 @@ import { loadOne, trap, inhibitOnNull, TRAP_ERROR } from "grafast"; function post_author_plan($post) { const $authorId = $post.get("authorId"); - // Guard against null authorId — skip the load entirely + // Allow Gra*fast* to skip the load when authorId is null const $guardedId = inhibitOnNull($authorId); // Load the author; if it errors, convert to null @@ -359,8 +359,8 @@ function post_author_plan($post) { The key flow control steps are: -- [`inhibitOnNull()`](../standard-steps/inhibitOnNull.mdx) — suppresses - downstream work when a value is `null` +- [`inhibitOnNull()`](../standard-steps/inhibitOnNull.mdx) — enables + Gra*fast* to suppress downstream work when a value is `null` - [`assertNotNull()`](../standard-steps/assertNotNull.mdx) — turns `null` into a `SafeError` visible to clients - [`trap()`](../standard-steps/trap.mdx) — recovers inhibited or errored diff --git a/grafast/website/grafast/standard-steps/inhibitIf.mdx b/grafast/website/grafast/standard-steps/inhibitIf.mdx index a163218739..c2c989ef58 100644 --- a/grafast/website/grafast/standard-steps/inhibitIf.mdx +++ b/grafast/website/grafast/standard-steps/inhibitIf.mdx @@ -5,6 +5,15 @@ import Mermaid from "@theme/Mermaid"; Returns a step that yields the same values as the passed in step, but downstream work is inhibited whenever the condition step yields `true`. +:::danger[Not a security boundary] + +`inhibitIf` enables Gra*fast* to optimize away unnecessary downstream work; it +does not guarantee that the work will not execute. Do not use it for +authorization or to prevent security-sensitive operations. Enforce those +requirements inside the protected operation or data source. + +::: + Unlike [`inhibitOnNull`](./inhibitOnNull.mdx), `inhibitIf` is an explicit step in the plan, so it will typically appear in plan diagrams as its own node. @@ -29,8 +38,9 @@ return trap($result, TRAP_INHIBITED, { ``` In the example above, when `$isEmpty` yields `true`, the dependent work is -inhibited and Gra*fast* omits the affected entries from downstream execution. -When the condition yields `false`, the wrapped step behaves as a pass-through. +inhibited and Gra*fast* can omit the affected entries from downstream +execution. When the condition yields `false`, the wrapped step behaves as a +pass-through. ## Plan diagrams diff --git a/grafast/website/grafast/standard-steps/inhibitOnEmpty.mdx b/grafast/website/grafast/standard-steps/inhibitOnEmpty.mdx index f2eac23596..6bf75e5f23 100644 --- a/grafast/website/grafast/standard-steps/inhibitOnEmpty.mdx +++ b/grafast/website/grafast/standard-steps/inhibitOnEmpty.mdx @@ -3,6 +3,15 @@ Returns a step that yields the same values as the passed in step, but downstream work is inhibited when the yielded value is considered empty. +:::danger[Not a security boundary] + +`inhibitOnEmpty` enables Gra*fast* to optimize away unnecessary downstream +work; it does not guarantee that the work will not execute. Do not use it for +authorization or to prevent security-sensitive operations. Enforce those +requirements inside the protected operation or data source. + +::: + Gra*fast* currently considers a value empty if it is: - `undefined` diff --git a/grafast/website/grafast/standard-steps/inhibitOnNull.mdx b/grafast/website/grafast/standard-steps/inhibitOnNull.mdx index f845f543a9..af3b4c78a4 100644 --- a/grafast/website/grafast/standard-steps/inhibitOnNull.mdx +++ b/grafast/website/grafast/standard-steps/inhibitOnNull.mdx @@ -5,6 +5,15 @@ import Mermaid from "@theme/Mermaid"; Returns a step that yields the same values as the passed in step, but downstream work is inhibited when a yielded value is `null` or `undefined`. +:::danger[Not a security boundary] + +`inhibitOnNull` enables Gra*fast* to optimize away unnecessary downstream work; +it does not guarantee that the work will not execute. Do not use it for +authorization or to prevent security-sensitive operations. Enforce those +requirements inside the protected operation or data source. + +::: + :::note[Declarative flow] The inhibition only affects steps that depend on the wrapper step (and those @@ -28,7 +37,9 @@ adjusted downwards to match. Once the step has executed, the result has these indices re-populated (with an inhibited value) such that the batch size remains consistent with other steps in the same _layer plan_. -Should all indices be inhibited, the step will not execute at all. +With the current implementation, if all indices are inhibited then the step +does not execute. This is an implementation detail, not a guarantee on which +security or correctness may depend. ::: @@ -36,16 +47,14 @@ Should all indices be inhibited, the step will not execute at all. ```ts const $parentId = get($post, "parentId"); -// If $parentId is null, the `loadOne` will be inhibited (will not execute) +// If $parentId is null, Gra*fast* can avoid executing `loadOne` const $parent = loadOne(inhibitOnNull($parentId), batchGetPostById); ``` In the example above, when the `$parentId` step yields `null`, the associated value for the `loadOne` step will automatically yield `null` (and be inhibited) -without needing to execute. Only the uninhibited non-null values pass through to -the `loadOne` step for execution. If all values for a step are inhibited then the -step will not execute at all. When a value for `$parentId` is not nullish, -execution for that value proceeds as normal. +and Gra*fast* can avoid executing the load for that entry. When a value for +`$parentId` is not nullish, execution for that value proceeds as normal. ## Trapping From 8a2bdc19eaab415612a205215ad06a1c3cb89358 Mon Sep 17 00:00:00 2001 From: Benjie Date: Fri, 7 Aug 2026 14:24:31 +0100 Subject: [PATCH 2/2] Apply suggestion from @benjie --- grafast/website/grafast/standard-steps/inhibitOnNull.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/grafast/website/grafast/standard-steps/inhibitOnNull.mdx b/grafast/website/grafast/standard-steps/inhibitOnNull.mdx index af3b4c78a4..4acaca0a4b 100644 --- a/grafast/website/grafast/standard-steps/inhibitOnNull.mdx +++ b/grafast/website/grafast/standard-steps/inhibitOnNull.mdx @@ -37,9 +37,7 @@ adjusted downwards to match. Once the step has executed, the result has these indices re-populated (with an inhibited value) such that the batch size remains consistent with other steps in the same _layer plan_. -With the current implementation, if all indices are inhibited then the step -does not execute. This is an implementation detail, not a guarantee on which -security or correctness may depend. +Should all indices be inhibited, the step will not execute at all. :::