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>
127 lines
4.5 KiB
C++
127 lines
4.5 KiB
C++
// 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, row_pk);
|
|
DTO_FIELD(String, id); // entity_id (logical), per the original test intent
|
|
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, row_pk, 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::string, oatpp::Object<OddNamesDto>> rows; // keyed by row_pk
|
|
public:
|
|
oatpp::Object<OddNamesDto> findByEntityId(const oatpp::String& id) override {
|
|
for (auto& kv : rows) if (kv.second->id && std::string(*kv.second->id) == 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[std::string(*dto->row_pk)] = dto;
|
|
}
|
|
void softDelete(const oatpp::String& id) override {
|
|
for (auto it = rows.begin(); it != rows.end(); ) {
|
|
if (it->second->id && std::string(*it->second->id) == 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;
|
|
}
|