#7: Repository<T> interface set + ITemporalEntity + IHistoryRepository<T>
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>
This commit is contained in:
parent
f43f5f0633
commit
a0c61b3d94
8 changed files with 396 additions and 0 deletions
|
|
@ -13,6 +13,7 @@ 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. |
|
||||||
|
|
||||||
## Consume via CMake
|
## Consume via CMake
|
||||||
|
|
||||||
|
|
|
||||||
27
include/oatpp-authkit/repo/ActorContext.hpp
Normal file
27
include/oatpp-authkit/repo/ActorContext.hpp
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_ACTOR_CONTEXT_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_ACTOR_CONTEXT_HPP
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Who is performing a repository action, plus what they're scoped to.
|
||||||
|
*
|
||||||
|
* Passed to the scope-guard decorator predicate (added in oatpp-authkit#8) so
|
||||||
|
* resource-level authorisation can be evaluated outside the concrete repo.
|
||||||
|
*
|
||||||
|
* Kept deliberately minimal — consumers extend by composing this struct into
|
||||||
|
* a richer per-app context if needed. The fields here are the union of what
|
||||||
|
* the fewo-webapp property-scope guard needs (user id + a list of allowed
|
||||||
|
* resource ids) and nothing more.
|
||||||
|
*/
|
||||||
|
struct ActorContext {
|
||||||
|
std::string userId;
|
||||||
|
std::vector<std::string> allowedScopes; ///< Opaque ids; consumer decides their meaning (property ids, tenant ids, …).
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
#endif
|
||||||
31
include/oatpp-authkit/repo/IHistoryRepository.hpp
Normal file
31
include/oatpp-authkit/repo/IHistoryRepository.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_I_HISTORY_REPOSITORY_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_I_HISTORY_REPOSITORY_HPP
|
||||||
|
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief All historical versions for a temporal entity.
|
||||||
|
*
|
||||||
|
* Kept separate from `Repository<T>` deliberately — non-temporal repos
|
||||||
|
* (caches, lookup tables, anything without `valid_from` / `valid_until`)
|
||||||
|
* don't have a meaningful answer to `history()` and shouldn't be forced
|
||||||
|
* to implement a stub. The temporal decorator in oatpp-authkit#8 is the
|
||||||
|
* canonical implementer.
|
||||||
|
*
|
||||||
|
* Returns versions ordered ascending by `valid_from`, oldest first. An
|
||||||
|
* empty vector means the entity id was never seen.
|
||||||
|
*/
|
||||||
|
template <class TDto>
|
||||||
|
class IHistoryRepository {
|
||||||
|
public:
|
||||||
|
virtual ~IHistoryRepository() = default;
|
||||||
|
|
||||||
|
virtual oatpp::Vector<oatpp::Object<TDto>>
|
||||||
|
history(const oatpp::String& entityId) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
#endif
|
||||||
32
include/oatpp-authkit/repo/ITemporalEntity.hpp
Normal file
32
include/oatpp-authkit/repo/ITemporalEntity.hpp
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#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
|
||||||
68
include/oatpp-authkit/repo/Repository.hpp
Normal file
68
include/oatpp-authkit/repo/Repository.hpp
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_REPOSITORY_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_REPOSITORY_HPP
|
||||||
|
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pure-abstract per-DTO repository interface.
|
||||||
|
*
|
||||||
|
* Generic on `TDto`, temporal-agnostic. Concrete adapters wrap an
|
||||||
|
* `oatpp::orm::DbClient` (or any other store) and implement the four
|
||||||
|
* methods below. Cross-cutting concerns (temporal versioning, scope
|
||||||
|
* authorisation) are added by stacking decorators from oatpp-authkit#8
|
||||||
|
* around the concrete adapter at construction time.
|
||||||
|
*
|
||||||
|
* @section semantics Method semantics
|
||||||
|
*
|
||||||
|
* - `findByEntityId(entityId)` — Single live row matching `entity_id`.
|
||||||
|
* Returns null `oatpp::Object` when not found. The decorator that adds
|
||||||
|
* point-in-time reads exposes a different method (`findByEntityId(id, at)`)
|
||||||
|
* on its own surface; the abstract here stays narrow.
|
||||||
|
*
|
||||||
|
* - `list()` — All live rows for this entity type, no filtering. Filtered
|
||||||
|
* reads land in the optional `IQueryable<T>` capability tracked by
|
||||||
|
* oatpp-authkit#9; do not bake filter predicates into this base interface.
|
||||||
|
*
|
||||||
|
* - `save(dto)` — Mixed `entity_id` allocation:
|
||||||
|
* - If `dto->entity_id` is null on entry, the implementation generates
|
||||||
|
* a fresh UUID and writes it back to the DTO before persisting.
|
||||||
|
* - If `dto->entity_id` is non-null, it is used as-is.
|
||||||
|
* No upsert semantics are implied at this layer — the temporal decorator
|
||||||
|
* in oatpp-authkit#8 turns "save" into a versioning insert; without that
|
||||||
|
* decorator the concrete repo decides whether `save` is insert-or-update
|
||||||
|
* on its own.
|
||||||
|
*
|
||||||
|
* - `softDelete(entityId)` — Marks the row removed without erasing history.
|
||||||
|
* Concrete repos typically set a `deleted_at` column or its equivalent.
|
||||||
|
*
|
||||||
|
* @section design Design decisions (all settled in the issue body)
|
||||||
|
*
|
||||||
|
* 1. `entity_id` allocation is mixed (caller may supply or leave null).
|
||||||
|
* 2. UnitOfWork / cross-repo transactions are explicitly out of scope.
|
||||||
|
* 3. `Repository<T>` is a virtual-method interface, not a C++20 concept.
|
||||||
|
* 4. History queries live on a separate `IHistoryRepository<T>` so non-
|
||||||
|
* temporal repos don't have to implement them.
|
||||||
|
*/
|
||||||
|
template <class TDto>
|
||||||
|
class Repository {
|
||||||
|
public:
|
||||||
|
virtual ~Repository() = default;
|
||||||
|
|
||||||
|
/** @brief Single live row by stable entity id. Null oatpp::Object when not found. */
|
||||||
|
virtual oatpp::Object<TDto> findByEntityId(const oatpp::String& entityId) = 0;
|
||||||
|
|
||||||
|
/** @brief All live rows for this entity type (no filtering at this layer). */
|
||||||
|
virtual oatpp::Vector<oatpp::Object<TDto>> list() = 0;
|
||||||
|
|
||||||
|
/** @brief Persist DTO; allocate UUID for `entity_id` if null on entry. */
|
||||||
|
virtual void save(const oatpp::Object<TDto>& dto) = 0;
|
||||||
|
|
||||||
|
/** @brief Mark the row removed without erasing it. */
|
||||||
|
virtual void softDelete(const oatpp::String& entityId) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
#endif
|
||||||
33
include/oatpp-authkit/repo/TemporalAt.hpp
Normal file
33
include/oatpp-authkit/repo/TemporalAt.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_TEMPORAL_AT_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_TEMPORAL_AT_HPP
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Point-in-time selector for temporal reads.
|
||||||
|
*
|
||||||
|
* Concrete repositories implementing temporal versioning use this to
|
||||||
|
* choose between "live" (`valid_until = sentinel`) and "as-of a specific
|
||||||
|
* timestamp" reads. The interface is decoupled from any particular clock
|
||||||
|
* type; consumers pass milliseconds-since-epoch.
|
||||||
|
*/
|
||||||
|
struct TemporalAt {
|
||||||
|
enum class Kind { Live, At };
|
||||||
|
|
||||||
|
Kind kind{Kind::Live};
|
||||||
|
int64_t timestamp{0}; ///< Milliseconds since epoch; only meaningful when kind == At.
|
||||||
|
|
||||||
|
static TemporalAt live() {
|
||||||
|
return TemporalAt{Kind::Live, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
static TemporalAt at(int64_t ts) {
|
||||||
|
return TemporalAt{Kind::At, ts};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -21,3 +21,7 @@ add_test(NAME security_headers COMMAND test_security_headers)
|
||||||
add_executable(test_json_serialization test_json_serialization.cpp)
|
add_executable(test_json_serialization test_json_serialization.cpp)
|
||||||
target_link_libraries(test_json_serialization PRIVATE oatpp::authkit oatpp::oatpp)
|
target_link_libraries(test_json_serialization PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
add_test(NAME json_serialization COMMAND test_json_serialization)
|
add_test(NAME json_serialization COMMAND test_json_serialization)
|
||||||
|
|
||||||
|
add_executable(test_repository_interface test_repository_interface.cpp)
|
||||||
|
target_link_libraries(test_repository_interface PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
|
add_test(NAME repository_interface COMMAND test_repository_interface)
|
||||||
|
|
|
||||||
200
test/test_repository_interface.cpp
Normal file
200
test/test_repository_interface.cpp
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
// Tests for the oatpp-authkit#7 Repository<T> interface set. Exercises the
|
||||||
|
// contract through a trivial in-memory fake — confirms the abstract methods
|
||||||
|
// compile against an oatpp DTO, the mixed-id allocation branch on save() is
|
||||||
|
// implementable, and findByEntityId/list/softDelete round-trip as documented.
|
||||||
|
//
|
||||||
|
// No SQL involvement — that's the concrete adapters' job (out of scope).
|
||||||
|
|
||||||
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
|
#include "oatpp-authkit/repo/IHistoryRepository.hpp"
|
||||||
|
#include "oatpp-authkit/repo/ITemporalEntity.hpp"
|
||||||
|
#include "oatpp-authkit/repo/TemporalAt.hpp"
|
||||||
|
#include "oatpp-authkit/repo/ActorContext.hpp"
|
||||||
|
|
||||||
|
#include "oatpp/core/macro/codegen.hpp"
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <random>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MockDto : public oatpp::DTO {
|
||||||
|
DTO_INIT(MockDto, DTO)
|
||||||
|
DTO_FIELD(String, entity_id);
|
||||||
|
DTO_FIELD(String, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Trivial UUID-ish generator — sufficient for the in-memory fake; concrete
|
||||||
|
// adapters can use libuuid or similar in production.
|
||||||
|
std::string generateId() {
|
||||||
|
static std::mt19937_64 rng{std::random_device{}()};
|
||||||
|
char buf[33];
|
||||||
|
std::snprintf(buf, sizeof(buf), "%016llx%016llx",
|
||||||
|
(unsigned long long)rng(), (unsigned long long)rng());
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
class InMemoryRepo : public oatpp_authkit::repo::Repository<MockDto> {
|
||||||
|
std::unordered_map<std::string, oatpp::Object<MockDto>> live;
|
||||||
|
std::unordered_map<std::string, oatpp::Object<MockDto>> deleted;
|
||||||
|
public:
|
||||||
|
oatpp::Object<MockDto> findByEntityId(const oatpp::String& id) override {
|
||||||
|
auto it = live.find(*id);
|
||||||
|
return it == live.end() ? nullptr : it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
oatpp::Vector<oatpp::Object<MockDto>> list() override {
|
||||||
|
auto v = oatpp::Vector<oatpp::Object<MockDto>>::createShared();
|
||||||
|
for (auto& kv : live) v->push_back(kv.second);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
void save(const oatpp::Object<MockDto>& dto) override {
|
||||||
|
if (!dto->entity_id) {
|
||||||
|
dto->entity_id = generateId();
|
||||||
|
}
|
||||||
|
live[*dto->entity_id] = dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
void softDelete(const oatpp::String& id) override {
|
||||||
|
auto it = live.find(*id);
|
||||||
|
if (it != live.end()) {
|
||||||
|
deleted[*id] = it->second;
|
||||||
|
live.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void test_save_allocates_uuid_when_id_null() {
|
||||||
|
InMemoryRepo repo;
|
||||||
|
auto dto = MockDto::createShared();
|
||||||
|
dto->name = oatpp::String("alice");
|
||||||
|
REQUIRE(!dto->entity_id); // precondition: id is null
|
||||||
|
|
||||||
|
repo.save(dto);
|
||||||
|
|
||||||
|
REQUIRE(dto->entity_id); // id was filled in
|
||||||
|
REQUIRE(std::string(*dto->entity_id).size() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_save_uses_supplied_id_when_present() {
|
||||||
|
InMemoryRepo repo;
|
||||||
|
auto dto = MockDto::createShared();
|
||||||
|
dto->entity_id = oatpp::String("supplied-id-42");
|
||||||
|
dto->name = oatpp::String("bob");
|
||||||
|
|
||||||
|
repo.save(dto);
|
||||||
|
|
||||||
|
REQUIRE(std::string(*dto->entity_id) == "supplied-id-42");
|
||||||
|
auto loaded = repo.findByEntityId(oatpp::String("supplied-id-42"));
|
||||||
|
REQUIRE(loaded);
|
||||||
|
REQUIRE(std::string(*loaded->name) == "bob");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_find_by_entity_id_round_trip() {
|
||||||
|
InMemoryRepo repo;
|
||||||
|
auto dto = MockDto::createShared();
|
||||||
|
dto->entity_id = oatpp::String("abc");
|
||||||
|
dto->name = oatpp::String("carol");
|
||||||
|
repo.save(dto);
|
||||||
|
|
||||||
|
auto found = repo.findByEntityId(oatpp::String("abc"));
|
||||||
|
REQUIRE(found);
|
||||||
|
REQUIRE(std::string(*found->name) == "carol");
|
||||||
|
|
||||||
|
auto missing = repo.findByEntityId(oatpp::String("does-not-exist"));
|
||||||
|
REQUIRE(!missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_list_returns_all_live_rows() {
|
||||||
|
InMemoryRepo repo;
|
||||||
|
for (const char* n : {"a", "b", "c"}) {
|
||||||
|
auto dto = MockDto::createShared();
|
||||||
|
dto->entity_id = oatpp::String(n);
|
||||||
|
dto->name = oatpp::String(n);
|
||||||
|
repo.save(dto);
|
||||||
|
}
|
||||||
|
auto all = repo.list();
|
||||||
|
REQUIRE(all->size() == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_soft_delete_removes_from_live_view() {
|
||||||
|
InMemoryRepo repo;
|
||||||
|
auto dto = MockDto::createShared();
|
||||||
|
dto->entity_id = oatpp::String("delete-me");
|
||||||
|
dto->name = oatpp::String("doomed");
|
||||||
|
repo.save(dto);
|
||||||
|
REQUIRE(repo.findByEntityId(oatpp::String("delete-me")));
|
||||||
|
|
||||||
|
repo.softDelete(oatpp::String("delete-me"));
|
||||||
|
|
||||||
|
REQUIRE(!repo.findByEntityId(oatpp::String("delete-me")));
|
||||||
|
REQUIRE(repo.list()->size() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_temporal_at_value_type() {
|
||||||
|
using oatpp_authkit::repo::TemporalAt;
|
||||||
|
auto live = TemporalAt::live();
|
||||||
|
REQUIRE(live.kind == TemporalAt::Kind::Live);
|
||||||
|
|
||||||
|
auto pin = TemporalAt::at(1700000000000LL);
|
||||||
|
REQUIRE(pin.kind == TemporalAt::Kind::At);
|
||||||
|
REQUIRE(pin.timestamp == 1700000000000LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_actor_context_minimal() {
|
||||||
|
oatpp_authkit::repo::ActorContext ctx;
|
||||||
|
ctx.userId = "user-1";
|
||||||
|
ctx.allowedScopes = {"prop-A", "prop-B"};
|
||||||
|
REQUIRE(ctx.userId == "user-1");
|
||||||
|
REQUIRE(ctx.allowedScopes.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time check: ITemporalEntity is usable as a base marker.
|
||||||
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
||||||
|
|
||||||
|
class TemporalDto : public oatpp::DTO, public oatpp_authkit::repo::ITemporalEntity {
|
||||||
|
DTO_INIT(TemporalDto, DTO)
|
||||||
|
DTO_FIELD(String, entity_id);
|
||||||
|
DTO_FIELD(String, valid_from);
|
||||||
|
DTO_FIELD(String, valid_until);
|
||||||
|
};
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
|
void test_temporal_marker_compiles() {
|
||||||
|
auto dto = TemporalDto::createShared();
|
||||||
|
dto->entity_id = oatpp::String("t");
|
||||||
|
REQUIRE(std::string(*dto->entity_id) == "t");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_save_allocates_uuid_when_id_null();
|
||||||
|
test_save_uses_supplied_id_when_present();
|
||||||
|
test_find_by_entity_id_round_trip();
|
||||||
|
test_list_returns_all_live_rows();
|
||||||
|
test_soft_delete_removes_from_live_view();
|
||||||
|
test_temporal_at_value_type();
|
||||||
|
test_actor_context_minimal();
|
||||||
|
test_temporal_marker_compiles();
|
||||||
|
|
||||||
|
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