Lifts role_templates / role_template_fields / user_role_assignments from fewo-webapp into oatpp-authkit, exposed via the declarative SchemaContract introduced in PR 0. New files (all in oatpp-authkit): - dto/RoleTemplateDto.hpp — RoleTemplateDto, RoleTemplateFieldDto, UserRoleAssignmentDto. UserWithPermissionsDto stays in fewo (fewo- specific /api/auth/me response shape). - db/RoleTemplateDb.hpp — DbClient with all queries (CRUD + cascade soft-delete + getEffectiveFieldPermissions). RoleTemplateSchema struct declares the three tables' columns/indexes/sidecar tables in the new declarative form. TemporalRepository overlays valid_until + the composite UNIQUE(entity_id, valid_until) index. - repo/ConcreteRoleTemplateRepository.hpp — Repository<RoleTemplateDto> inner adapter; makeRoleTemplateRepository helper composes the stack. - docs/MIGRATIONS.md — Atlas workflow for consumers (atlasgo.io as the diff-driven migration tool; SchemaBuilder produces desired state, Atlas generates versioned SQL, SchemaContract::verify asserts at runtime). - test/test_role_template_schema.cpp — verifies SchemaBuilder< RoleTemplateSchema, TemporalRepository<RoleTemplateDto>> emits the expected 5 DDL statements (2 sidecars + entity table + 2 indexes) with composite-FK + ON UPDATE CASCADE on both sidecars. 11 of 11 tests pass. RoleTemplateDto is registered as temporal via OATPP_AUTHKIT_REGISTER_TEMPORAL so TemporalRepository compiles cleanly. Atlas binary integration in CI is documented but not yet wired — owner deferred to a follow-up after the first concrete consumer migration. The shipped role_templates stack itself is fully consumable today; fewo- webapp's switch from local copies to oatpp-authkit-shipped headers is the natural next PR. Bumped 0.9.0 → 0.10.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
277 lines
12 KiB
C++
277 lines
12 KiB
C++
#ifndef OATPP_AUTHKIT_DB_ROLE_TEMPLATE_DB_HPP
|
|
#define OATPP_AUTHKIT_DB_ROLE_TEMPLATE_DB_HPP
|
|
|
|
// DbClient + declarative schema contribution for role templates,
|
|
// field permissions, and user role assignments (authkit#14 PR 1).
|
|
//
|
|
// Lifted from fewo-webapp's `src/db/RoleTemplateDb.hpp`. The queries are
|
|
// unchanged; new in this header is `RoleTemplateSchema::kSchema`, which
|
|
// declares the columns/indexes/sidecar tables this module needs in the
|
|
// declarative `SchemaContract` style introduced in PR 0.
|
|
|
|
#include "oatpp-authkit/dto/RoleTemplateDto.hpp"
|
|
#include "oatpp-authkit/repo/SchemaContract.hpp"
|
|
|
|
#include "oatpp-sqlite/orm.hpp"
|
|
|
|
#include OATPP_CODEGEN_BEGIN(DbClient)
|
|
|
|
namespace oatpp_authkit::db {
|
|
|
|
/**
|
|
* @brief DbClient for role templates / field permissions / user assignments.
|
|
*
|
|
* @section schema Schema contract
|
|
*
|
|
* `RoleTemplateSchema::kSchema` (defined below) names the three tables
|
|
* this module owns: `role_templates` (entity), `role_template_fields`
|
|
* (sidecar with composite-FK), `user_role_assignments` (sidecar with
|
|
* composite-FK). Composes into a `SchemaBuilder` parameter pack alongside
|
|
* `TemporalRepository<RoleTemplateDto>` to produce the full schema.
|
|
*
|
|
* @section queries Queries
|
|
*
|
|
* All temporal CRUD goes through the `TemporalRepository<RoleTemplateDto>`
|
|
* decorator on top of `ConcreteRoleTemplateRepository`. The DbClient
|
|
* methods below cover queries that don't fit the basic Repository<T>
|
|
* contract (effective-permission resolution, cascade soft-delete) and the
|
|
* raw queries the concrete repo uses internally.
|
|
*/
|
|
class RoleTemplateDb : public oatpp::orm::DbClient {
|
|
public:
|
|
RoleTemplateDb(const std::shared_ptr<oatpp::orm::Executor>& executor)
|
|
: oatpp::orm::DbClient(executor) {}
|
|
|
|
// ========== Role Templates (basic temporal CRUD) ==========
|
|
|
|
/// All live templates, ordered by name. Used by the controller list endpoint.
|
|
QUERY(getAllTemplates,
|
|
"SELECT * FROM role_templates "
|
|
"WHERE valid_from <= datetime('now') AND valid_until > datetime('now') "
|
|
"ORDER BY name;")
|
|
|
|
/// All rows (live + historical) for the temporal decorator's filter.
|
|
QUERY(getAllTemplatesRaw,
|
|
"SELECT * FROM role_templates;")
|
|
|
|
QUERY(getTemplateByEntityId,
|
|
"SELECT * FROM role_templates "
|
|
"WHERE entity_id = :id "
|
|
"AND valid_from <= datetime('now') AND valid_until > datetime('now');",
|
|
PARAM(oatpp::String, id))
|
|
|
|
/// Upsert keyed by `id` (per-row PK), per the Repository<T> contract
|
|
/// for temporal stacks. The temporal decorator drives close-then-insert
|
|
/// via this single method.
|
|
QUERY(upsertTemplateById,
|
|
"INSERT INTO role_templates "
|
|
" (id, entity_id, name, description, is_system, valid_from, valid_until) "
|
|
"VALUES "
|
|
" (:dto.id, :dto.entityId, :dto.name, :dto.description, "
|
|
" :dto.isSystem, :dto.validFrom, :dto.validUntil) "
|
|
"ON CONFLICT(id) DO UPDATE SET "
|
|
" entity_id = excluded.entity_id, "
|
|
" name = excluded.name, "
|
|
" description = excluded.description, "
|
|
" is_system = excluded.is_system, "
|
|
" valid_from = excluded.valid_from, "
|
|
" valid_until = excluded.valid_until;",
|
|
PARAM(oatpp::Object<dto::RoleTemplateDto>, dto))
|
|
|
|
QUERY(softDeleteTemplate,
|
|
"UPDATE role_templates SET valid_until = datetime('now') "
|
|
"WHERE entity_id = :id AND valid_until > datetime('now');",
|
|
PARAM(oatpp::String, id))
|
|
|
|
/// Cascade the soft-delete to link rows. On fresh installs the
|
|
/// composite FK + ON UPDATE CASCADE handles this automatically; these
|
|
/// explicit UPDATEs are defensive.
|
|
QUERY(cascadeSoftDeleteFields,
|
|
"UPDATE role_template_fields SET template_valid_until = datetime('now') "
|
|
"WHERE template_id = :id AND template_valid_until > datetime('now');",
|
|
PARAM(oatpp::String, id))
|
|
|
|
QUERY(cascadeSoftDeleteAssignments,
|
|
"UPDATE user_role_assignments SET template_valid_until = datetime('now') "
|
|
"WHERE template_id = :id AND template_valid_until > datetime('now');",
|
|
PARAM(oatpp::String, id))
|
|
|
|
// ========== Template Fields ==========
|
|
|
|
QUERY(getFieldsForTemplate,
|
|
"SELECT * FROM role_template_fields "
|
|
"WHERE template_id = :templateId "
|
|
"ORDER BY entity_type, field_name;",
|
|
PARAM(oatpp::String, templateId))
|
|
|
|
QUERY(insertField,
|
|
"INSERT OR REPLACE INTO role_template_fields "
|
|
" (id, template_id, entity_type, field_name, permission) "
|
|
"VALUES "
|
|
" (:dto.id, :dto.templateId, :dto.entityType, :dto.fieldName, "
|
|
" :dto.permission);",
|
|
PARAM(oatpp::Object<dto::RoleTemplateFieldDto>, dto))
|
|
|
|
QUERY(deleteField,
|
|
"DELETE FROM role_template_fields WHERE id = :id;",
|
|
PARAM(oatpp::String, id))
|
|
|
|
QUERY(deleteFieldsForTemplate,
|
|
"DELETE FROM role_template_fields WHERE template_id = :templateId;",
|
|
PARAM(oatpp::String, templateId))
|
|
|
|
// ========== User Assignments ==========
|
|
|
|
QUERY(getAllAssignments,
|
|
"SELECT * FROM user_role_assignments "
|
|
"WHERE valid_from <= datetime('now') AND valid_until > datetime('now') "
|
|
"ORDER BY user_id;")
|
|
|
|
QUERY(getAssignmentsForUser,
|
|
"SELECT * FROM user_role_assignments "
|
|
"WHERE user_id = :userId "
|
|
"AND valid_from <= datetime('now') AND valid_until > datetime('now');",
|
|
PARAM(oatpp::String, userId))
|
|
|
|
QUERY(insertAssignment,
|
|
"INSERT INTO user_role_assignments "
|
|
" (id, entity_id, user_id, template_id, property_id) "
|
|
"VALUES "
|
|
" (:dto.id, :dto.entityId, :dto.userId, :dto.templateId, "
|
|
" :dto.propertyId);",
|
|
PARAM(oatpp::Object<dto::UserRoleAssignmentDto>, dto))
|
|
|
|
QUERY(softDeleteAssignment,
|
|
"UPDATE user_role_assignments SET valid_until = datetime('now') "
|
|
"WHERE entity_id = :entityId AND valid_until > datetime('now');",
|
|
PARAM(oatpp::String, entityId))
|
|
|
|
// ========== Field Permission Resolution ==========
|
|
|
|
/**
|
|
* @brief Effective field permissions for a user on one entity type.
|
|
*
|
|
* Combines all active template assignments. If a user has multiple
|
|
* templates (e.g. property-scoped), takes the MAX permission per field
|
|
* (write > readonly > hidden). Returns only explicitly granted fields
|
|
* — unlisted fields are denied.
|
|
*
|
|
* The composite-temporal joins (`template_valid_until > now()`) make
|
|
* soft-deleted templates drop out automatically.
|
|
*/
|
|
QUERY(getEffectiveFieldPermissions,
|
|
"SELECT rtf.entity_type, rtf.field_name, MAX(rtf.permission) AS permission "
|
|
"FROM user_role_assignments ura "
|
|
"JOIN role_template_fields rtf ON rtf.template_id = ura.template_id "
|
|
"WHERE ura.user_id = :userId "
|
|
" AND ura.valid_from <= datetime('now') AND ura.valid_until > datetime('now') "
|
|
" AND ura.template_valid_until > datetime('now') "
|
|
" AND rtf.template_valid_until > datetime('now') "
|
|
" AND rtf.entity_type = :entityType "
|
|
" AND (ura.property_id IS NULL OR ura.property_id = :propertyId) "
|
|
"GROUP BY rtf.entity_type, rtf.field_name;",
|
|
PARAM(oatpp::String, userId),
|
|
PARAM(oatpp::String, entityType),
|
|
PARAM(oatpp::String, propertyId))
|
|
|
|
QUERY(getAllEffectiveFieldPermissions,
|
|
"SELECT rtf.entity_type, rtf.field_name, MAX(rtf.permission) AS permission "
|
|
"FROM user_role_assignments ura "
|
|
"JOIN role_template_fields rtf ON rtf.template_id = ura.template_id "
|
|
"WHERE ura.user_id = :userId "
|
|
" AND ura.valid_from <= datetime('now') AND ura.valid_until > datetime('now') "
|
|
" AND ura.template_valid_until > datetime('now') "
|
|
" AND rtf.template_valid_until > datetime('now') "
|
|
"GROUP BY rtf.entity_type, rtf.field_name;",
|
|
PARAM(oatpp::String, userId))
|
|
};
|
|
|
|
/**
|
|
* @brief Declarative schema contribution for the role-templates module.
|
|
*
|
|
* Three tables: `role_templates` (the entity), plus the two sidecars
|
|
* `role_template_fields` and `user_role_assignments` that carry the
|
|
* composite-FK temporal partner column `template_valid_until`.
|
|
*
|
|
* Designed to compose into a `SchemaBuilder` parameter pack alongside
|
|
* `TemporalRepository<RoleTemplateDto>`:
|
|
*
|
|
* @code
|
|
* SchemaBuilder<
|
|
* RoleTemplateSchema,
|
|
* TemporalRepository<RoleTemplateDto>>::create("role_templates", exec);
|
|
* @endcode
|
|
*
|
|
* The `id`, `entity_id`, business columns, and a `valid_from`-with-default
|
|
* are contributed here; the temporal decorator's `kSchema` overlays
|
|
* `valid_until` with the SENTINEL default + the composite UNIQUE index.
|
|
*
|
|
* The sidecar tables are emitted by name (no `{table}` substitution) and
|
|
* carry the composite FK to `role_templates(entity_id, valid_until)`.
|
|
*/
|
|
struct RoleTemplateSchema {
|
|
inline static constexpr repo::ColumnSpec kRoleTemplateColumns[] = {
|
|
{"id", "TEXT PRIMARY KEY"},
|
|
{"entity_id", "TEXT NOT NULL"},
|
|
{"name", "TEXT NOT NULL"},
|
|
{"description", "TEXT NOT NULL DEFAULT ''"},
|
|
{"is_system", "INTEGER NOT NULL DEFAULT 0"},
|
|
{"valid_from", "TEXT NOT NULL DEFAULT (datetime('now'))"},
|
|
// valid_until is contributed by TemporalRepository's kSchema,
|
|
// along with the composite UNIQUE(entity_id, valid_until).
|
|
};
|
|
inline static constexpr repo::IndexSpec kRoleTemplateIndexes[] = {
|
|
{"ix_{table}_entity_id", false, "(entity_id)"},
|
|
};
|
|
|
|
inline static constexpr repo::ColumnSpec kFieldColumns[] = {
|
|
{"id", "TEXT PRIMARY KEY"},
|
|
{"template_id", "TEXT NOT NULL"},
|
|
{"template_valid_until", "TEXT NOT NULL DEFAULT '9999-12-31T23:59:59Z'"},
|
|
{"entity_type", "TEXT NOT NULL"},
|
|
{"field_name", "TEXT NOT NULL"},
|
|
{"permission", "TEXT NOT NULL"},
|
|
{"_fk_to_role_templates",
|
|
"FOREIGN KEY (template_id, template_valid_until) REFERENCES "
|
|
"role_templates(entity_id, valid_until) ON UPDATE CASCADE"},
|
|
};
|
|
|
|
inline static constexpr repo::ColumnSpec kAssignmentColumns[] = {
|
|
{"id", "TEXT PRIMARY KEY"},
|
|
{"entity_id", "TEXT NOT NULL"},
|
|
{"user_id", "TEXT NOT NULL"},
|
|
{"template_id", "TEXT NOT NULL"},
|
|
{"template_valid_until", "TEXT NOT NULL DEFAULT '9999-12-31T23:59:59Z'"},
|
|
{"property_id", "TEXT"},
|
|
{"valid_from", "TEXT NOT NULL DEFAULT (datetime('now'))"},
|
|
{"valid_until", "TEXT NOT NULL DEFAULT '9999-12-31T23:59:59Z'"},
|
|
{"_fk_to_role_templates",
|
|
"FOREIGN KEY (template_id, template_valid_until) REFERENCES "
|
|
"role_templates(entity_id, valid_until) ON UPDATE CASCADE"},
|
|
};
|
|
|
|
inline static constexpr repo::SidecarTableSpec kSidecars[] = {
|
|
{"role_template_fields",
|
|
kFieldColumns,
|
|
sizeof(kFieldColumns) / sizeof(kFieldColumns[0])},
|
|
{"user_role_assignments",
|
|
kAssignmentColumns,
|
|
sizeof(kAssignmentColumns) / sizeof(kAssignmentColumns[0])},
|
|
};
|
|
|
|
inline static constexpr repo::DecoratorSchema kSchema = {
|
|
"RoleTemplateSchema",
|
|
kRoleTemplateColumns,
|
|
sizeof(kRoleTemplateColumns) / sizeof(kRoleTemplateColumns[0]),
|
|
kRoleTemplateIndexes,
|
|
sizeof(kRoleTemplateIndexes) / sizeof(kRoleTemplateIndexes[0]),
|
|
kSidecars,
|
|
sizeof(kSidecars) / sizeof(kSidecars[0]),
|
|
};
|
|
};
|
|
|
|
} // namespace oatpp_authkit::db
|
|
|
|
#include OATPP_CODEGEN_END(DbClient)
|
|
|
|
#endif
|