#10: TemporalFieldTraits<T> — decouple decorator from canonical column names
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>
This commit is contained in:
parent
55516d4cf1
commit
1baff07b71
9 changed files with 225 additions and 68 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(oatpp-authkit VERSION 0.4.0 LANGUAGES CXX)
|
project(oatpp-authkit VERSION 0.5.0 LANGUAGES CXX)
|
||||||
|
|
||||||
# Header-only interface library — no compilation, just an include path and
|
# Header-only interface library — no compilation, just an include path and
|
||||||
# a CMake config package so consumers do:
|
# a CMake config package so consumers do:
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ hardened auth / security stack. Header-only, oatpp 1.3+, C++17.
|
||||||
| `util/RateLimiter.hpp` | In-memory token-bucket keyed on an arbitrary string (typically the client IP from `clientIpTrusted`). |
|
| `util/RateLimiter.hpp` | In-memory token-bucket keyed on an arbitrary string (typically the client IP from `clientIpTrusted`). |
|
||||||
| `util/TokenExtract.hpp` | `extractToken` (Cookie/Bearer), `isValidIp` (IPv4/IPv6 via `inet_pton`), `clientIpTrusted` (loopback-gated XFF). |
|
| `util/TokenExtract.hpp` | `extractToken` (Cookie/Bearer), `isValidIp` (IPv4/IPv6 via `inet_pton`), `clientIpTrusted` (loopback-gated XFF). |
|
||||||
| `startup/RequireEncryptionKey.hpp` | `requireEncryptionKey(envVarName, encryptionEnabled, allowPlaintext)` — refuse startup without a symmetric key unless a dev flag overrides. |
|
| `startup/RequireEncryptionKey.hpp` | `requireEncryptionKey(envVarName, encryptionEnabled, allowPlaintext)` — refuse startup without a symmetric key unless a dev flag overrides. |
|
||||||
| `repo/Repository.hpp` + `IHistoryRepository.hpp` + `ITemporalEntity.hpp` + `TemporalAt.hpp` + `ActorContext.hpp` | Pure-abstract `Repository<TDto>` interface set distilled from fewo-webapp's per-entity `*Db` clients. Mixed UUID allocation on `save`, separate `IHistoryRepository<T>` for temporal versions, `ActorContext` placeholder for the upcoming scope-guard decorator. |
|
| `repo/Repository.hpp` + `IHistoryRepository.hpp` + `TemporalFieldTraits.hpp` + `TemporalAt.hpp` + `ActorContext.hpp` | Pure-abstract `Repository<TDto>` interface set distilled from fewo-webapp's per-entity `*Db` clients. Mixed UUID allocation on `save`, separate `IHistoryRepository<T>` for temporal versions, `TemporalFieldTraits<T>` to map canonical (entity_id, valid_from, valid_until) onto whatever a DTO actually calls them, `ActorContext` placeholder for the scope-guard decorator. |
|
||||||
| `repo/TemporalRepository.hpp` | Decorator that wraps any `Repository<TDto : ITemporalEntity>` and turns it into a temporally-versioned one. `save` closes the prior live version and inserts a new one; `findByEntityIdAt(id, at)` returns the version live at a point in time; implements `IHistoryRepository<T>`. Inner adapter is expected to expose all rows (live + historical) and treat `save` as upsert keyed by `(entity_id, valid_from)`. |
|
| `repo/TemporalRepository.hpp` | Decorator that wraps any `Repository<TDto>` and turns it into a temporally-versioned one. `save` closes the prior live version and inserts a new one; `findByEntityIdAt(id, at)` returns the version live at a point in time; implements `IHistoryRepository<T>`. Inner adapter is expected to expose all rows (live + historical) and treat `save` as upsert keyed by `(entity_id, valid_from)`. DTOs register their three temporal columns via `OATPP_AUTHKIT_REGISTER_TEMPORAL`. |
|
||||||
| `repo/ScopeGuardRepository.hpp` | Generic resource-scope decorator. Takes a `bool(ActorContext, TDto)` predicate at construction; gates every method on it. Throws `ScopeDeniedException` on deny (catchers translate to 403). Knows nothing about consumer-specific concepts like "property" or "tenant" — the predicate decides. |
|
| `repo/ScopeGuardRepository.hpp` | Generic resource-scope decorator. Takes a `bool(ActorContext, TDto)` predicate at construction; gates every method on it. Throws `ScopeDeniedException` on deny (catchers translate to 403). Knows nothing about consumer-specific concepts like "property" or "tenant" — the predicate decides. |
|
||||||
| `repo/IQueryable.hpp` | Optional capability for repos that resolve a typed query AST. `field<&Dto::col>().eq(...)` style DSL composes via `&&` / `||` / `!`; `Query<TDto>::toSql()` emits parameterised SQL plus a bind bag. Bounded surface — equality, range, IN, LIKE, NULL, ORDER BY, LIMIT/OFFSET. No joins, subqueries, or aggregates. Concrete repos opt in by deriving `IQueryable<TDto>`. |
|
| `repo/IQueryable.hpp` | Optional capability for repos that resolve a typed query AST. `field<&Dto::col>().eq(...)` style DSL composes via `&&` / `||` / `!`; `Query<TDto>::toSql()` emits parameterised SQL plus a bind bag. Bounded surface — equality, range, IN, LIKE, NULL, ORDER BY, LIMIT/OFFSET. No joins, subqueries, or aggregates. Concrete repos opt in by deriving `IQueryable<TDto>`. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
#ifndef OATPP_AUTHKIT_REPO_I_TEMPORAL_ENTITY_HPP
|
|
||||||
#define OATPP_AUTHKIT_REPO_I_TEMPORAL_ENTITY_HPP
|
|
||||||
|
|
||||||
namespace oatpp_authkit::repo {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Marker for DTOs that carry temporal-versioning columns.
|
|
||||||
*
|
|
||||||
* A DTO opts in by inheriting this empty marker, signalling to the temporal
|
|
||||||
* decorator (oatpp-authkit#8) that the DTO has the three required oatpp
|
|
||||||
* `String` fields:
|
|
||||||
*
|
|
||||||
* @code
|
|
||||||
* DTO_FIELD(String, entity_id); // stable across versions
|
|
||||||
* DTO_FIELD(String, valid_from); // ISO-8601 UTC
|
|
||||||
* DTO_FIELD(String, valid_until); // ISO-8601 UTC; '9999-12-31T23:59:59Z' = live
|
|
||||||
* @endcode
|
|
||||||
*
|
|
||||||
* The marker is intentionally a plain empty struct rather than a C++20
|
|
||||||
* concept — the issue explicitly chose "pure abstract interface, not
|
|
||||||
* `requires` clause" for the wider Repository<T> shape, and this matches.
|
|
||||||
*
|
|
||||||
* Because oatpp DTOs use macros (`DTO_INIT` / `DTO_FIELD`), the field-shape
|
|
||||||
* contract above is documentation-enforced, not compiler-enforced. The
|
|
||||||
* temporal decorator dynamic-casts and accesses fields by name; a missing
|
|
||||||
* field surfaces as a clean runtime error at decorator construction.
|
|
||||||
*/
|
|
||||||
struct ITemporalEntity {};
|
|
||||||
|
|
||||||
} // namespace oatpp_authkit::repo
|
|
||||||
|
|
||||||
#endif
|
|
||||||
47
include/oatpp-authkit/repo/TemporalFieldTraits.hpp
Normal file
47
include/oatpp-authkit/repo/TemporalFieldTraits.hpp
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_TEMPORAL_FIELD_TRAITS_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_TEMPORAL_FIELD_TRAITS_HPP
|
||||||
|
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Trait that tells `TemporalRepository<T>` where `T` keeps its
|
||||||
|
* identity, valid_from and valid_until columns.
|
||||||
|
*
|
||||||
|
* Primary template is intentionally undefined — using
|
||||||
|
* `TemporalFieldTraits<MyDto>` against a DTO that hasn't been registered
|
||||||
|
* is a hard compile error pointing at the call site. Specialise with
|
||||||
|
* `OATPP_AUTHKIT_REGISTER_TEMPORAL` once per temporal DTO.
|
||||||
|
*
|
||||||
|
* Each accessor returns `oatpp::String&` so the repository can both read
|
||||||
|
* and rewrite the value (closing a prior version sets `valid_until` on a
|
||||||
|
* loaded row). Field names on the DTO are arbitrary — the trait is the
|
||||||
|
* canonical name, the DTO column is whatever the consumer picked.
|
||||||
|
*/
|
||||||
|
template <class TDto>
|
||||||
|
struct TemporalFieldTraits; // intentionally undefined
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a temporal DTO with the trait machinery. Place at namespace
|
||||||
|
* scope (typically right after the DTO definition):
|
||||||
|
*
|
||||||
|
* OATPP_AUTHKIT_REGISTER_TEMPORAL(PersonDto,
|
||||||
|
* entity_id, valid_from, valid_until)
|
||||||
|
*
|
||||||
|
* The three trailing identifiers are the actual DTO_FIELD member names —
|
||||||
|
* they don't have to match the canonical names. A DTO that uses
|
||||||
|
* `effective_from` / `effective_until` registers exactly the same way.
|
||||||
|
*/
|
||||||
|
#define OATPP_AUTHKIT_REGISTER_TEMPORAL(Dto, IdMember, FromMember, UntilMember) \
|
||||||
|
namespace oatpp_authkit::repo { \
|
||||||
|
template<> struct TemporalFieldTraits<Dto> { \
|
||||||
|
static ::oatpp::String& entityId (const ::oatpp::Object<Dto>& d) { return d->IdMember; } \
|
||||||
|
static ::oatpp::String& validFrom (const ::oatpp::Object<Dto>& d) { return d->FromMember; } \
|
||||||
|
static ::oatpp::String& validUntil (const ::oatpp::Object<Dto>& d) { return d->UntilMember; } \
|
||||||
|
}; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
#include "oatpp-authkit/repo/Repository.hpp"
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
#include "oatpp-authkit/repo/IHistoryRepository.hpp"
|
#include "oatpp-authkit/repo/IHistoryRepository.hpp"
|
||||||
#include "oatpp-authkit/repo/ITemporalEntity.hpp"
|
#include "oatpp-authkit/repo/TemporalFieldTraits.hpp"
|
||||||
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
||||||
|
|
||||||
#include "oatpp/core/Types.hpp"
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
@ -24,11 +24,12 @@ namespace oatpp_authkit::repo {
|
||||||
/**
|
/**
|
||||||
* @brief Decorator that turns any `Repository<TDto>` into a temporally-versioned one.
|
* @brief Decorator that turns any `Repository<TDto>` into a temporally-versioned one.
|
||||||
*
|
*
|
||||||
* `TDto` must inherit `ITemporalEntity` and expose three oatpp `String` fields
|
* `TDto` must register a `TemporalFieldTraits<TDto>` specialisation (use
|
||||||
* declared via `DTO_FIELD`: `entity_id`, `valid_from`, `valid_until`. The
|
* the `OATPP_AUTHKIT_REGISTER_TEMPORAL` macro right after the DTO
|
||||||
* marker is asserted at compile time; the field shape is documentation-
|
* definition). The trait names the DTO members that hold the canonical
|
||||||
* enforced (oatpp DTOs don't expose a static field-list mechanism the
|
* `entity_id`, `valid_from`, `valid_until` columns — actual member names
|
||||||
* decorator could verify via `static_assert`).
|
* on the DTO are arbitrary, the trait does the mapping. Forgetting to
|
||||||
|
* register surfaces as a hard compile error at the first trait use.
|
||||||
*
|
*
|
||||||
* @section contract Inner repository contract
|
* @section contract Inner repository contract
|
||||||
*
|
*
|
||||||
|
|
@ -84,18 +85,19 @@ public:
|
||||||
: m_inner(std::move(inner))
|
: m_inner(std::move(inner))
|
||||||
, m_clock(clock ? std::move(clock) : defaultClock())
|
, m_clock(clock ? std::move(clock) : defaultClock())
|
||||||
, m_idgen(idgen ? std::move(idgen) : defaultIdGen())
|
, m_idgen(idgen ? std::move(idgen) : defaultIdGen())
|
||||||
{
|
{}
|
||||||
static_assert(std::is_base_of_v<ITemporalEntity, TDto>,
|
|
||||||
"TemporalRepository<TDto> requires TDto to inherit ITemporalEntity");
|
using F = TemporalFieldTraits<TDto>;
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief Live row for the given entity_id, or null. */
|
/** @brief Live row for the given entity_id, or null. */
|
||||||
oatpp::Object<TDto> findByEntityId(const oatpp::String& entityId) override {
|
oatpp::Object<TDto> findByEntityId(const oatpp::String& entityId) override {
|
||||||
auto all = m_inner->list();
|
auto all = m_inner->list();
|
||||||
for (auto& row : *all) {
|
for (auto& row : *all) {
|
||||||
if (row->entity_id && row->valid_until
|
auto& id = F::entityId(row);
|
||||||
&& std::string(*row->entity_id) == std::string(*entityId)
|
auto& vu = F::validUntil(row);
|
||||||
&& std::string(*row->valid_until) == SENTINEL) {
|
if (id && vu
|
||||||
|
&& std::string(*id) == std::string(*entityId)
|
||||||
|
&& std::string(*vu) == SENTINEL) {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,9 +112,12 @@ public:
|
||||||
const std::string atIso = isoFromMillis(at.timestamp);
|
const std::string atIso = isoFromMillis(at.timestamp);
|
||||||
auto all = m_inner->list();
|
auto all = m_inner->list();
|
||||||
for (auto& row : *all) {
|
for (auto& row : *all) {
|
||||||
if (!row->entity_id || std::string(*row->entity_id) != std::string(*entityId)) continue;
|
auto& id = F::entityId(row);
|
||||||
const std::string from = row->valid_from ? std::string(*row->valid_from) : std::string();
|
if (!id || std::string(*id) != std::string(*entityId)) continue;
|
||||||
const std::string until = row->valid_until ? std::string(*row->valid_until) : std::string();
|
auto& vf = F::validFrom(row);
|
||||||
|
auto& vu = F::validUntil(row);
|
||||||
|
const std::string from = vf ? std::string(*vf) : std::string();
|
||||||
|
const std::string until = vu ? std::string(*vu) : std::string();
|
||||||
if (from <= atIso && atIso < until) return row;
|
if (from <= atIso && atIso < until) return row;
|
||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -123,7 +128,8 @@ public:
|
||||||
auto out = oatpp::Vector<oatpp::Object<TDto>>::createShared();
|
auto out = oatpp::Vector<oatpp::Object<TDto>>::createShared();
|
||||||
auto all = m_inner->list();
|
auto all = m_inner->list();
|
||||||
for (auto& row : *all) {
|
for (auto& row : *all) {
|
||||||
if (row->valid_until && std::string(*row->valid_until) == SENTINEL) {
|
auto& vu = F::validUntil(row);
|
||||||
|
if (vu && std::string(*vu) == SENTINEL) {
|
||||||
out->push_back(row);
|
out->push_back(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,21 +142,21 @@ public:
|
||||||
* and `valid_until`.
|
* and `valid_until`.
|
||||||
*/
|
*/
|
||||||
void save(const oatpp::Object<TDto>& dto) override {
|
void save(const oatpp::Object<TDto>& dto) override {
|
||||||
if (!dto->entity_id) dto->entity_id = m_idgen();
|
if (!F::entityId(dto)) F::entityId(dto) = m_idgen();
|
||||||
|
|
||||||
const int64_t nowMs = m_clock();
|
const int64_t nowMs = m_clock();
|
||||||
const std::string nowIso = isoFromMillis(nowMs);
|
const std::string nowIso = isoFromMillis(nowMs);
|
||||||
|
|
||||||
// Close the existing live version (if any).
|
// Close the existing live version (if any).
|
||||||
auto live = findByEntityId(dto->entity_id);
|
auto live = findByEntityId(F::entityId(dto));
|
||||||
if (live) {
|
if (live) {
|
||||||
live->valid_until = oatpp::String(nowIso);
|
F::validUntil(live) = oatpp::String(nowIso);
|
||||||
m_inner->save(live);
|
m_inner->save(live);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the new live version.
|
// Insert the new live version.
|
||||||
dto->valid_from = oatpp::String(nowIso);
|
F::validFrom(dto) = oatpp::String(nowIso);
|
||||||
dto->valid_until = oatpp::String(SENTINEL);
|
F::validUntil(dto) = oatpp::String(SENTINEL);
|
||||||
m_inner->save(dto);
|
m_inner->save(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +164,7 @@ public:
|
||||||
void softDelete(const oatpp::String& entityId) override {
|
void softDelete(const oatpp::String& entityId) override {
|
||||||
auto live = findByEntityId(entityId);
|
auto live = findByEntityId(entityId);
|
||||||
if (!live) return;
|
if (!live) return;
|
||||||
live->valid_until = oatpp::String(isoFromMillis(m_clock()));
|
F::validUntil(live) = oatpp::String(isoFromMillis(m_clock()));
|
||||||
m_inner->save(live);
|
m_inner->save(live);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,14 +175,17 @@ public:
|
||||||
std::vector<oatpp::Object<TDto>> bucket;
|
std::vector<oatpp::Object<TDto>> bucket;
|
||||||
auto all = m_inner->list();
|
auto all = m_inner->list();
|
||||||
for (auto& row : *all) {
|
for (auto& row : *all) {
|
||||||
if (row->entity_id && std::string(*row->entity_id) == std::string(*entityId)) {
|
auto& id = F::entityId(row);
|
||||||
|
if (id && std::string(*id) == std::string(*entityId)) {
|
||||||
bucket.push_back(row);
|
bucket.push_back(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::sort(bucket.begin(), bucket.end(),
|
std::sort(bucket.begin(), bucket.end(),
|
||||||
[](const oatpp::Object<TDto>& a, const oatpp::Object<TDto>& b) {
|
[](const oatpp::Object<TDto>& a, const oatpp::Object<TDto>& b) {
|
||||||
const std::string af = a->valid_from ? std::string(*a->valid_from) : std::string();
|
auto& af_s = F::validFrom(a);
|
||||||
const std::string bf = b->valid_from ? std::string(*b->valid_from) : std::string();
|
auto& bf_s = F::validFrom(b);
|
||||||
|
const std::string af = af_s ? std::string(*af_s) : std::string();
|
||||||
|
const std::string bf = bf_s ? std::string(*bf_s) : std::string();
|
||||||
return af < bf;
|
return af < bf;
|
||||||
});
|
});
|
||||||
auto out = oatpp::Vector<oatpp::Object<TDto>>::createShared();
|
auto out = oatpp::Vector<oatpp::Object<TDto>>::createShared();
|
||||||
|
|
|
||||||
|
|
@ -33,3 +33,7 @@ add_test(NAME repository_decorators COMMAND test_repository_decorators)
|
||||||
add_executable(test_queryable test_queryable.cpp)
|
add_executable(test_queryable test_queryable.cpp)
|
||||||
target_link_libraries(test_queryable PRIVATE oatpp::authkit oatpp::oatpp)
|
target_link_libraries(test_queryable PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
add_test(NAME queryable COMMAND test_queryable)
|
add_test(NAME queryable COMMAND test_queryable)
|
||||||
|
|
||||||
|
add_executable(test_temporal_field_traits test_temporal_field_traits.cpp)
|
||||||
|
target_link_libraries(test_temporal_field_traits PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
|
add_test(NAME temporal_field_traits COMMAND test_temporal_field_traits)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
#include "oatpp-authkit/repo/TemporalRepository.hpp"
|
#include "oatpp-authkit/repo/TemporalRepository.hpp"
|
||||||
#include "oatpp-authkit/repo/ScopeGuardRepository.hpp"
|
#include "oatpp-authkit/repo/ScopeGuardRepository.hpp"
|
||||||
#include "oatpp-authkit/repo/Repository.hpp"
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
#include "oatpp-authkit/repo/ITemporalEntity.hpp"
|
#include "oatpp-authkit/repo/TemporalFieldTraits.hpp"
|
||||||
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
||||||
#include "oatpp-authkit/repo/ActorContext.hpp"
|
#include "oatpp-authkit/repo/ActorContext.hpp"
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class MockTemporalDto : public oatpp::DTO, public oatpp_authkit::repo::ITemporalEntity {
|
class MockTemporalDto : public oatpp::DTO {
|
||||||
DTO_INIT(MockTemporalDto, DTO)
|
DTO_INIT(MockTemporalDto, DTO)
|
||||||
DTO_FIELD(String, entity_id);
|
DTO_FIELD(String, entity_id);
|
||||||
DTO_FIELD(String, valid_from);
|
DTO_FIELD(String, valid_from);
|
||||||
|
|
@ -40,6 +40,10 @@ class MockTemporalDto : public oatpp::DTO, public oatpp_authkit::repo::ITemporal
|
||||||
|
|
||||||
#include OATPP_CODEGEN_END(DTO)
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
OATPP_AUTHKIT_REGISTER_TEMPORAL(MockTemporalDto, entity_id, valid_from, valid_until)
|
||||||
|
namespace {
|
||||||
|
|
||||||
int g_failures = 0;
|
int g_failures = 0;
|
||||||
|
|
||||||
#define REQUIRE(expr) do { \
|
#define REQUIRE(expr) do { \
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
#include "oatpp-authkit/repo/Repository.hpp"
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
#include "oatpp-authkit/repo/IHistoryRepository.hpp"
|
#include "oatpp-authkit/repo/IHistoryRepository.hpp"
|
||||||
#include "oatpp-authkit/repo/ITemporalEntity.hpp"
|
|
||||||
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
||||||
#include "oatpp-authkit/repo/ActorContext.hpp"
|
#include "oatpp-authkit/repo/ActorContext.hpp"
|
||||||
|
|
||||||
|
|
@ -165,10 +164,10 @@ void test_actor_context_minimal() {
|
||||||
REQUIRE(ctx.allowedScopes.size() == 2);
|
REQUIRE(ctx.allowedScopes.size() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check: ITemporalEntity is usable as a base marker.
|
// Compile-time check: a temporal DTO with all three canonical fields builds.
|
||||||
#include OATPP_CODEGEN_BEGIN(DTO)
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
||||||
|
|
||||||
class TemporalDto : public oatpp::DTO, public oatpp_authkit::repo::ITemporalEntity {
|
class TemporalDto : public oatpp::DTO {
|
||||||
DTO_INIT(TemporalDto, DTO)
|
DTO_INIT(TemporalDto, DTO)
|
||||||
DTO_FIELD(String, entity_id);
|
DTO_FIELD(String, entity_id);
|
||||||
DTO_FIELD(String, valid_from);
|
DTO_FIELD(String, valid_from);
|
||||||
|
|
@ -177,7 +176,7 @@ class TemporalDto : public oatpp::DTO, public oatpp_authkit::repo::ITemporalEnti
|
||||||
|
|
||||||
#include OATPP_CODEGEN_END(DTO)
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
void test_temporal_marker_compiles() {
|
void test_temporal_dto_compiles() {
|
||||||
auto dto = TemporalDto::createShared();
|
auto dto = TemporalDto::createShared();
|
||||||
dto->entity_id = oatpp::String("t");
|
dto->entity_id = oatpp::String("t");
|
||||||
REQUIRE(std::string(*dto->entity_id) == "t");
|
REQUIRE(std::string(*dto->entity_id) == "t");
|
||||||
|
|
@ -193,7 +192,7 @@ int main() {
|
||||||
test_soft_delete_removes_from_live_view();
|
test_soft_delete_removes_from_live_view();
|
||||||
test_temporal_at_value_type();
|
test_temporal_at_value_type();
|
||||||
test_actor_context_minimal();
|
test_actor_context_minimal();
|
||||||
test_temporal_marker_compiles();
|
test_temporal_dto_compiles();
|
||||||
|
|
||||||
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
||||||
return g_failures ? 1 : 0;
|
return g_failures ? 1 : 0;
|
||||||
|
|
|
||||||
126
test/test_temporal_field_traits.cpp
Normal file
126
test/test_temporal_field_traits.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
// Tests for the oatpp-authkit#10 TemporalFieldTraits<T> extension.
|
||||||
|
//
|
||||||
|
// Exercises the temporal decorator against a DTO whose column names are
|
||||||
|
// NOT entity_id / valid_from / valid_until. The trait specialisation
|
||||||
|
// supplied via OATPP_AUTHKIT_REGISTER_TEMPORAL bridges the canonical
|
||||||
|
// names used by TemporalRepository to whatever the DTO actually calls
|
||||||
|
// them — here `id`, `effective_from`, `effective_until`. Same save/close/
|
||||||
|
// history flow as the existing decorator tests; only the field names move.
|
||||||
|
|
||||||
|
#include "oatpp-authkit/repo/TemporalRepository.hpp"
|
||||||
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
|
#include "oatpp-authkit/repo/TemporalFieldTraits.hpp"
|
||||||
|
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
||||||
|
|
||||||
|
#include "oatpp/core/macro/codegen.hpp"
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// DTO with intentionally non-canonical column names. Without the trait,
|
||||||
|
// TemporalRepository<T> couldn't reach these fields.
|
||||||
|
class OddNamesDto : public oatpp::DTO {
|
||||||
|
DTO_INIT(OddNamesDto, DTO)
|
||||||
|
DTO_FIELD(String, id);
|
||||||
|
DTO_FIELD(String, effective_from);
|
||||||
|
DTO_FIELD(String, effective_until);
|
||||||
|
DTO_FIELD(String, payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
OATPP_AUTHKIT_REGISTER_TEMPORAL(OddNamesDto, id, effective_from, effective_until)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int g_failures = 0;
|
||||||
|
|
||||||
|
#define REQUIRE(expr) do { \
|
||||||
|
if (!(expr)) { \
|
||||||
|
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \
|
||||||
|
++g_failures; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
// Same in-memory adapter shape as the decorator tests — keys rows by
|
||||||
|
// (id, effective_from), exposes ALL rows via list().
|
||||||
|
class InMemoryAllRows : public oatpp_authkit::repo::Repository<OddNamesDto> {
|
||||||
|
std::map<std::pair<std::string, std::string>, oatpp::Object<OddNamesDto>> rows;
|
||||||
|
public:
|
||||||
|
oatpp::Object<OddNamesDto> findByEntityId(const oatpp::String& id) override {
|
||||||
|
for (auto& kv : rows) if (kv.first.first == std::string(*id)) return kv.second;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
oatpp::Vector<oatpp::Object<OddNamesDto>> list() override {
|
||||||
|
auto v = oatpp::Vector<oatpp::Object<OddNamesDto>>::createShared();
|
||||||
|
for (auto& kv : rows) v->push_back(kv.second);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
void save(const oatpp::Object<OddNamesDto>& dto) override {
|
||||||
|
rows[{*dto->id, *dto->effective_from}] = dto;
|
||||||
|
}
|
||||||
|
void softDelete(const oatpp::String& id) override {
|
||||||
|
for (auto it = rows.begin(); it != rows.end(); ) {
|
||||||
|
if (it->first.first == std::string(*id)) it = rows.erase(it); else ++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StepClock {
|
||||||
|
int64_t ms{1700000000000LL};
|
||||||
|
int64_t operator()() { int64_t v = ms; ms += 1000; return v; }
|
||||||
|
};
|
||||||
|
|
||||||
|
void test_save_close_and_history_against_renamed_columns() {
|
||||||
|
using namespace oatpp_authkit::repo;
|
||||||
|
auto inner = std::make_shared<InMemoryAllRows>();
|
||||||
|
auto clock = std::make_shared<StepClock>();
|
||||||
|
TemporalRepository<OddNamesDto> repo(inner,
|
||||||
|
[clock]{ return (*clock)(); });
|
||||||
|
|
||||||
|
// First save — id auto-allocated, effective_from = now, effective_until = SENTINEL.
|
||||||
|
auto v1 = OddNamesDto::createShared();
|
||||||
|
v1->payload = oatpp::String("first");
|
||||||
|
repo.save(v1);
|
||||||
|
REQUIRE(v1->id);
|
||||||
|
REQUIRE(std::string(*v1->effective_until)
|
||||||
|
== TemporalRepository<OddNamesDto>::SENTINEL);
|
||||||
|
|
||||||
|
// Second save — close prior, insert new live.
|
||||||
|
auto v2 = OddNamesDto::createShared();
|
||||||
|
v2->id = v1->id;
|
||||||
|
v2->payload = oatpp::String("second");
|
||||||
|
repo.save(v2);
|
||||||
|
|
||||||
|
auto live = repo.findByEntityId(v1->id);
|
||||||
|
REQUIRE(live);
|
||||||
|
REQUIRE(std::string(*live->payload) == "second");
|
||||||
|
|
||||||
|
// history() returns both versions, oldest first.
|
||||||
|
auto h = repo.history(v1->id);
|
||||||
|
REQUIRE(h->size() == 2);
|
||||||
|
REQUIRE(std::string(*(*h)[0]->payload) == "first");
|
||||||
|
REQUIRE(std::string(*(*h)[1]->payload) == "second");
|
||||||
|
|
||||||
|
// softDelete closes live.
|
||||||
|
repo.softDelete(v1->id);
|
||||||
|
REQUIRE(!repo.findByEntityId(v1->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_save_close_and_history_against_renamed_columns();
|
||||||
|
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
||||||
|
return g_failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue