refactor: 각 어댑터터별 리펙토링 진행
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,67 @@
|
||||
# ADR-JPA-006 — `audit` is the canonical technical audit model; `auditing` stays a frozen candidate
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-24
|
||||
- Review: `docs/reviews/2026-08-14-jpa-module-code-review.md` JPA-022
|
||||
|
||||
## Context
|
||||
|
||||
Two complete technical-audit mechanisms live in this leaf and they disagree about the schema.
|
||||
|
||||
`audit/AuditableEntity` stamps `created_at`/`created_by`/`updated_at`/`updated_by` with an actor
|
||||
column of length 256, captured through explicit `initializeAudit`/`applyModification` calls and an
|
||||
`AuditContextPort`. `auditing/AuditMetadata` is a Spring Data embeddable that stamps
|
||||
`created_*`/`modified_*` with an actor column of length 64, captured by `@CreatedDate` and friends
|
||||
through an `AuditorAware`.
|
||||
|
||||
Only the first is real: it is what the sample entities extend and what the migrations were written
|
||||
for. `JpaAuditingConfiguration` is not a Spring `@Configuration`, and nothing in production
|
||||
constructs any of the three `auditing` types.
|
||||
|
||||
The review asked for one canonical model with a migration or activation decision. The failure mode
|
||||
it was protecting against is specific: an author of a new entity picks whichever package they find
|
||||
first, and column names, actor lengths and capture lifecycles then diverge per table.
|
||||
|
||||
## Decision
|
||||
|
||||
`audit/AuditableEntity` is canonical. `auditing` stays in the tree as a candidate and is excluded
|
||||
from the Stable capability report.
|
||||
|
||||
The candidate is not deleted and not promoted. Deleting it would discard a working Spring Data
|
||||
integration that a deployment preferring declarative auditing would want. Promoting it would mean
|
||||
either renaming `modified_*` to `updated_*` and widening the actor column — a schema migration of
|
||||
every audited table to gain nothing a caller asked for — or moving the sample entities onto
|
||||
`modified_*`, which is the same migration in the other direction.
|
||||
|
||||
Neither is worth doing now. What the divergence actually needed was not consolidation but a rule
|
||||
that an entity cannot straddle the two, and that rule is cheaper than either migration.
|
||||
|
||||
## Consequences
|
||||
|
||||
Two audit mechanisms remain readable in one leaf, and a reader has to be told which one is live.
|
||||
That cost is paid in this document, in the package javadoc and in a test whose name says so.
|
||||
|
||||
Two failure modes stay silent unless they are asserted, so both are:
|
||||
|
||||
- The candidate acquires a stereotype and starts stamping in every deployment that has this module
|
||||
on the classpath, including the ones whose tables have no `modified_*` columns — where the result
|
||||
is a failed startup rather than a feature.
|
||||
- Somebody "harmonises" the two by editing one side's column names, at which point the schema a
|
||||
deployed table was migrated for and the schema its entity expects diverge with no migration
|
||||
between them.
|
||||
|
||||
If the candidate is ever promoted, it is promoted atomically: forward migration, sample conversion,
|
||||
`AuditContextPort → AuditorAware` and `Clock → DateTimeProvider` bridges land together, and this
|
||||
ADR is superseded rather than amended.
|
||||
|
||||
Bulk and native updates stamp nothing under either mechanism. That is a property of JPA, not of the
|
||||
choice made here, so it is enforced separately rather than assumed away.
|
||||
|
||||
## Enforcement
|
||||
|
||||
`JpaAuditMechanismRule.entitiesUseExactlyOneAuditMechanism` and
|
||||
`bulkUpdatesOfAuditedEntitiesStampAudit`, run against the real production graph by
|
||||
`JpaProductionArchitectureTest` at the composition root — not against fixtures, which is how the
|
||||
earlier version of this rule pack passed while applying to nothing. `AuditingCandidateStatusTest`
|
||||
asserts the candidate carries no composing stereotype and that the two column sets stay distinct.
|
||||
`JpaAuditMechanismRuleTest` exercises the rules' own negative cases.
|
||||
Reference in New Issue
Block a user