Adds a decorator that sits below TemporalRepository and redacts
configured fields whenever it sees a save with valid_until != SENTINEL
(i.e., a historical row being closed by the temporal close-then-update
flow). The live row keeps its values intact.
Per Option B from the issue thread: by default the user-repo factory
redacts both passwordHash and tlsCertDn. Empty redaction list passes
everything through unchanged, so non-user temporal stacks compose the
decorator without surprise behaviour.
Files:
- repo/RedactedFieldRepository.hpp — new decorator. Schema contribution
is empty (purely a save-time transform). Field-name matching uses
oatpp's reflective property dispatcher and matches against the C++
identifier name (first DTO_FIELD argument).
- repo/ConcreteUserRepository.hpp — makeUserRepository now wraps the
concrete repo in RedactedFieldRepository<UserDto>{"passwordHash",
"tlsCertDn"} before passing to TemporalRepository. Optional second
argument lets consumers override the redaction list.
- test/test_redacted_field_repository.cpp — five tests cover live-row
pass-through, historical-row redaction (both fields), partial
redaction list, empty list, and null-valid_until treated as live.
- README.md — adds RedactedFieldRepository to the header inventory.
14 of 14 tests pass. Bumped 0.12.0 → 0.13.0.
Closes#15
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
D-replace per #14: rip out PREREQ + RESHAPE_STEPS + applyDecoratorMigrations
and replace with declarative DecoratorSchema (entity columns + indexes +
sidecar tables). SchemaBuilder<Decorators...>::create composes the stack
into a single CREATE TABLE per entity table; SchemaContract::verify
introspects-and-asserts at runtime so code can never run against an
under-migrated DB. Atlas (atlasgo.io) becomes the authority for schema
evolution between deploys — decorator code never runs ALTER at runtime.
- TemporalRepository contributes valid_from/valid_until + UNIQUE composite index
- AuditLogRepository contributes the audit_log sidecar table
- ScopeGuardRepository declares empty contributions for clean stacking
- 8 new tests in test_schema_contract.cpp covering compose / dedup / verify
- README updated; bumped 0.8.0 → 0.9.0
fewo-webapp does not yet call applyDecoratorMigrations, so this is a
clean cut — no consumer-side breakage. PRs 1-4 (role_templates,
user_property_permissions, user_group_permissions, users) follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The decorator's save() flow now preserves the live row's id PK across
updates and captures each prior version as a fresh row with a new id.
This unblocks fewo-webapp#459: the consumer's composite-FK schema needs
stable child references to the live row (UNIQUE(entity_id, valid_until)
with ON UPDATE CASCADE on every child FK), which the previous
close-then-insert flow couldn't provide.
New flow on update (when a live row exists for entity_id):
1. Clone the live row in memory (cloneDto via oatpp reflection),
assign a fresh id and set valid_until=now, save → INSERT historical.
2. Set the new dto's id=live.id (preserve PK), valid_from=now,
valid_until=SENTINEL, save → inner UPDATEs the live row in place by
PK.
Inner adapter contract changes from "upsert keyed by (entity_id,
valid_from)" to "upsert keyed by id (per-row PK)". TemporalFieldTraits
gains an id() accessor; OATPP_AUTHKIT_REGISTER_TEMPORAL grows from 4 to
5 args (Dto + IdMember + EntityIdMember + FromMember + UntilMember).
Tests: test_repository_decorators asserts livePk stability across saves
and fresh historicalPk per version; remaining decorator tests updated to
the 5-arg macro form. README's TemporalRepository.hpp row rewritten to
describe the new write semantics.
Bumped CMake version 0.7.0 → 0.8.0 (semantic break — save() no longer
reallocates the live PK; consumers depending on the old contract need
audit).
Closes#13
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each decorator now bundles its schema prereqs alongside its code via
DecoratorPrereq (additive CREATE-IF-NOT-EXISTS) and ReshapeStep
(non-idempotent reshape gated on a detectSql probe).
applyDecoratorMigrations<Decorators...>(table, probe, exec, recorder)
walks the listed decorators at startup, runs every PREREQ, runs every
reshape step whose probe returns false. Database-agnostic — consumer
wires probe/exec to their DbClient. SCHEMA_MIGRATIONS_TABLE_SQL is
provided for observability; the detect-probe is the source of truth.
TemporalRepository ships add_valid_from / add_valid_until /
drop_unique_entity_id / composite_unique (UNIQUE(entity_id, valid_until)
so close-then-insert can run in a deferred-FK transaction).
AuditLogRepository ships the audit_log CREATE TABLE.
ScopeGuardRepository ships nothing — exposes empty PREREQ + zero-length
RESHAPE_STEPS so it can be listed in applyDecoratorMigrations alongside
the schema-touching decorators without SFINAE.
Closes#12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace hard-coded dto->entity_id/valid_from/valid_until accesses in
TemporalRepository with trait calls (F::entityId/validFrom/validUntil).
DTOs register canonical→actual member name mapping via
OATPP_AUTHKIT_REGISTER_TEMPORAL. Forgetting to register is a hard
compile error. ITemporalEntity marker is gone; the trait specialisation
carries the contract. Bumps version 0.4.0 → 0.5.0.
New test verifies the full save/close/history/softDelete flow against a
DTO whose columns are id/effective_from/effective_until rather than the
canonical names — exercises the renaming the trait enables.
Closes#10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two cross-cutting decorators that wrap any Repository<TDto> from #7.
TemporalRepository<TDto>:
- Requires TDto : ITemporalEntity (compile-time static_assert).
- save() finds the existing live version, closes its valid_until, and
inserts a new row at valid_until = '9999-12-31T23:59:59Z' sentinel.
- findByEntityId() returns the live row; findByEntityIdAt(id, at) does
the [valid_from, valid_until) point-in-time read.
- list() returns live rows only; history(id) returns all versions
ordered by valid_from. Implements IHistoryRepository<TDto>.
- softDelete closes the live row without inserting a new version.
- Clock and id-generator are constructor-injected (defaults: system_clock
+ 32-char hex from mt19937_64) so the unit tests are deterministic.
The decorator's contract on the inner repository: list() must expose all
rows including historical, and save() must be upsert keyed by
(entity_id, valid_from). Documented on the class.
ScopeGuardRepository<TDto>:
- Generic; knows nothing about "property"/"tenant"/etc. Constructor
takes a std::function<bool(ActorContext, TDto)> predicate plus a
std::function<ActorContext()> accessor (so a single instance can
serve many requests with different actors).
- list() filters; findByEntityId/save/softDelete throw
ScopeDeniedException on deny.
Tests cover the five acceptance criteria from the issue body:
- Temporal save closes the prior version
- Live read returns only the row with valid_until = sentinel
- Point-in-time read returns the version live at that time
- History returns all versions in order
- Scope guard short-circuits when the predicate returns false
ctest: 6/6 green (4 prior + repository_interface + repository_decorators).
Closes#8
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Header-only foundation for the structural refactor that moves fewo-webapp
from per-entity *Db clients to a shared Repository<TDto> abstraction. This
ships interfaces only — no concrete implementations, no callers updated.
Decisions baked in (all settled in the issue body):
- Mixed entity_id allocation: caller may supply, otherwise the concrete
repo generates a UUID inside save().
- UnitOfWork / cross-repo transactions: explicitly out of scope.
- Repository<T> is a virtual-method interface, not a C++20 concept.
- History queries live on a separate IHistoryRepository<T> so non-temporal
repos don't have to implement a stub.
Decorators (TemporalRepository<T>, ScopeGuardRepository<T>) follow in #8;
the optional IQueryable<T> capability for typed filtering follows in #9.
The fewo-webapp Person pilot (uwe.admin/fewo-webapp#457) and the wider
26-entity rollout (uwe.admin/fewo-webapp#458) build on this.
Closes#7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AuthInterceptor previously returned application/json for every rejection,
which is wrong for browser navigation: the user followed a /set-password
link and saw a raw {"status":"Unauthorized"} blob.
Add wantsJson() negotiation (path /api/* OR X-Requested-With OR Accept
prefers application/json over text/html) and an IAuthPolicy hook
unauthenticatedRedirect(path) that lets consumers bounce browser
navigations to a landing/login page. JSON callers (fetch/axios) still
get JSON 401/403. Default policy returns nullopt → minimal HTML error
page, never raw JSON to a browser.
Same hook covers both 401 and 403 (decision Option A on the issue) so
consumers wire one redirect target for both unauth and forbidden cases.
Bootstrap a minimal test harness (decision Option T2): CMake option
OATPP_AUTHKIT_BUILD_TESTS gates enable_testing() + a tests subdir.
Adds test_negotiation covering wantsJson + urlEncode. No third-party
test framework — assertions use <cassert> + a tiny REQUIRE macro so the
suite stays dependency-free for future tests.
Closes#2
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>