71 lines
4.0 KiB
Markdown
71 lines
4.0 KiB
Markdown
# ADR-GQL-001 — GraphQL context stays inbound; object authorization moves to application-core; the persisted-operation store stays an inbound SPI
|
|
|
|
- Status: Accepted
|
|
- Date: 2026-08-24
|
|
- Review: `docs/reviews/2026-08-14-graphql-module-code-review.md` GQL-026
|
|
|
|
## Context
|
|
|
|
The GraphQL leaf's own documentation described three things crossing its boundary: a
|
|
`GraphQlRequestContext` with a deadline propagated into application, JPA, Mongo and the HTTP client;
|
|
object authorization decided inside the transport; and a persisted-operation registry implemented by
|
|
an external durable store.
|
|
|
|
Two of those invert the dependency direction. If `application-core` or an outbound adapter
|
|
implements a type that lives in `adapter:inbound:graphql`, the registry edge that says inbound
|
|
depends on application is satisfied while the real compile-time dependency runs the other way.
|
|
|
|
The third is a business rule in the wrong layer: whether an actor may see an object is a decision
|
|
about the domain, and GraphQL is one of four transports this skeleton ships.
|
|
|
|
## Decision
|
|
|
|
Three different answers, because the three problems are not the same problem.
|
|
|
|
**GraphQL context stays inbound-local.** It is mapped explicitly onto application command fields —
|
|
actor, tenant, deadline — rather than travelling as a type. Nothing outside the leaf references
|
|
`GraphQlRequestContext`, and the boundary test is that grep returns nothing outside it.
|
|
|
|
**Object authorization moves to `application-core`.** `ObjectAccessPolicy`, `ObjectAccessRequest`
|
|
and `ObjectAccessDecision` are transport-neutral and live with the other application policies;
|
|
`ApplicationObjectAuthorization` in the GraphQL leaf is the bridge that calls them. This is the one
|
|
of the three that was a real layering defect, and it is fixed rather than documented.
|
|
|
|
**The persisted-operation store stays an inbound-owned SPI.** `GraphQlPersistedOperationRegistry`
|
|
remains in `advanced/persisted`, and no leaf outside GraphQL implements it.
|
|
|
|
## Consequences
|
|
|
|
The third decision is the one that needs defending, because it leaves the reported risk in place
|
|
rather than removing it.
|
|
|
|
The risk is conditional: the direction inverts only when something outside the leaf implements the
|
|
interface. Nothing does. The template ships an in-memory registry and no durable one, because it
|
|
ships no persisted-operation store at all.
|
|
|
|
The alternative was to introduce a generic operational key-value store port owned by a neutral
|
|
contract holder, with the GraphQL adapter owning only the key and value mapping. That port would
|
|
have exactly one interface, zero implementations and one speculative consumer — a new abstraction
|
|
whose shape is guessed from a requirement nobody has stated. This repository has spent a full
|
|
remediation pass deleting controls that existed and were reached by nothing, and inventing a port
|
|
for a store that does not exist is how the next one of those gets written.
|
|
|
|
So the decision is to leave the SPI where it is and to move it when a durable store is actually
|
|
built. Moving it then is a rename across one leaf and one new adapter, which is cheaper than
|
|
carrying a wrong abstraction until then. What must not happen in the meantime is an outbound leaf
|
|
implementing the inbound interface, because that is the moment the direction actually inverts, and
|
|
it would happen in a commit whose diff looks like an implementation rather than a layering change.
|
|
|
|
The composition root wires these and owns no business or storage policy of its own.
|
|
|
|
## Enforcement
|
|
|
|
`verifyCleanArchitectureDependencies` and `modules.json` hold the leaf's edges to
|
|
`domain-core`, `application-core` and `shared-contract`. `ObjectAccessPolicyTest` covers the
|
|
application-side policy and `ApplicationObjectAuthorizationTest` the bridge.
|
|
|
|
The condition this ADR turns on — that nothing outside the GraphQL leaf implements the
|
|
persisted-operation SPI — is a claim about the whole repository, so it is checked at the
|
|
composition root rather than inside the leaf, next to the other GraphQL boundary rules in
|
|
`app-bootstrap`'s architecture suite.
|