ores.cpp.history-provider-registrar.history_provider_registrar_implementation

Table of Contents

Per-entity history-provider registrar implementation. Defines register_<entity>_history_provider() which registers the entity's history provider under the product.component.entity_singular key. The provider body constructs the entity's service from the scoped context, reads the entity's version history through the service, and renders each version through the presentation mapper, via ores::history::service::build_entity_history_versions(). An entity whose rows the system tenant holds (system_tenant_validation), such as the pricing engine vocabulary, is read in the system tenant: the caller's tenant holds none of those rows, so a read there answers an empty history for a row the caller may read.

The provider is registered with the permission a caller needs to read the history: the entity's own read code, <component>::<entity_plural>:read. A history holds every version of the rows the entity's reads serve, so it needs the same code, and the dispatcher refuses a caller who does not hold it. The history subject is never on the allow-list of Authorised reads, so a history is guarded even when some of the entity's reads are open.

Each entity's registrar includes only that entity's service and mapper headers, so a compile failure in one entity's provider cannot block the rest of the service.

A compound-key entity emits a registrar with an empty body. Its rows have no single-string id on the history_provider interface, so nothing can be registered until a composite-key representation exists. The direct service call carries the full key.

An entity whose declared key is shared by the rows of other scopes, such as a contact type held by every party, is read by the row's own id (history_by_row_id). The entity id is then the row's id, not the declared key, so the history of one party's contact is never another party's.

Model inputs consumed by this template:

The product, component, and entity_singular composition mirrors the SQL create trigger verbatim, so the server-side provider key and the row-change side of the history pipeline can never drift out of sync.

See the Template variable reference for the complete list of available variables and their semantics.

1. Template

The full template source. Edit here and re-tangle with compass build --direct tangle_codegen_templates to regenerate library/templates/cpp_history_provider_registrar.cpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.history-provider-registrar.history_provider_registrar_implementation.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#include "ores.{{component_core}}/messaging/{{entity_singular}}_history_provider_registrar.hpp"
#include "ores.history.core/service/dispatch_registry.hpp"
{{^primary_key.is_compound}}
#include "ores.database/domain/context.hpp"
#include "ores.{{component_core}}/presentation/{{entity_singular}}_history_field_mapper.hpp"
#include "ores.{{component_core}}/service/{{entity_singular}}_service.hpp"
#include "ores.history.api/service/version_builder.hpp"
{{#system_tenant_validation}}
#include "ores.utility/uuid/tenant_id.hpp"
{{/system_tenant_validation}}
{{#history_by_row_id}}
#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid_io.hpp>
{{/history_by_row_id}}
{{/primary_key.is_compound}}

namespace ores::{{component}}::messaging {

void register_{{entity_singular}}_history_provider(
    ores::history::service::dispatch_registry& registry) {
{{#primary_key.is_compound}}
    // A compound-key row has no single-string id on the history_provider
    // interface, so nothing is registered for it yet. The direct service
    // call carries the full key; a provider shape is a composite-key
    // representation decision that stays open until a consumer exists.
    (void)registry;
{{/primary_key.is_compound}}
{{^primary_key.is_compound}}
    registry.register_history_provider(
        "{{product}}.{{component}}.{{entity_singular}}",
        "{{component}}::{{entity_plural}}:read",
        [](const ores::database::context& scoped_ctx, const std::string& entity_id) {
{{#system_tenant_validation}}
            // The rows are the registry's, held by the system tenant, so the
            // read is scoped there. A read in the caller's tenant answers an
            // empty history for a row the caller may read.
            const auto sys_ctx = scoped_ctx.with_tenant(
                ores::utility::uuid::tenant_id::system(), scoped_ctx.actor());
            service::{{entity_singular}}_service svc(sys_ctx);
{{/system_tenant_validation}}
{{^system_tenant_validation}}
            service::{{entity_singular}}_service svc(scoped_ctx);
{{/system_tenant_validation}}
{{#history_by_row_id}}
            // The declared key is shared by the rows of other scopes, so the
            // history is addressed by the row's own id.
            auto versions = svc.get_{{entity_singular_short}}_history(
                boost::lexical_cast<boost::uuids::uuid>(entity_id));
{{/history_by_row_id}}
{{^history_by_row_id}}
            auto versions = svc.get_{{entity_singular_short}}_history(entity_id);
{{/history_by_row_id}}
            return ores::history::service::build_entity_history_versions(
                versions, presentation::render_{{entity_singular}}_fields);
        });
{{/primary_key.is_compound}}
}

}
{{/domain_entity}}
{{#junction}}
{{! The provider reads through the repository: a junction's service carries the
    protocol's operations and nothing a history needs, and one that an operation
    model owns has no service at all. A junction's history is read by the parent side: the id is the parent's,
    and the answer is every version of every link of that parent, closed links
    included. A link has two keys, so no single link id would do. The rows
    arrive grouped by link and newest first, and each group is numbered and
    diffed on its own, because a version number is a link's, not the parent's. }}
#include "ores.{{component_core}}/messaging/{{name_singular}}_history_provider_registrar.hpp"
#include "ores.database/domain/context.hpp"
#include "ores.history.api/messaging/history_protocol.hpp"
#include "ores.history.api/service/version_builder.hpp"
#include "ores.history.core/service/dispatch_registry.hpp"
#include "ores.{{component_core}}/presentation/{{name_singular}}_history_field_mapper.hpp"
#include "ores.{{component_core}}/repository/{{name_singular}}_repository.hpp"
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
{{#history_parent.is_uuid}}
#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>
{{/history_parent.is_uuid}}

namespace ores::{{component}}::messaging {

void register_{{name_singular}}_history_provider(
    ores::history::service::dispatch_registry& registry) {
    registry.register_history_provider(
        "{{history_entity_type}}",
        "{{history_read_permission}}",
        [](const ores::database::context& scoped_ctx, const std::string& entity_id) {
            repository::{{name_singular}}_repository repo(scoped_ctx);
{{#history_parent.is_uuid}}
            const auto rows = repo.read_all_by_{{history_parent.column_short}}(
                boost::lexical_cast<boost::uuids::uuid>(entity_id));
{{/history_parent.is_uuid}}
{{^history_parent.is_uuid}}
            const auto rows = repo.read_all_by_{{history_parent.column_short}}(entity_id);
{{/history_parent.is_uuid}}

            std::vector<ores::history::messaging::entity_history_version> versions;
            for (auto first = rows.begin(); first != rows.end();) {
                const auto last = std::find_if(first, rows.end(), [&](const auto& row) {
                    return row.link.{{history_child.column}} != first->link.{{history_child.column}};
                });
                const std::vector<repository::{{name_singular}}_history_row> link(first, last);
                auto own = ores::history::service::build_entity_history_versions(
                    link, presentation::render_{{name_singular}}_fields);
                versions.insert(versions.end(), std::make_move_iterator(own.begin()),
                                std::make_move_iterator(own.end()));
                first = last;
            }
            return versions;
        });
}

}
{{/junction}}

2. See also

Emacs 29.3 (Org mode 9.6.15)