ores.cpp.service.service_header

Table of Contents

Entity service declarations. service profile (per entity). Application-layer service wrapping repository calls with business-rule validation, NATS event emission, and structured error handling.

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_service.hpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.service.service_header.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#ifndef ORES_{{component_core_upper}}_SERVICE_{{entity_singular_upper}}_SERVICE_HPP
#define ORES_{{component_core_upper}}_SERVICE_{{entity_singular_upper}}_SERVICE_HPP

#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "ores.logging/make_logger.hpp"
#include "ores.database/domain/context.hpp"
#include "ores.{{component_include}}/domain/{{entity_singular}}.hpp"
{{#protocol_derived}}
#include "ores.{{component_include}}/messaging/{{entity_singular}}_protocol.hpp"
{{/protocol_derived}}
#include "ores.{{component_core}}/repository/{{entity_singular}}_repository.hpp"
#include "ores.{{component_core}}/export.hpp"
{{#has_parent_id}}
#include "ores.utility/domain/hierarchy.hpp"
{{/has_parent_id}}
{{#has_uuid_include}}
#include <boost/uuid/uuid.hpp>
{{/has_uuid_include}}
<<paste:C5F35C7E-6EED-48D8-99F8-793145586882>>

namespace ores::{{component}}::service {

/**
 * @brief Service for managing {{entity_plural_words}}.
 *
 * Provides a higher-level interface for {{entity_singular_words}} operations,
 * wrapping the underlying repository.
 */
class ORES_{{component_core_upper}}_EXPORT {{entity_singular}}_service {
private:
    inline static std::string_view logger_name =
        "ores.{{component}}.service.{{entity_singular}}_service";

    [[nodiscard]] static auto& lg() {
        using namespace ores::logging;
        static auto instance = make_logger(logger_name);
        return instance;
    }

public:
    using context = ores::database::context;

    /**
     * @brief Constructs a {{entity_singular}}_service with a database context.
     *
     * @param ctx The database context for operations.
     */
    explicit {{entity_singular}}_service(context ctx);

{{#protocol_derived}}
    /**
     * @brief The protocol operations, one method per subject.
     *
     * A method takes the canonical request and answers its response, so the
     * handler that serves the subject decodes, calls and replies without
     * deciding anything. The result a caller reads -- missing, conflicting,
     * denied -- is filled here, where the storage call that decided it is
     * made, rather than being inferred from an exception.
     */
    /**@{*/
{{#operations}}
    messaging::{{response}} {{method}}(const messaging::{{request}}& request);
{{/operations}}
    /**@}*/
{{/protocol_derived}}

    /**
     * @brief Lists {{entity_plural_words}} with pagination support.
     *
     * @param offset Number of records to skip.
     * @param limit Maximum number of records to return.
     * @return Vector of {{entity_plural_words}} for the requested page.
     */
    std::vector<domain::{{entity_singular}}>
    list_{{entity_plural_short}}(std::uint32_t offset, std::uint32_t limit);

    /**
     * @brief Gets the total count of active {{entity_plural_words}}.
     *
     * @return Total number of active {{entity_plural_words}}.
     */
    std::uint32_t count_{{entity_plural_short}}();

{{#list_filter_column}}
    /**
     * @brief Lists a page of {{entity_plural_words}} under one {{list_filter_column}}.
     *
     * An empty {{list_filter_column}} covers every {{entity_singular_words}} the
     * tenant can see, so the caller need not special-case the unfiltered
     * listing.
     */
    std::vector<domain::{{entity_singular}}>
    list_{{entity_plural_short}}(std::uint32_t offset, std::uint32_t limit,
        const std::string& {{list_filter_column}});

    /**
     * @brief Counts the {{entity_plural_words}} under one {{list_filter_column}},
     * for the pager the page itself cannot supply.
     */
    std::uint32_t count_{{entity_plural_short}}(const std::string& {{list_filter_column}});

{{/list_filter_column}}


{{#foreign_keys}}
{{#list_by}}
    /**
     * @brief Lists {{entity_plural_words}} filtered by {{column}}, with pagination.
     *
     * @param {{column}} The {{column}} to filter by.
     * @param offset Number of records to skip.
     * @param limit Maximum number of records to return.
     * @return Vector of matching {{entity_plural_words}} for the requested page.
     */
    std::vector<domain::{{entity_singular}}>
    list_{{entity_plural_short}}_by_{{column}}(const std::string& {{column}},
                                              std::uint32_t offset, std::uint32_t limit);

    /**
     * @brief Gets the total count of active {{entity_plural_words}} filtered by {{column}}.
     *
     * @param {{column}} The {{column}} to filter by.
     * @return Total number of matching {{entity_plural_words}}.
     */
    std::uint32_t count_{{entity_plural_short}}_by_{{column}}(const std::string& {{column}});
{{#list_by_uuid}}

    /**
     * @brief Lists {{entity_plural_words}} filtered by {{column}}, with pagination.
     *
     * @param {{column}} The {{column}} to filter by.
     * @param offset Number of records to skip.
     * @param limit Maximum number of records to return.
     * @return Vector of matching {{entity_plural_words}} for the requested page.
     */
    std::vector<domain::{{entity_singular}}>
    list_{{entity_plural_short}}_by_{{column}}(const boost::uuids::uuid& {{column}},
                                              std::uint32_t offset, std::uint32_t limit);

    /**
     * @brief Gets the total count of active {{entity_plural_words}} filtered by {{column}}.
     *
     * @param {{column}} The {{column}} to filter by.
     * @return Total number of matching {{entity_plural_words}}.
     */
    std::uint32_t count_{{entity_plural_short}}_by_{{column}}(const boost::uuids::uuid& {{column}});
{{/list_by_uuid}}
{{/list_by}}


{{#list_by_as_of}}
    /**
     * @brief Lists {{entity_plural_words}} filtered by {{column}} that were live at
     * any point during a parent version's own [valid_from, valid_to) window.
     * See the "Temporal composite entity versioning" architecture doc.
     *
     * @param {{column}} The {{column}} to filter by.
     * @param valid_from_bound The parent version's own valid_from.
     * @param valid_to_bound The parent version's own valid_to.
     * @return Vector of matching {{entity_plural_words}}.
     */
    std::vector<domain::{{entity_singular}}>
    list_{{entity_plural_short}}_by_{{column}}_as_of(const std::string& {{column}},
                                                     std::chrono::system_clock::time_point valid_from_bound,
                                                     std::chrono::system_clock::time_point valid_to_bound);
{{/list_by_as_of}}

{{/foreign_keys}}
{{#has_audit_columns}}
    /**
     * @brief Retrieves a single {{entity_singular_words}} as it stood at a specific
     * version. See the "Temporal composite entity versioning" architecture doc.
     *
     * @param version The version to fetch.
     * @return The {{entity_singular_words}} at that version if found, std::nullopt otherwise.
     */
    std::optional<domain::{{entity_singular}}>
    get_{{entity_singular_short}}_at_version({{{primary_key.typed_params}}}, std::uint32_t version);
{{/has_audit_columns}}

    /**
     * @brief Retrieves a single {{entity_singular_words}} by its primary key.
{{#primary_key.is_single_uuid}}
     *
     * The storage key is a uuid, so the signature says which key is meant and
     * the human-readable key cannot be passed here by mistake.
{{/primary_key.is_single_uuid}}
     *
     * @return The {{entity_singular_words}} if found, std::nullopt otherwise.
     */
    std::optional<domain::{{entity_singular}}>
    {{#service_find_prefix}}find_{{/service_find_prefix}}{{^service_find_prefix}}get_{{/service_find_prefix}}{{entity_singular_short}}({{{primary_key.typed_params}}});
{{^key_is_primary}}

    /**
     * @brief Retrieves a single {{entity_singular_words}} by the key the model
     * declares -- the human-readable key a caller holds.
{{#primary_key.is_single_uuid}}
     *
     * This is the counterpart of the uuid overload above: the two keys an
     * entity holds are different keys, and a call site has to say which one it
     * means.
{{/primary_key.is_single_uuid}}
     *
     * @return The {{entity_singular_words}} if found, std::nullopt otherwise.
     */
    std::optional<domain::{{entity_singular}}>
    get_{{entity_singular_short}}_by_{{declared_key}}(const std::string& {{declared_key}});
{{/key_is_primary}}
{{#service_find_by_uuid}}

    /**
     * @brief Retrieves a single {{entity_singular_words}} by its uuid primary key.
     *
     * @return The {{entity_singular_words}} if found, std::nullopt otherwise.
     */
    std::optional<domain::{{entity_singular}}>
    find_{{entity_singular_short}}(const boost::uuids::uuid& {{primary_key.column}});
{{/service_find_by_uuid}}
{{#service_find_by_code}}
{{#parent_column}}

    /**
     * @brief Retrieves a single {{entity_singular_words}} by its
     * {{parent_column}} and {{column}} (this entity's natural key is the
     * pair, not {{column}} alone).
     *
     * @return The {{entity_singular_words}} if found, std::nullopt otherwise.
     */
    std::optional<domain::{{entity_singular}}>
    find_{{entity_singular_short}}_by_code(const boost::uuids::uuid& {{parent_column}},
                                             const std::string& {{column}});
{{/parent_column}}
{{^parent_column}}

    /**
     * @brief Retrieves a single {{entity_singular_words}} by its {{column}}.
     *
     * @return The {{entity_singular_words}} if found, std::nullopt otherwise.
     */
    std::optional<domain::{{entity_singular}}>
    find_{{entity_singular_short}}_by_code(const std::string& {{column}});
{{/parent_column}}
{{/service_find_by_code}}

    /**
     * @brief Retrieves a batch of {{entity_plural_words}} by primary key.
     */
    std::vector<domain::{{entity_singular}}>
    get_{{entity_plural_short}}({{{primary_key.batch_params}}});

    /**
     * @brief Saves a {{entity_singular_words}} (creates or updates).
     *
     * @param {{entity_singular_short}} The {{entity_singular_words}} to save.
     * @throws std::exception on failure.
     */
    void save_{{entity_singular_short}}(const domain::{{entity_singular}}& {{entity_singular_short}});

    /**
     * @brief Saves a batch of {{entity_plural_words}}.
     *
     * @param {{entity_plural_short}} The {{entity_plural_words}} to save.
     * @throws std::exception on failure.
     */
    void save_{{entity_plural_short}}(
        const std::vector<domain::{{entity_singular}}>& {{entity_plural_short}});

    /**
     * @brief Deletes a {{entity_singular_words}} by its primary key.
     *
     * @throws std::exception on failure.
     */
    void delete_{{entity_singular_short}}({{{primary_key.typed_params}}});
{{#service_find_by_uuid}}

    /**
     * @brief Removes a {{entity_singular_words}} by its uuid primary key.
     *
     * @throws std::exception on failure.
     */
    void remove_{{entity_singular_short}}(const boost::uuids::uuid& {{primary_key.column}});
{{/service_find_by_uuid}}

    /**
     * @brief Deletes {{entity_plural_words}} by their primary keys.
     */
    void delete_{{entity_plural_short}}({{{primary_key.batch_params}}});

{{^current_state}}
    /**
     * @brief Retrieves all historical versions of a {{entity_singular_words}}.
{{#key_is_primary}}
     *
     * Addressed by the entity's key, which is its storage key.
{{/key_is_primary}}
{{^key_is_primary}}
     *
     * Addressed by the key the model declares, which is the one a caller
     * holds; the storage key is resolved from it here, the same step every
     * other read makes.
{{/key_is_primary}}
     */
    std::vector<domain::{{entity_singular}}>
{{#key_is_primary}}
    get_{{entity_singular_short}}_history({{{primary_key.params}}});
{{/key_is_primary}}
{{^key_is_primary}}
    get_{{entity_singular_short}}_history(const std::string& key);
{{/key_is_primary}}
{{#service_find_by_uuid}}

    /**
     * @brief Retrieves all historical versions of a {{entity_singular_words}}
     * by its uuid primary key.
     */
    std::vector<domain::{{entity_singular}}>
    get_{{entity_singular_short}}_history(const boost::uuids::uuid& {{primary_key.column}});
{{/service_find_by_uuid}}
{{/current_state}}
{{#has_parent_id}}

    /**
     * @brief Gets the {{entity_singular}} hierarchy (as a forest of trees) rooted
     * at, or containing, the given {{entity_singular}}.
     *
     * @param root_id The {{entity_singular}} to start from.
     * @param from_root If true, returns the whole tree the given node
     * belongs to instead of just its subtree.
     * @return A forest of hierarchy_node trees (normally a single root).
     */
    std::vector<ores::utility::domain::hierarchy_node>
    get_hierarchy(const boost::uuids::uuid& root_id, bool from_root);
{{/has_parent_id}}

<<paste:2D4EA0F9-6AF6-45A5-A959-F672BF866C6A>>
private:
    context ctx_;
    repository::{{entity_singular}}_repository repo_;
{{#protocol_derived}}
{{#put_operations}}

    /**
     * @brief Checks one change against the row it names, and stamps it.
     *
     * A single write and a batch state the same claim, so the check, the
     * server-derived provenance and the version the store must match are one
     * decision made in one place. A batch that made the decision per element
     * would eventually make it differently from the single write.
     *
     * @param change The change as the caller stated it.
     * @param intent The reason and commentary the caller gave.
     * @param out The stamped domain object, written only when the result is ok.
     * @return ok, or why the change was refused.
     */
    ores::utility::domain::result prepare_change(
        const messaging::{{entity_singular}}_change& change,
        const ores::utility::domain::change_intent& intent,
        domain::{{entity_singular}}& out);
{{/put_operations}}
{{/protocol_derived}}
<<paste:047A96EF-1CC4-4661-B80B-C6C4532076AB>>
};

}

#endif
{{/domain_entity}}
{{#junction}}
#ifndef ORES_{{component_upper}}_SERVICE_{{name_singular_upper}}_SERVICE_HPP
#define ORES_{{component_upper}}_SERVICE_{{name_singular_upper}}_SERVICE_HPP

#include <cstdint>
#include <optional>
#include <string>
#include <vector>
{{#has_uuid_left_or_right}}
#include <boost/uuid/uuid.hpp>
{{/has_uuid_left_or_right}}
#include "ores.logging/make_logger.hpp"
#include "ores.database/domain/context.hpp"
#include "ores.{{component_include}}/domain/{{name_singular}}.hpp"
{{#protocol_derived}}
#include "ores.{{component_include}}/messaging/{{name_singular}}_protocol.hpp"
{{/protocol_derived}}
#include "ores.{{component_core}}/repository/{{name_singular}}_repository.hpp"

namespace ores::{{component}}::service {

/**
 * @brief Service for managing {{name_words}}.
 *
 * Provides a higher-level interface for {{name_singular_words}} operations,
 * wrapping the underlying repository.
 */
class {{name_singular}}_service {
private:
    inline static std::string_view logger_name =
        "ores.{{component}}.service.{{name_singular}}_service";

    [[nodiscard]] static auto& lg() {
        using namespace ores::logging;
        static auto instance = make_logger(logger_name);
        return instance;
    }

public:
    using context = ores::database::context;

    /**
     * @brief Constructs a {{name_singular}}_service with a database context.
     *
     * @param ctx The database context for operations.
     */
    explicit {{name_singular}}_service(context ctx);

{{#protocol_derived}}
    /**
     * @brief The protocol operations, one method per subject.
     *
     * A method takes the canonical request and answers its response, so the
     * handler that serves the subject decodes, calls and replies without
     * deciding anything. The result a caller reads -- missing, conflicting,
     * denied -- is filled here, where the storage call that decided it is
     * made, rather than being inferred from an exception.
     */
    /**@{*/
{{#operations}}
    messaging::{{response}} {{method}}(const messaging::{{request}}& request);
{{/operations}}
    /**@}*/
{{/protocol_derived}}

private:
    context ctx_;
    repository::{{name_singular}}_repository repo_;
{{#protocol_derived}}
{{#put_operations}}

    /**
     * @brief Checks one change against the row it names, and stamps it.
     *
     * A single write and a batch state the same claim, so the check, the
     * server-derived provenance and the version the store must match are one
     * decision made in one place. A batch that made the decision per element
     * would eventually make it differently from the single write.
     *
     * @param change The change as the caller stated it.
     * @param intent The reason and commentary the caller gave.
     * @param out The stamped domain object, written only when the result is ok.
     * @return ok, or why the change was refused.
     */
    ores::utility::domain::result prepare_change(
        const messaging::{{name_singular}}_change& change,
        const ores::utility::domain::change_intent& intent,
        domain::{{name_singular}}& out);
{{/put_operations}}
{{/protocol_derived}}
};

}

#endif
{{/junction}}

2. See also

Emacs 29.3 (Org mode 9.6.15)