#14 PR 4: relocate users with temporal shape (Option B)
Lifts the auth-essential users table from fewo-webapp into oatpp-authkit in temporal form per Option B from the issue body. The previous shape (id INTEGER autoinc + is_active flag) is replaced with the entity_id + valid_from/valid_until triple; soft-delete via valid_until = now() instead of toggling is_active. New files (all in oatpp-authkit): - dto/UserDto.hpp — auth-essential columns only: id, entity_id, username, password_hash, role, tls_cert_dn, valid_from, valid_until. Registered as temporal so TemporalRepository composes cleanly. Application- specific columns (email, profile data) belong on a consumer-side DTO + parallel SchemaContract that contributes additional columns to the same users table. - db/UserDb.hpp — DbClient with login-path queries (findLiveByUsername, findLiveByTlsCertDn) plus generic CRUD. UserSchema declares the schema: TEXT id, entity_id, username, password_hash, role, tls_cert_dn, with natural-key UNIQUE on (username, valid_until) so no two live rows can share a username while historical rows for the same username are allowed. - repo/ConcreteUserRepository.hpp — Repository<UserDto> adapter + makeUserRepository factory wrapping in TemporalRepository. - test/test_user_schema.cpp — verifies SchemaBuilder<UserSchema, TemporalRepository<UserDto>>::create produces the expected 5 DDL statements; specifically asserts is_active and created_at are NOT present in the temporal shape (Option B replacement). 13 of 13 tests pass. Bumped 0.11.0 → 0.12.0. Per owner directive on authkit#14: password_hash rides the temporal row. A separate security follow-up issue tracks the redaction policy for historical password hashes (likely blank the hash but keep the row so change-history is auditable). The migration of an existing non-temporal users table to this shape is documented in db/UserDb.hpp: Atlas-generated migration handles the structural conversion + backfill (each existing row becomes its own entity with entity_id = CAST(id AS TEXT)). Sessions/certificates FKs that referenced users.id (INTEGER) need rewiring to reference users.entity_id — that's a consumer-side rewire, separate PR. Closes #14 — the four migration sub-PRs (PR 1 role_templates, PRs 2+3 permissions, PR 4 users) are now landed; the umbrella issue can close. Follow-ups (security hash redaction, fewo-webapp consumer migration, Atlas CI integration) get their own issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0bb8bef634
commit
9040a9ec48
6 changed files with 369 additions and 1 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(oatpp-authkit VERSION 0.11.0 LANGUAGES CXX)
|
project(oatpp-authkit VERSION 0.12.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:
|
||||||
|
|
|
||||||
140
include/oatpp-authkit/db/UserDb.hpp
Normal file
140
include/oatpp-authkit/db/UserDb.hpp
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_DB_USER_DB_HPP
|
||||||
|
#define OATPP_AUTHKIT_DB_USER_DB_HPP
|
||||||
|
|
||||||
|
// DbClient + declarative schema for temporal `users` (authkit#14 PR 4).
|
||||||
|
|
||||||
|
#include "oatpp-authkit/dto/UserDto.hpp"
|
||||||
|
#include "oatpp-authkit/repo/SchemaContract.hpp"
|
||||||
|
|
||||||
|
#include "oatpp-sqlite/orm.hpp"
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_BEGIN(DbClient)
|
||||||
|
|
||||||
|
namespace oatpp_authkit::db {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief DbClient for the temporal `users` table.
|
||||||
|
*
|
||||||
|
* Login lookup goes through `findLiveByUsername` — the natural-key
|
||||||
|
* index `ux_users_username_until` makes that an indexed scan. The
|
||||||
|
* temporal decorator on top filters live-vs-historical itself for the
|
||||||
|
* generic `Repository<T>` surface; the dedicated find-by-username here
|
||||||
|
* exists because login doesn't have an `entity_id` to dispatch on.
|
||||||
|
*/
|
||||||
|
class UserDb : public oatpp::orm::DbClient {
|
||||||
|
public:
|
||||||
|
UserDb(const std::shared_ptr<oatpp::orm::Executor>& executor)
|
||||||
|
: oatpp::orm::DbClient(executor) {}
|
||||||
|
|
||||||
|
QUERY(getAllUsersRaw,
|
||||||
|
"SELECT * FROM users;")
|
||||||
|
|
||||||
|
QUERY(getLiveUsers,
|
||||||
|
"SELECT * FROM users "
|
||||||
|
"WHERE valid_from <= datetime('now') AND valid_until > datetime('now') "
|
||||||
|
"ORDER BY username;")
|
||||||
|
|
||||||
|
QUERY(findUserByEntityId,
|
||||||
|
"SELECT * FROM users "
|
||||||
|
"WHERE entity_id = :entityId "
|
||||||
|
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
|
||||||
|
PARAM(oatpp::String, entityId))
|
||||||
|
|
||||||
|
/// Live row by username — the canonical login lookup path.
|
||||||
|
QUERY(findLiveByUsername,
|
||||||
|
"SELECT * FROM users "
|
||||||
|
"WHERE username = :username "
|
||||||
|
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
|
||||||
|
PARAM(oatpp::String, username))
|
||||||
|
|
||||||
|
/// Live row by tls_cert_dn — used by mTLS auth.
|
||||||
|
QUERY(findLiveByTlsCertDn,
|
||||||
|
"SELECT * FROM users "
|
||||||
|
"WHERE tls_cert_dn = :dn "
|
||||||
|
" AND valid_from <= datetime('now') AND valid_until > datetime('now');",
|
||||||
|
PARAM(oatpp::String, dn))
|
||||||
|
|
||||||
|
QUERY(upsertUserById,
|
||||||
|
"INSERT INTO users "
|
||||||
|
" (id, entity_id, username, password_hash, role, tls_cert_dn, "
|
||||||
|
" valid_from, valid_until) "
|
||||||
|
"VALUES "
|
||||||
|
" (:dto.id, :dto.entityId, :dto.username, :dto.passwordHash, "
|
||||||
|
" :dto.role, :dto.tlsCertDn, :dto.validFrom, :dto.validUntil) "
|
||||||
|
"ON CONFLICT(id) DO UPDATE SET "
|
||||||
|
" entity_id = excluded.entity_id, "
|
||||||
|
" username = excluded.username, "
|
||||||
|
" password_hash = excluded.password_hash, "
|
||||||
|
" role = excluded.role, "
|
||||||
|
" tls_cert_dn = excluded.tls_cert_dn, "
|
||||||
|
" valid_from = excluded.valid_from, "
|
||||||
|
" valid_until = excluded.valid_until;",
|
||||||
|
PARAM(oatpp::Object<dto::UserDto>, dto))
|
||||||
|
|
||||||
|
QUERY(softDeleteUser,
|
||||||
|
"UPDATE users SET valid_until = datetime('now') "
|
||||||
|
"WHERE entity_id = :entityId AND valid_until > datetime('now');",
|
||||||
|
PARAM(oatpp::String, entityId))
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declarative schema for `users` (auth-essential columns only).
|
||||||
|
*
|
||||||
|
* Composes with `TemporalRepository<UserDto>` and any consumer-side
|
||||||
|
* `*UserExtensionSchema` that contributes additional columns (email,
|
||||||
|
* profile data, …). The natural-key UNIQUE on `(username, valid_until)`
|
||||||
|
* prevents two live rows from sharing a username while still allowing
|
||||||
|
* historical rows; same for `(tls_cert_dn, valid_until)` (skipped when
|
||||||
|
* `tls_cert_dn IS NULL`, expressed via partial index below).
|
||||||
|
*
|
||||||
|
* @section migration Migration from a non-temporal users table
|
||||||
|
*
|
||||||
|
* Atlas-generated migration handles the structural conversion:
|
||||||
|
*
|
||||||
|
* 1. Rebuild `users` with the new column shape (TEXT id, entity_id,
|
||||||
|
* valid_from, valid_until; drop is_active, created_at).
|
||||||
|
* 2. Backfill: each existing row becomes its own entity:
|
||||||
|
* `entity_id = CAST(old_id AS TEXT)`,
|
||||||
|
* `id = CAST(old_id AS TEXT)`,
|
||||||
|
* `valid_from = COALESCE(old_created_at, datetime('now'))`,
|
||||||
|
* `valid_until = CASE WHEN old_is_active = 1 THEN '<sentinel>'
|
||||||
|
* ELSE datetime('now') END`.
|
||||||
|
* 3. Sessions/certificates FKs that referenced `users.id` (INTEGER) get
|
||||||
|
* rewired to reference `users.entity_id` — that's a consumer-side
|
||||||
|
* rewire, not part of this PR. The migration generated by Atlas
|
||||||
|
* will surface those FK changes for review.
|
||||||
|
*/
|
||||||
|
struct UserSchema {
|
||||||
|
inline static constexpr repo::ColumnSpec kColumns[] = {
|
||||||
|
{"id", "TEXT PRIMARY KEY"},
|
||||||
|
{"entity_id", "TEXT NOT NULL"},
|
||||||
|
{"username", "TEXT NOT NULL"},
|
||||||
|
{"password_hash", "TEXT"},
|
||||||
|
{"role", "TEXT NOT NULL DEFAULT 'editor'"},
|
||||||
|
{"tls_cert_dn", "TEXT"},
|
||||||
|
// valid_from / valid_until come from TemporalRepository.
|
||||||
|
};
|
||||||
|
inline static constexpr repo::IndexSpec kIndexes[] = {
|
||||||
|
{"ix_{table}_entity_id", false, "(entity_id)"},
|
||||||
|
{"ux_{table}_username_until", true, "(username, valid_until)"},
|
||||||
|
// tls_cert_dn UNIQUE is expressed as a partial index; the
|
||||||
|
// SchemaBuilder index emitter doesn't yet support WHERE clauses
|
||||||
|
// on indexes, so a regular index here lets duplicate-NULL rows
|
||||||
|
// through. Consumers can layer a partial UNIQUE in their own
|
||||||
|
// schema contribution if needed.
|
||||||
|
{"ix_{table}_tls_cert_dn", false, "(tls_cert_dn)"},
|
||||||
|
};
|
||||||
|
|
||||||
|
inline static constexpr repo::DecoratorSchema kSchema = {
|
||||||
|
"UserSchema",
|
||||||
|
kColumns, sizeof(kColumns)/sizeof(kColumns[0]),
|
||||||
|
kIndexes, sizeof(kIndexes)/sizeof(kIndexes[0]),
|
||||||
|
nullptr, 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::db
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_END(DbClient)
|
||||||
|
|
||||||
|
#endif
|
||||||
63
include/oatpp-authkit/dto/UserDto.hpp
Normal file
63
include/oatpp-authkit/dto/UserDto.hpp
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_DTO_USER_DTO_HPP
|
||||||
|
#define OATPP_AUTHKIT_DTO_USER_DTO_HPP
|
||||||
|
|
||||||
|
// Temporal `users` DTO (authkit#14 PR 4, Option B).
|
||||||
|
//
|
||||||
|
// Ships the auth-essential columns: id (TEXT PK), entity_id, username,
|
||||||
|
// password_hash, role, tls_cert_dn, plus the temporal triple. Consumers
|
||||||
|
// add application-specific columns (email, profile data, …) by
|
||||||
|
// contributing a second `*Schema` to the SchemaBuilder parameter pack.
|
||||||
|
//
|
||||||
|
// **Migration from non-temporal users**: existing fewo-webapp `users`
|
||||||
|
// rows have `id INTEGER autoinc` and `is_active` flag. Atlas-generated
|
||||||
|
// migration (per docs/MIGRATIONS.md) handles the conversion: each row
|
||||||
|
// becomes its own entity (`entity_id = CAST(id AS TEXT)`), `valid_until
|
||||||
|
// = SENTINEL` for active users and `= datetime('now')` for inactive
|
||||||
|
// ones. Sessions/certificates FKs to `users.id` move to `users.entity_id`
|
||||||
|
// (consumer-side rewire — out of scope for this PR).
|
||||||
|
//
|
||||||
|
// **Password hash temporality**: per owner directive on authkit#14,
|
||||||
|
// password_hash rides the temporal row. A separate issue (filed by this
|
||||||
|
// PR) tracks the redaction policy for historical hashes — likely blank
|
||||||
|
// the hash but keep the row so the change-history is auditable.
|
||||||
|
|
||||||
|
#include "oatpp/core/macro/codegen.hpp"
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_BEGIN(DTO)
|
||||||
|
|
||||||
|
namespace oatpp_authkit::dto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Auth-essential view of an application user.
|
||||||
|
*
|
||||||
|
* The `password` write-only field is intentionally absent here — it
|
||||||
|
* arrives via the consumer's auth controller (login / password-set
|
||||||
|
* endpoints) and gets hashed before reaching `password_hash` on this
|
||||||
|
* DTO. Consumers that ship richer user profiles add application-
|
||||||
|
* specific columns through their own DTO + a parallel SchemaContract.
|
||||||
|
*/
|
||||||
|
class UserDto : public oatpp::DTO {
|
||||||
|
DTO_INIT(UserDto, DTO)
|
||||||
|
|
||||||
|
DTO_FIELD(String, id);
|
||||||
|
DTO_FIELD(String, entityId, "entity_id");
|
||||||
|
DTO_FIELD(String, username);
|
||||||
|
DTO_FIELD(String, passwordHash, "password_hash");
|
||||||
|
DTO_FIELD(String, role);
|
||||||
|
DTO_FIELD(String, tlsCertDn, "tls_cert_dn");
|
||||||
|
DTO_FIELD(String, validFrom, "valid_from");
|
||||||
|
DTO_FIELD(String, validUntil, "valid_until");
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::dto
|
||||||
|
|
||||||
|
#include OATPP_CODEGEN_END(DTO)
|
||||||
|
|
||||||
|
#include "oatpp-authkit/repo/TemporalFieldTraits.hpp"
|
||||||
|
|
||||||
|
OATPP_AUTHKIT_REGISTER_TEMPORAL(
|
||||||
|
oatpp_authkit::dto::UserDto,
|
||||||
|
id, entityId, validFrom, validUntil)
|
||||||
|
|
||||||
|
#endif
|
||||||
72
include/oatpp-authkit/repo/ConcreteUserRepository.hpp
Normal file
72
include/oatpp-authkit/repo/ConcreteUserRepository.hpp
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#ifndef OATPP_AUTHKIT_REPO_CONCRETE_USER_REPOSITORY_HPP
|
||||||
|
#define OATPP_AUTHKIT_REPO_CONCRETE_USER_REPOSITORY_HPP
|
||||||
|
|
||||||
|
// Concrete inner adapter of `Repository<UserDto>` (authkit#14 PR 4).
|
||||||
|
// Stacks under TemporalRepository<UserDto> via `makeUserRepository`.
|
||||||
|
|
||||||
|
#include "oatpp-authkit/db/UserDb.hpp"
|
||||||
|
#include "oatpp-authkit/dto/UserDto.hpp"
|
||||||
|
#include "oatpp-authkit/repo/Repository.hpp"
|
||||||
|
#include "oatpp-authkit/repo/SchemaContract.hpp"
|
||||||
|
#include "oatpp-authkit/repo/TemporalRepository.hpp"
|
||||||
|
|
||||||
|
#include "oatpp/core/Types.hpp"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace oatpp_authkit::repo {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inner adapter of `Repository<UserDto>`, delegating to `db::UserDb`.
|
||||||
|
*
|
||||||
|
* Empty schema — `db::UserSchema` owns the table declaration.
|
||||||
|
*/
|
||||||
|
class ConcreteUserRepository : public Repository<dto::UserDto> {
|
||||||
|
public:
|
||||||
|
inline static constexpr DecoratorSchema kSchema = {
|
||||||
|
"ConcreteUserRepository",
|
||||||
|
nullptr, 0, nullptr, 0, nullptr, 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit ConcreteUserRepository(std::shared_ptr<db::UserDb> udb)
|
||||||
|
: m_db(std::move(udb)) {}
|
||||||
|
|
||||||
|
oatpp::Object<dto::UserDto> findByEntityId(const oatpp::String& entityId) override {
|
||||||
|
auto res = m_db->findUserByEntityId(entityId);
|
||||||
|
if (!res || !res->isSuccess()) return nullptr;
|
||||||
|
auto rows = res->template fetch<oatpp::Vector<oatpp::Object<dto::UserDto>>>();
|
||||||
|
if (!rows || rows->empty()) return nullptr;
|
||||||
|
return (*rows)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
oatpp::Vector<oatpp::Object<dto::UserDto>> list() override {
|
||||||
|
auto res = m_db->getAllUsersRaw();
|
||||||
|
auto out = oatpp::Vector<oatpp::Object<dto::UserDto>>::createShared();
|
||||||
|
if (!res || !res->isSuccess()) return out;
|
||||||
|
auto fetched = res->template fetch<oatpp::Vector<oatpp::Object<dto::UserDto>>>();
|
||||||
|
if (!fetched) return out;
|
||||||
|
for (auto& row : *fetched) if (row) out->push_back(row);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void save(const oatpp::Object<dto::UserDto>& d) override {
|
||||||
|
m_db->upsertUserById(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
void softDelete(const oatpp::String& entityId) override {
|
||||||
|
m_db->softDeleteUser(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::shared_ptr<db::UserDb> m_db;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::shared_ptr<Repository<dto::UserDto>>
|
||||||
|
makeUserRepository(std::shared_ptr<db::UserDb> udb) {
|
||||||
|
auto concrete = std::make_shared<ConcreteUserRepository>(std::move(udb));
|
||||||
|
return std::make_shared<TemporalRepository<dto::UserDto>>(concrete);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace oatpp_authkit::repo
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -62,4 +62,9 @@ if(oatpp-sqlite_FOUND AND Threads_FOUND)
|
||||||
target_link_libraries(test_user_permission_schema
|
target_link_libraries(test_user_permission_schema
|
||||||
PRIVATE oatpp::authkit oatpp::oatpp oatpp::oatpp-sqlite Threads::Threads)
|
PRIVATE oatpp::authkit oatpp::oatpp oatpp::oatpp-sqlite Threads::Threads)
|
||||||
add_test(NAME user_permission_schema COMMAND test_user_permission_schema)
|
add_test(NAME user_permission_schema COMMAND test_user_permission_schema)
|
||||||
|
|
||||||
|
add_executable(test_user_schema test_user_schema.cpp)
|
||||||
|
target_link_libraries(test_user_schema
|
||||||
|
PRIVATE oatpp::authkit oatpp::oatpp oatpp::oatpp-sqlite Threads::Threads)
|
||||||
|
add_test(NAME user_schema COMMAND test_user_schema)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
88
test/test_user_schema.cpp
Normal file
88
test/test_user_schema.cpp
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Tests for authkit#14 PR 4 — temporal users schema.
|
||||||
|
|
||||||
|
#include "oatpp-authkit/db/UserDb.hpp"
|
||||||
|
#include "oatpp-authkit/dto/UserDto.hpp"
|
||||||
|
#include "oatpp-authkit/repo/ConcreteUserRepository.hpp"
|
||||||
|
#include "oatpp-authkit/repo/SchemaContract.hpp"
|
||||||
|
#include "oatpp-authkit/repo/TemporalRepository.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define REQUIRE(cond) do { \
|
||||||
|
if (!(cond)) { std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", \
|
||||||
|
#cond, __FILE__, __LINE__); std::abort(); } } while (0)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool contains(const std::string& haystack, const std::string& needle) {
|
||||||
|
return haystack.find(needle) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_users_temporal_create() {
|
||||||
|
using namespace oatpp_authkit::repo;
|
||||||
|
using namespace oatpp_authkit::db;
|
||||||
|
using namespace oatpp_authkit::dto;
|
||||||
|
|
||||||
|
std::vector<std::string> sqls;
|
||||||
|
SqlExec exec = [&](const std::string& sql) { sqls.push_back(sql); };
|
||||||
|
|
||||||
|
SchemaBuilder<
|
||||||
|
UserSchema,
|
||||||
|
TemporalRepository<UserDto>>::create("users", exec);
|
||||||
|
|
||||||
|
// 1 entity table + 3 schema-side indexes + 1 temporal composite index = 5
|
||||||
|
REQUIRE(sqls.size() == 5);
|
||||||
|
|
||||||
|
REQUIRE(contains(sqls[0], "CREATE TABLE IF NOT EXISTS users"));
|
||||||
|
REQUIRE(contains(sqls[0], "id TEXT PRIMARY KEY"));
|
||||||
|
REQUIRE(contains(sqls[0], "entity_id TEXT NOT NULL"));
|
||||||
|
REQUIRE(contains(sqls[0], "username TEXT NOT NULL"));
|
||||||
|
REQUIRE(contains(sqls[0], "password_hash TEXT"));
|
||||||
|
REQUIRE(contains(sqls[0], "role TEXT NOT NULL DEFAULT 'editor'"));
|
||||||
|
REQUIRE(contains(sqls[0], "valid_from TEXT NOT NULL DEFAULT ''"));
|
||||||
|
REQUIRE(contains(sqls[0], "valid_until TEXT NOT NULL DEFAULT '9999-12-31T23:59:59Z'"));
|
||||||
|
// is_active and created_at must NOT appear — those are dropped in
|
||||||
|
// the temporal shape (Option B).
|
||||||
|
REQUIRE(!contains(sqls[0], "is_active"));
|
||||||
|
REQUIRE(!contains(sqls[0], "created_at"));
|
||||||
|
|
||||||
|
REQUIRE(contains(sqls[1], "ix_users_entity_id"));
|
||||||
|
REQUIRE(contains(sqls[2], "ux_users_username_until"));
|
||||||
|
REQUIRE(contains(sqls[2], "(username, valid_until)"));
|
||||||
|
REQUIRE(contains(sqls[3], "ix_users_tls_cert_dn"));
|
||||||
|
REQUIRE(contains(sqls[4], "ux_users_entity_valid_until"));
|
||||||
|
REQUIRE(contains(sqls[4], "(entity_id, valid_until)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConcreteUserRepository contributes nothing; ensure SchemaBuilder is
|
||||||
|
// idempotent w.r.t. its presence in the parameter pack.
|
||||||
|
void test_concrete_user_repo_no_schema() {
|
||||||
|
using namespace oatpp_authkit::repo;
|
||||||
|
using namespace oatpp_authkit::db;
|
||||||
|
using namespace oatpp_authkit::dto;
|
||||||
|
|
||||||
|
std::vector<std::string> with_repo;
|
||||||
|
std::vector<std::string> without_repo;
|
||||||
|
|
||||||
|
SchemaBuilder<
|
||||||
|
UserSchema, ConcreteUserRepository, TemporalRepository<UserDto>>::create(
|
||||||
|
"users", [&](const std::string& s){ with_repo.push_back(s); });
|
||||||
|
|
||||||
|
SchemaBuilder<
|
||||||
|
UserSchema, TemporalRepository<UserDto>>::create(
|
||||||
|
"users", [&](const std::string& s){ without_repo.push_back(s); });
|
||||||
|
|
||||||
|
REQUIRE(with_repo == without_repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
test_users_temporal_create();
|
||||||
|
test_concrete_user_repo_no_schema();
|
||||||
|
std::printf("test_user_schema: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue