Rakesh Vidyadharan Help

REST API Framework

A framework I have used to build a few REST APIs. API is implemented using a few template functions which handle common CRUD operations I typically use when creating REST APIs. Persistence likewise is handled through a few template functions.

Server

The server is implemented using nghttp2-asio with the nghttp2-framework providing more advanced routing and asynchronous processing. The template functions uses the Serialisation framework for JSON serialisation.

Template functions for handling common CRUD requests for a REST API service.

// // Created by Rakesh on 15/02/2021. // #pragma once #include "common.hpp" #include "db/repository.hpp" #include "db/filter/between.hpp" #include "model/entities.hpp" #include "model/entitiesquery.hpp" #include "util/cache.hpp" #include "util/config.hpp" #include "validate/validate.hpp" #include <sstream> #include <bsoncxx/exception/exception.hpp> #include <log/NanoLog.hpp> #include <mongo-service/common/util/bson.hpp> #include <mongo-service/common/util/date.hpp> namespace spt::http2::rest::handlers { using std::operator""s; using std::operator""sv; using model::Model; namespace ptemplate { template <Model M> bool canRead( const M& m, const model::JwtToken& token ) { if ( !hasEntityAccess( m.EntityType(), token ) ) return false; if ( token.user.role == model::Role::superuser ) return true; // implement other rules as appropriate return true; // return result of applying rules } } template <Model M, typename AuthFunction> auto create( const Request& req, std::string_view payload, const std::vector<std::string>& methods, AuthFunction&& authfn, bool skipVersion = false ) -> Response { LOG_INFO << "Handling POST request for " << req.path; if ( payload.empty() ) { LOG_WARN << "Request to " << req.path << " did not include payload."; return error( 400, "No payload"sv, methods, req.header ); } try { auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto m = M{ payload }; if ( m.id != model::DEFAULT_OID ) { LOG_WARN << "Create entity payload included id. " << payload; return error( 400, "Cannot specify id"sv, methods, req.header ); } auto [vstatus, vmesg, vtime] = validate::validate( m, *jwt ); if ( vstatus != 200 ) { return error( vstatus, vmesg, methods, req.header ); } m.id = bsoncxx::oid{}; auto [status, time] = db::create( m, skipVersion ); if ( status != 200 ) { return error( status, "Error creating entity"sv, methods, req.header ); } std::stringstream ss; ss << m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.compressed = compressed; resp.correlationId = correlationId( req ); resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const simdjson::simdjson_error& e ) { LOG_WARN << "JSON parse error processing " << req.path << ". " << e.what(); return error( 400, "Error parsing payload"sv, methods, req.header ); } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error creating entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto at( const Request& req, std::string_view timestamp, std::string_view payload, const std::vector<std::string>& methods, AuthFunction&& authfn, bool skipVersion = false ) -> Response { LOG_INFO << "Handling POST request for " << req.path; if ( payload.empty() ) { LOG_WARN << "Request to " << req.path << " did not include payload."; return error( 400, "No payload"sv, methods, req.header ); } if ( timestamp.empty() ) { LOG_WARN << "Request to " << req.path << " did not include timestamp."; return error( 400, "No timestamp"sv, methods, req.header ); } try { auto ts = spt::util::parseISO8601( timestamp ); if ( !ts.has_value() ) { LOG_WARN << "Invalid timestamp: " << timestamp; return error( 400, "Invalid timestamp"sv, methods, req.header ); } auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto m = M{ payload }; if ( m.id != model::DEFAULT_OID ) { LOG_WARN << "Create entity payload included id. " << payload; return error( 400, "Cannot specify id"sv, methods, req.header ); } auto [vstatus, vmesg, vtime] = validate::validate( m, *jwt ); if ( vstatus != 200 ) { return error( vstatus, vmesg, methods, req.header ); } m.id = spt::util::generateId( *ts, bsoncxx::oid{} ); auto count = 0; while ( count < 10 ) // since we are generating id at specified time, ensure there is no clash { auto [status, time, opt] = db::retrieve<M>( m.id ); if ( opt ) { m.id = spt::util::generateId( *ts, bsoncxx::oid{} ); ++count; } else break; } m.metadata.created = *ts; auto [status, time] = db::create( m, skipVersion ); if ( status != 200 ) { return error( status, "Error creating entity"sv, methods, req.header ); } std::stringstream ss; ss << m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.compressed = compressed; resp.correlationId = correlationId( req ); resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const simdjson::simdjson_error& e ) { LOG_WARN << "JSON parse error processing " << req.path << ". " << e.what(); return error( 400, "Error parsing payload"sv, methods, req.header ); } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error creating entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto update( const Request& req, std::string_view payload, std::string_view entityId, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling PUT request for " << req.path; if ( payload.empty() ) { LOG_WARN << "Request to " << req.path << " did not include payload."; return error( 400, "No payload"sv, methods, req.header ); } try { auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) { return error( astatus, methods, req.header ); } if ( !authfn( M::EntityType(), *jwt ) ) { return error( 401, methods, req.header ); } auto m = M{ payload }; if ( m.id.to_string() != entityId ) { LOG_WARN << "Update entity id " << m.id << " not same as path " << entityId << ". " << spt::util::json::str( m ); return error( 400, "Incorrect id"sv, methods, req.header ); } auto [vstatus, vmesg, vtime] = validate::validate( m, *jwt ); if ( vstatus != 200 ) { return error( vstatus, vmesg, methods, req.header ); } std::optional<bsoncxx::oid> restoredFrom = std::nullopt; if ( auto iter = req.header.find( "x-spt-restored-from" ); iter != req.header.end() ) restoredFrom = spt::util::parseId( iter->second.value ); auto [status, time] = db::update( m, false, restoredFrom ); if ( status != 200 ) { return error( status, "Error updating entity"sv, methods, req.header ); } std::stringstream ss; if ( auto p = model::pipeline<M>(); !p.empty() ) { const auto [mstatus, mtime, mopt] = db::retrieve<M>( m.id, m.customer.code ); if ( mopt ) ss << *mopt; else ss << m; } else ss << m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.compressed = compressed; resp.correlationId = correlationId( req ); resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const simdjson::simdjson_error& e ) { LOG_WARN << "JSON parse error processing " << req.path << ". " << e.what(); return error( 400, "Error parsing payload"sv, methods, req.header ); } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error updating entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto get( const Request& req, std::string_view entityId, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { try { LOG_INFO << "Handling GET request for " << req.path; auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) { return error( astatus, methods, req.header ); } if ( !authfn( M::EntityType(), *jwt ) ) { return error( 401, methods, req.header ); } auto id = spt::util::parseId( entityId ); if ( !id ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid id " << entityId; return error( 400, methods, req.header ); } auto [mstatus, mtime, m] = db::retrieve<M>( *id ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entity"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); if ( !ptemplate::canRead( *m, *jwt ) ) { return error( 403, "User not allowed to view entity"sv, methods, req.header ); } std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.compressed = compressed; resp.correlationId = correlationId( req ); resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename ValueType, typename AuthFunction> auto get( const Request& req, std::string_view property, ValueType value, const std::vector<std::string>& methods, AuthFunction&& authfn, bool caseInsensitive = false ) -> Response { try { LOG_INFO << "Handling GET request for " << req.path; auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) { return error( astatus, methods, req.header ); } if ( !authfn( M::EntityType(), *jwt ) ) { return error( 401, methods, req.header ); } auto [mstatus, mtime, m] = db::retrieve<M,ValueType>( property, value, caseInsensitive ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entity"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); if ( !ptemplate::canRead( *m, *jwt ) ) { return error( 403, "User not allowed to view entity"sv, methods, req.header ); } std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.compressed = compressed; resp.correlationId = correlationId( req ); resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto retrieveAll( const Request& req, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling GET request for " << req.path; try { auto [qstatus, eq] = parseQuery( req ); if ( qstatus != 200 || !eq ) return error( 400, "Invalid query"sv, methods, req.header ); auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto query = bsoncxx::builder::stream::document{}; if ( eq->after ) { auto oid = spt::util::parseId( *eq->after ); if ( !oid ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid after " << *eq->after; return error( 400, methods, req.header ); } query << "_id" << bsoncxx::builder::stream::open_document << ( eq->descending ? "$lt" : "$gt" ) << *oid << bsoncxx::builder::stream::close_document; } auto [mstatus, mtime, m] = db::query<M>( query << bsoncxx::builder::stream::finalize, *eq ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entity"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.correlationId = correlationId( req ); resp.compressed = compressed; resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename ValueType, typename AuthFunction> auto retrieveAll( const Request& req, std::string_view property, ValueType value, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling GET request for " << req.path; try { auto [qstatus, eq] = parseQuery( req ); if ( qstatus != 200 || !eq ) return error( 400, "Invalid query"sv, methods, req.header ); auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto query = bsoncxx::builder::stream::document{}; query << property << value; if ( eq->after ) { auto oid = spt::util::parseId( *eq->after ); if ( !oid ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid after " << *eq->after; return error( 400, methods, req.header ); } query << "_id" << bsoncxx::builder::stream::open_document << ( eq->descending ? "$lt" : "$gt" ) << *oid << bsoncxx::builder::stream::close_document; } auto [mstatus, mtime, m] = db::query<M>( query << bsoncxx::builder::stream::finalize, *eq ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entity"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.correlationId = correlationId( req ); resp.compressed = compressed; resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto between( const Request& req, std::string_view property, std::chrono::time_point<std::chrono::system_clock> start, std::chrono::time_point<std::chrono::system_clock> end, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling GET request for " << req.path; try { auto [qstatus, eq] = parseQuery( req ); if ( qstatus != 200 || !eq ) { return error( 400, "Invalid query"sv, methods, req.header ); } auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto filter = db::filter::Between{}; filter.field = property; filter.from = start; filter.to = end; filter.descending = eq->descending; if ( eq->after ) { const auto a = spt::util::parseISO8601( *eq->after ); if ( !a.has_value() ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid after " << *eq->after; return error( 400, a.error(), methods, req.header ); } filter.after = a.value(); } auto [mstatus, mtime, m] = db::between<M>( std::move( filter ), *eq ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entities"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.correlationId = correlationId( req ); resp.compressed = compressed; resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto refcounts( const Request& req, std::string_view entityId, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling GET request for " << req.path; try { auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto id = spt::util::parseId( entityId ); if ( !id ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid id " << entityId; return error( 400, methods, req.header ); } auto [mstatus, mtime, m] = db::refcounts<M>( *id ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error counting references to entity"sv, methods, req.header ); } if ( mstatus == 404 || !m ) return error( 404, methods, req.header ); std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.correlationId = correlationId( req ); resp.compressed = compressed; resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error deleting entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto remove( const Request& req, std::string_view entityId, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling DELETE request for " << req.path; try { auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto id = spt::util::parseId( entityId ); if ( !id ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid id " << entityId; return error( 400, methods, req.header ); } auto [mstatus, mtime] = db::remove<M>( *id ); if ( mstatus != 200 && mstatus != 404 ) return error( mstatus, "Error deleting entity"sv, methods, req.header ); if ( mstatus == 404 ) return error( mstatus, methods, req.header ); LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.correlationId = correlationId( req ); resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error deleting entity"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto versionHistorySummary( const Request& req, std::string_view entityId, const std::vector<std::string>& methods, AuthFunction&& authfn ) -> Response { LOG_INFO << "Handling GET request for " << req.path; try { auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto id = spt::util::parseId( entityId ); if ( !id ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid id " << entityId; return error( 400, methods, req.header ); } auto [mstatus, mtime, m] = db::versionHistorySummary<M>( *id ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entity history summaries"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.correlationId = correlationId( req ); resp.compressed = compressed; resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity history summaries"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } template <Model M, typename AuthFunction> auto versionHistoryDocument( const Request& req, std::string_view historyId, const std::vector<std::string>& methods, AuthFunction authfn ) -> Response { LOG_INFO << "Handling GET request for " << req.path; try { auto [astatus, jwt] = authorise( req ); if ( astatus != 200 || !jwt ) return error( astatus, methods, req.header ); if ( !authfn( M::EntityType(), *jwt ) ) return error( 401, methods, req.header ); auto id = spt::util::parseId( historyId ); if ( !id ) { LOG_INFO << "Rejecting request for " << req.path << " with invalid id " << historyId; return error( 400, methods, req.header ); } auto [mstatus, mtime, m] = db::versionHistoryDocument<M>( *id ); if ( mstatus != 200 && mstatus != 404 ) { return error( mstatus, "Error retrieving entity history document"sv, methods, req.header ); } if ( !m ) return error( 404, methods, req.header ); std::stringstream ss; ss << *m; auto [out, compressed] = shouldCompress( req ) ? compress( ss ) : Output{ ss.str(), false }; LOG_INFO << "Writing response for " << req.path; auto resp = Response{ req.header }; resp.jwt = jwt; resp.body = std::move( out ); resp.correlationId = correlationId( req ); resp.compressed = compressed; resp.set( methods ); resp.entity = M::EntityType(); return resp; } catch ( const bsoncxx::exception& b ) { LOG_WARN << "BSON error processing " << req.path << ". " << b.what(); LOG_WARN << util::stacktrace(); return error( 417, "Error retrieving entity history summaries"sv, methods, req.header ); } catch ( const std::exception& ex ) { LOG_WARN << "Error processing request " << req.path << ". " << ex.what(); LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } catch ( ... ) { LOG_WARN << "Unexpected error processing request " << req.path << ". "; LOG_WARN << util::stacktrace(); return error( 500, "Internal server error"sv, methods, req.header ); } } }

Utility functions used to send error responses etc.

// // Created by Rakesh on 10/10/2020. // #pragma once #include "model/entitiesquery.hpp" #include "model/jwttoken.hpp" #include <sstream> #include <tuple> #include <vector> #include <http2/framework/request.hpp> #include <http2/framework/response.hpp> namespace spt::http2::rest::handlers { framework::Response unsupported( const std::vector<std::string>& methods, const nghttp2::asio_http2::header_map& headers ); framework::Response error( uint16_t code, const std::vector<std::string>& methods, const nghttp2::asio_http2::header_map& headers ); framework::Response error( uint16_t code, std::string_view message, const std::vector<std::string>& methods, const nghttp2::asio_http2::header_map& headers ); void cors( const nghttp2::asio_http2::header_map& headers, const nghttp2::asio_http2::server::response& res, const std::string& methods = {"DELETE,GET,OPTIONS,PATCH,POST,PUT"} ); using AuthResponse = std::tuple<uint16_t, model::JwtToken::Ptr>; AuthResponse authorise( const Request& req ); std::string correlationId( const Request& req ); bool hasEntityAccess( std::string_view entity, const model::JwtToken& token ); bool superuserRole( std::string_view entity, const model::JwtToken& token ); bool adminRole( std::string_view entity, const model::JwtToken& token ); bool userRole( std::string_view entity, const model::JwtToken& token ); std::string statusMessage( int16_t status ); using EntitiesQueryResponse = std::tuple<uint16_t, std::optional<model::EntitiesQuery>>; EntitiesQueryResponse parseQuery( const Request& req, const std::vector<std::string_view>& additional = {} ); using Output = std::tuple<std::string,bool>; Output compress( std::stringstream& oss ); bool shouldCompress( const Request& req ); }

Sample showing adding API endpoint routes to the router. Delegates request processing to the appropriate template functions.

// // Created by Rakesh on 13/02/2021. // #include "http/common.hpp" #include "http/template.hpp" void spt::http2::rest::handlers::addRoutes( http2::framework::Router<Response>& router ) { static const std::array methods{ "DELETE"s, "GET"s, "OPTIONS"s, "POST"s, "PUT"s }; router.add( "GET"sv, "/entity/user/"sv, []( const http2::framework::RoutingRequest& req, auto&& ) { return retrieveAll<model::User>( req.req, methods, &userRole ); }, "./paths/user.yaml#/root"sv ); router.add( "GET"sv, "/entity/user/id/{id}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return get<model::User>( req.req, args["id"sv], methods, &userRole ); }, "./paths/user.yaml#/id"sv ); router.add( "GET"sv, "/entity/user/identifier/{identifier}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return get<model::User, std::string_view>( req.req, "identifier"sv, args["identifier"sv], methods, &userRole ); }, "./paths/user.yaml#/identifier"sv ); router.add( "GET"sv, "/entity/user/count/references/{id}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return refcounts<model::User>( req.req, args["id"sv], methods, &userRole ); }, "./paths/user.yaml#/refcount"sv ); router.add( "GET"sv, "/entity/user/history/summary/{id}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return versionHistorySummary<model::User>( req.req, args["id"sv], methods, &superuserRole ); }, "./paths/user.yaml#/historySummary"sv ); router.add( "GET"sv, "/entity/user/history/document/{id}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return versionHistoryDocument<model::User>( req.req, args["id"sv], methods, &superuserRole ); }, "./paths/user.yaml#/historyDocument"sv ); router.add( "GET"sv, "/entity/user/{type}/between/{start}/{end}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { auto type = args["type"sv]; if ( type != "created"sv && type != "modified"sv ) return error( 404, methods, req.req.header ); auto prop = std::format( "metadata.{}"sv, type ); auto svar = spt::util::parseISO8601( args["start"sv] ); if ( !svar.has_value() ) { LOG_WARN << "Invalid start date in path " << req.req.path; return error( 400, svar.error(), methods, req.req.header ); } auto evar = spt::util::parseISO8601( args["end"sv] ); if ( !evar.has_value() ) { LOG_WARN << "Invalid end date in path " << req.req.path; return error( 400, evar.error(), methods, req.req.header ); } return between<model::User>( req.req, prop, svar.value(), evar.value(), methods, &userRole ); }, "./paths/user.yaml#/between"sv ); router.add( "POST"sv, "/entity/user/"sv, []( const http2::framework::RoutingRequest& req, auto&& ) { return create<model::User>( req.req, req.body, methods, &adminRole ); }, "./paths/user.yaml#/root"sv ); router.add( "PUT"sv, "/entity/user/id/{id}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return update<model::User>( req.req, req.body, args["id"sv], methods, &adminRole ); } ); router.add( "DELETE"sv, "/entity/user/id/{id}"sv, []( const http2::framework::RoutingRequest& req, auto args ) { return remove<model::User>( req.req, args["id"sv], methods, &adminRole ); } ); }

Models

Common structures used to implement the REST API. User defined structures are developed as appropriate, and used as template types.

A template structure used to returns multiple entities as a response. The structure provides information to the caller related to the availability of further *pages* of data.

// // Created by Rakesh on 27/02/2021. // #pragma once #include <vector> #include <boost/json/object.hpp> #include <mongo-service/common/util/json.hpp> #include <mongo-service/common/visit_struct/visit_struct_intrusive.hpp> namespace spt::http2::rest::model { template <typename Model> struct Entities { explicit Entities( std::string_view json ) { spt::util::json::unmarshall( *this, json ); } Entities() = default; Entities(Entities&&) noexcept = default; Entities& operator=(Entities&&) = default; ~Entities() = default; Entities(const Entities&) = delete; Entities& operator=(const Entities&) = delete; void populate( simdjson::ondemand::object& object ) { FROM_JSON( total, object ); FROM_JSON( page, object ); } BEGIN_VISITABLES(Entities<Model>); VISITABLE(std::vector<Model>, entities); VISITABLE(std::string, next); int32_t total{ 0 }; int32_t page{ 0 }; END_VISITABLES; }; template <typename Model> void populate( Entities<Model>& model, simdjson::ondemand::object& object ) { model.populate( object ); } template <typename Model> void populate( const Entities<Model>& model, boost::json::object& object ) { object.emplace( "total", model.total ); object.emplace( "page", model.page ); if ( !object.contains( "entities" ) ) object.emplace( "entities", boost::json::array{} ); } }

A simple structure used to represent common query string parameters supported by the API.

// // Created by Rakesh on 17/02/2021. // #pragma once #include <optional> #include <string> namespace spt::http2::rest::model { struct EntitiesQuery { std::optional<std::string> after{ std::nullopt }; int16_t limit{ 25 }; bool descending{ false }; }; }

Persistence

Data persistence is based on MongoDB, and uses the mongo-service to provide access and common API for database access. The template functions use the Models provided by the API to perform CRUD operations on user defined data structures.

Template functions for performing CRUD operations against the database.

// // Created by Rakesh on 14/02/2021. // #pragma once #include "filter/between.hpp" #include "filter/id.hpp" #include "filter/property.hpp" #include "metadata.hpp" #include "model/entities.hpp" #include "model/entitiesquery.hpp" #include "model/json.hpp" #include "model/model.hpp" #include "model/refcount.hpp" #include "model/versionhistory.hpp" #include "util/config.hpp" #include "util/stacktrace.hpp" #include <bsoncxx/builder/stream/document.hpp> #include <bsoncxx/document/view.hpp> #include <bsoncxx/json.hpp> #include <chrono> #include <fmt/format.h> #include <log/NanoLog.hpp> #include <mongo-service/api/api.hpp> #include <mongo-service/api/repository/repository.hpp> #include <mongo-service/common/util/bson.hpp> #include <range/v3/range/concepts.hpp> #include <range/v3/algorithm/find_if.hpp> namespace spt::http2::rest::db { using StatusResponse = std::tuple<int16_t, int64_t>; using std::operator""sv; using std::operator""s; using model::Model; inline std::string cacheKey( std::string_view type, bsoncxx::oid id ) { return fmt::format( "/cache/entity/{}/id/{}", type, id.to_string() ); } template <Model M> std::tuple<int16_t, int32_t> count( bsoncxx::document::view query ) { auto count = spt::mongoservice::api::model::request::Count{ bsoncxx::document::value{ query } }; count.database = M::Database(); count.collection = M::Collection(); count.options.emplace(); count.options->limit = 10'000; count.options->maxTime = std::chrono::milliseconds{ 500 }; auto result = spt::mongoservice::api::repository::count( count ); if ( !result.has_value() ) { LOG_WARN << "Error counting " << M::Database() << ':' << M::Collection() << ". " << magic_enum::enum_name( result.error().cause ) << ". " << result.error().message; return { 417, 0 }; } return { 200, result.value().count }; } template <Model M> std::tuple<int16_t, std::optional<bsoncxx::oid>> lastId( bsoncxx::document::view query, bsoncxx::document::value&& projection, bsoncxx::document::value&& sort ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; auto retrieve = spt::mongoservice::api::model::request::Retrieve{ bsoncxx::document::value{ query } }; retrieve.database = M::Database(); retrieve.collection = M::Collection(); retrieve.options.emplace(); retrieve.options->projection.emplace( std::move( projection ) ); retrieve.options->limit = 1; retrieve.options->sort.emplace( std::move( sort ) ); const auto result = spt::mongoservice::api::repository::retrieve<spt::mongoservice::api::model::request::IdFilter>( retrieve ); if ( !result.has_value() ) { LOG_WARN << "Error retrieving last document id from " << M::Database() << ':' << M::Collection() << ". " << magic_enum::enum_name( result.error().cause ) << ". " << result.error().message; return { 417, std::nullopt }; } if ( result.value().results.empty() ) { LOG_INFO << "No last document id from " << M::Database() << ':' << M::Collection() << ". " << bsoncxx::to_json( query ); return { 412, std::nullopt }; } return { 200, result.value().results.front().id }; } template <Model M> StatusResponse create( const M& m, bool skipVersion = false ) { const auto st = std::chrono::steady_clock::now(); try { auto now = std::chrono::system_clock::now(); auto md = Metadata{}; md.year = std::format( "{:%Y}", now ); md.month = std::format( "{:%m}", now ); auto cr = spt::mongoservice::api::model::request::CreateWithReference<M, Metadata>{ m, md }; cr.database = M::Database(); cr.collection = M::Collection(); cr.skipVersion = skipVersion; const auto res = spt::mongoservice::api::repository::create( cr ); if ( !res.has_value() ) { LOG_WARN << "Error creating model " << spt::util::json::str( m ) << ". " << magic_enum::enum_name( res.error().cause ) << ". " << res.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count() }; } if ( !skipVersion ) { auto bson = spt::util::bson( m ); auto bv = bson.view().get_document().value; util::Configuration::instance().set( cacheKey( M::EntityType(), m.id ), std::string{ reinterpret_cast<const char*>( bv.data() ), bv.length() }, 600 ); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count() }; } catch ( const std::exception& ex ) { LOG_WARN << "Error writing model " << ex.what() << ". " << spt::util::json::str( m ); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error writing model. " << spt::util::json::str( m ); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count() }; } template <Model M> StatusResponse update( const M& m, bool skipVersion = false, std::optional<bsoncxx::oid> restoredFrom = std::nullopt ) { const auto st = std::chrono::steady_clock::now(); try { auto now = std::chrono::system_clock::now(); auto filter = filter::Id{}; filter.id = m.id; auto md = Metadata{}; md.year = std::format( "{:%Y}", now ); md.month = std::format( "{:%m}", now ); if ( restoredFrom ) md.restoredFrom = *restoredFrom; auto ur = spt::mongoservice::api::model::request::ReplaceWithReference<M, Metadata, filter::Id>{ filter, m, md }; ur.database = M::Database(); ur.collection = M::Collection(); ur.skipVersion = skipVersion; const auto res = spt::mongoservice::api::repository::update( ur ); if ( !res.has_value() ) { LOG_WARN << "Error replacing model " << spt::util::json::str( m ) << ". " << magic_enum::enum_name( res.error().cause ) << ". " << res.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count() }; } auto bson = spt::util::bson( m ); auto bv = bson.view().get_document().value; util::Configuration::instance().set( cacheKey( M::EntityType(), m.id ), std::string{ reinterpret_cast<const char*>( bv.data() ), bv.length() }, 600 ); const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count() }; } catch ( const std::exception& ex ) { LOG_WARN << "Error replacing model " << ex.what() << ". " << spt::util::json::str( m ); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error replacing model. " << spt::util::json::str( m ); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count() }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<M>> pipeline( std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage> stages, bool caseInsensitive = false ) { const auto st = std::chrono::steady_clock::now(); try { auto model = spt::mongoservice::api::model::request::Pipeline{}; model.database = M::Database(); model.collection = M::Collection(); if ( caseInsensitive ) { model.options.emplace(); model.options->collation.emplace(); model.options->collation->locale = "en"; model.options->collation->strength = 1; model.options->limit = 1; } model.document.specification.insert( model.document.specification.end(), std::make_move_iterator( stages.begin() ), std::make_move_iterator( stages.end() ) ); stages.erase( stages.begin(), stages.end() ); auto resp = spt::mongoservice::api::repository::pipeline<M>( model ); if ( !resp.has_value() ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << spt::util::json::str( model.document ) << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } if ( resp.value().results.empty() ) { LOG_WARN << "No matching documents for query against " << M::Database() << ':' << M::Collection() << ". " << spt::util::json::str( model.document ); const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 404, delta.count(), std::nullopt }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( resp.value().results.front() ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << db << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << db << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<model::Entities<M>>> pipeline( bsoncxx::document::value match, std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage> stages, model::EntitiesQuery options ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto st = std::chrono::steady_clock::now(); try { auto req = spt::mongoservice::api::model::request::Pipeline{}; req.database = M::Database(); req.collection = M::Collection(); req.document.specification = std::move( stages ); auto resp = spt::mongoservice::api::repository::pipeline<M>( req ); if ( !resp.has_value() ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << spt::util::json::str( req.document ) << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } model::Entities<M> ms; ms.entities = std::move( resp.value().results ); ms.page = std::size( ms.entities ); if ( !options.after && ms.page > 0 && ms.page < options.limit ) { ms.total = ms.page; } else { if ( const auto [cstatus, csize] = count<M>( match ); cstatus == 200 ) ms.total = csize; } if ( ms.page > 0 && ms.total != ms.page ) { auto [lstatus, lv] = lastId<M>( match, document{} << "_id" << 1 << finalize, document{} << "_id" << (options.descending ? 1 : -1) << finalize ); if ( lstatus != 200 ) { const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { lstatus, delta.count(), std::nullopt }; } auto lid = ms.entities.back().id; if ( lid != *lv ) ms.next = lid.to_string(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( ms ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << M::Database() << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<M>> retrieve( bsoncxx::oid id ) { if ( auto p = model::pipeline<M>(); !p.empty() ) { LOG_DEBUG << "Entity " << M::EntityType() << " requires pipeline"; auto filter = filter::Id{ id }; auto stages = std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage>{}; stages.reserve( p.size() + 1 ); stages.emplace_back( "$match", spt::util::bson( filter ) ); stages.insert( stages.end(), std::make_move_iterator( p.begin() ), std::make_move_iterator( p.end() ) ); p.erase( p.begin(), p.end() ); return pipeline<M>( std::move( stages ) ); } const auto st = std::chrono::steady_clock::now(); const auto ckey = cacheKey( M::EntityType(), id ); if ( auto cached = util::Configuration::instance().get( ckey ); cached ) { auto mv = bsoncxx::document::view( reinterpret_cast<const uint8_t*>( cached->data() ), cached->size() ); auto m = M{ mv }; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( m ) }; } try { auto r = spt::mongoservice::api::model::request::Retrieve<filter::Id>{}; r.database = M::Database(); r.collection = M::Collection(); r.document.emplace( id ); auto resp = spt::mongoservice::api::repository::retrieve<M>( r ); if ( !resp.has_value() ) { LOG_WARN << "Error retrieving document " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { resp.error().message == "Not found" ? 404 : 417, delta.count(), std::nullopt }; } if ( !resp.value().result ) { LOG_WARN << "No document " << M::Database() << ':' << M::Collection() << ':' << id.to_string(); const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 404, delta.count(), std::nullopt }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); auto bson = spt::util::marshall( *resp.value().result ); auto dv = bson.view(); auto str = std::string{ reinterpret_cast<const char *>( dv.data() ), dv.length() }; util::Configuration::instance().set( ckey, str, 600 ); return { 200, delta.count(), std::move( resp.value().result ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error retrieving document " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error retrieving document " << M::Database() << ':' << M::Collection() << ':' << id.to_string(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template<Model M, typename ValueType> std::tuple<int16_t, int64_t, std::optional<M>> retrieve( std::string_view property, ValueType value, bool caseInsensitive = false ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::finalize; if ( auto p = model::pipeline<M>(); !p.empty() ) { LOG_DEBUG << "Entity " << M::EntityType() << " requires pipeline"; auto stages = std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage>{}; stages.reserve( 3 + p.size() ); auto match = filter::Property<ValueType>{ property, value, customer }; stages.emplace_back( "$match", spt::util::bson( match ) ); stages.emplace_back( "$sort", document{} << "_id" << -1 << finalize ); stages.emplace_back( "$limit", bsoncxx::types::b_int32{ 1 } ); stages.insert( stages.end(), std::make_move_iterator( p.begin() ), std::make_move_iterator( p.end() ) ); p.erase( p.begin(), p.end() ); return pipeline<M>( std::move( stages ), database, caseInsensitive ); } const auto st = std::chrono::steady_clock::now(); try { auto r = spt::mongoservice::api::model::request::Retrieve<filter::Property<ValueType>>{}; r.database = M::Database(); r.collection = M::Collection(); r.document.emplace( property, value, customer ); r.options.emplace(); r.options->collation.emplace(); r.options->collation->locale = "en"; r.options->collation->strength = 1; r.options->limit = 1; r.options->sort = document{} << "_id" << -1 << finalize; auto resp = spt::mongoservice::api::repository::retrieve<M>( r ); if ( !resp.has_value() ) { LOG_WARN << "Unable to retrieve document from " << M::Database() << ':' << M::Collection() << " with property: " << property << ", value: " << value << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { resp.error().message == "Not found" ? 404 : 417, delta.count(), std::nullopt }; } if ( resp.value().results.empty() ) { LOG_WARN << "No matching document " << M::Database() << ':' << M::Collection() << " with property: " << property << ", value: " << value; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 404, delta.count(), std::nullopt }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( resp.value().results.front() ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error retrieving document " << M::Database() << ':' << M::Collection() << " with property: " << property << ", value: " << value << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error retrieving document " << M::Database() << ':' << M::Collection() << " with property: " << property << ", value: " << value; LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<model::Entities<M>>> query( bsoncxx::document::value query, model::EntitiesQuery options ) { using bsoncxx::builder::stream::array; using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; if ( auto p = model::pipeline<M>(); !p.empty() ) { LOG_DEBUG << "Entity " << M::EntityType() << " requires pipeline"; auto stages = std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage>{}; stages.reserve( p.size() + 3 ); stages.emplace_back( "$match", query.view() ); stages.emplace_back( "$sort", document{} << "_id" << (options.descending ? -1 : 1) << finalize ); stages.emplace_back( "$limit", bsoncxx::types::b_int32{ 1 } ); stages.insert( stages.end(), std::make_move_iterator( p.begin() ), std::make_move_iterator( p.end() ) ); p.erase( p.begin(), p.end() ); return pipeline<M>( std::move( query ), std::move( stages ), options, database ); } const auto st = std::chrono::steady_clock::now(); try { auto retrieve = spt::mongoservice::api::model::request::Retrieve{ bsoncxx::document::value{ query } }; retrieve.database = M::Database(); retrieve.collection = M::Collection(); retrieve.options.emplace(); retrieve.options->limit = options.limit; retrieve.options->sort = document{} << "_id" << (options.descending ? -1 : 1) << finalize; auto result = spt::mongoservice::api::repository::retrieve<M>( retrieve ); if ( !result.has_value() ) { LOG_WARN << "Unable to execute query against " << M::Database() << ':' << M::Collection() << ". " << magic_enum::enum_name( result.error().cause ) << ". " << result.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } model::Entities<M> ms; ms.entities = std::move( result.value().results ); ms.page = std::size( ms.entities ); if ( !options.after && ms.page > 0 && ms.page < options.limit ) { ms.total = ms.page; } else { if ( const auto [cstatus, csize] = count<M>( query ); cstatus == 200 ) ms.total = csize; } if ( ms.page > 0 && ms.total != ms.page ) { auto [lstatus, lv] = lastId<M>( query, document{} << "_id" << 1 << finalize, document{} << "_id" << (options.descending ? 1 : -1) << finalize ); if ( lstatus != 200 ) { const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { lstatus, delta.count(), std::nullopt }; } auto lid = ms.entities.back().id; if ( lid != *lv ) ms.next = lid.to_string(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( ms ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << M::Database() << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<std::vector<M>>> rawquery( bsoncxx::document::value query, model::EntitiesQuery options ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; const auto st = std::chrono::steady_clock::now(); const auto db = database.empty() ? M::Database() : database; try { auto retrieve = spt::mongoservice::api::model::request::Retrieve{ bsoncxx::document::value{ query } }; retrieve.database = M::Database(); retrieve.collection = M::Collection(); retrieve.options.emplace(); retrieve.options->limit = options.limit; retrieve.options->sort = document{} << "_id" << (options.descending ? -1 : 1) << finalize; auto result = spt::mongoservice::api::repository::retrieve<M>( retrieve ); if ( !result.has_value() ) { LOG_WARN << "Unable to execute query against " << M::Database() << ':' << M::Collection() << ". " << magic_enum::enum_name( result.error().cause ) << ". " << result.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( result.value().results ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << M::Database() << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t> transaction( bsoncxx::array::view items ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto st = std::chrono::steady_clock::now(); try { auto req = Request::transaction( std::string{ M::Database() }, std::string{ M::Collection() }, document{} << "items" << items << finalize ); const auto [type, opt] = execute( req ); if ( !opt ) { LOG_WARN << "Unable to execute transaction against " << db << ':' << M::Collection() << ". " << req.document; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 424, delta.count() }; } const auto view = opt->view(); if ( const auto err = spt::util::bsonValueIfExists<std::string>( "error", view ); err ) { LOG_WARN << "Error executing query against " << db << ':' << M::Collection() << ". " << req.document << ". " << view; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count() }; } LOG_INFO << "Transaction results " << view; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count() }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << M::Database() << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count() }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<std::vector<M>>> rawpipeline( std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage> stages ) { const auto st = std::chrono::steady_clock::now(); try { auto req = spt::mongoservice::api::model::request::Pipeline{}; req.database = M::Database(); req.collection = M::Collection(); req.document.specification = std::move( stages ); auto res = spt::mongoservice::api::repository::pipeline<M>( req ); if ( !res.has_value() ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << spt::util::json::str( req.document ) << ". " << magic_enum::enum_name( res.error().cause ) << ". " << res.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( res.value().results ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << M::Database() << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<model::Entities<M>>> between( const filter::Between& filter, model::EntitiesQuery options ) { using bsoncxx::builder::stream::array; using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; if ( auto p = model::pipeline<M>(); !p.empty() ) { LOG_DEBUG << "Entity " << M::EntityType() << " requires pipeline"; auto stages = std::vector<spt::mongoservice::api::model::request::Pipeline::Document::Stage>{}; stages.reserve( p.size() + 3 ); stages.emplace_back( "$match", spt::util::bson( filter ) ); stages.emplace_back( "$sort", document{} << filter.field << (options.descending ? -1 : 1) << finalize ); stages.emplace_back( "$limit", bsoncxx::types::b_int32{ options.limit } ); stages.insert( stages.end(), std::make_move_iterator( p.begin() ), std::make_move_iterator( p.end() ) ); p.erase( p.begin(), p.end() ); return pipeline<M>( spt::util::marshall( filter ), std::move( stages ), options, database ); } const auto st = std::chrono::steady_clock::now(); try { auto retrieve = spt::mongoservice::api::model::request::Retrieve{ spt::util::marshall( filter ) }; retrieve.database = M::Database(); retrieve.collection = M::Collection(); retrieve.options.emplace(); retrieve.options->limit = options.limit; retrieve.options->sort = document{} << filter.field << (options.descending ? -1 : 1) << finalize; auto res = spt::mongoservice::api::repository::retrieve<M>( retrieve ); if ( !res.has_value() ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << magic_enum::enum_name( res.error().cause ) << ". " << res.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } model::Entities<M> ms; ms.entities = std::move( res.value().results ); ms.page = std::size( ms.entities ); if ( !options.after && ms.page > 0 && ms.page < options.limit ) { ms.total = ms.page; } else { if ( const auto [cstatus, csize] = count<M>( *retrieve.document ); cstatus == 200 ) ms.total = csize; } if ( ms.page > 0 && ms.total != ms.page ) { auto [lstatus, lv] = lastId<M>( *retrieve.document, document{} << filter.field << 1 << finalize, document{} << filter.field << (options.descending ? 1 : -1) << finalize ); if ( lstatus != 200 ) { const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { lstatus, delta.count(), std::nullopt }; } if ( ms.entities.back().id != *lv ) { ms.next = spt::util::isoDateMillis( filter.field == "created"sv ? ms.entities.back().metadata.created : ms.entities.back().metadata.modified ); } } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( ms ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error executing query against " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error executing query against " << M::Database() << ':' << M::Collection(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<model::RefCounts>> refcounts( bsoncxx::oid id ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto refs = M::References(); if ( refs.empty() ) return { 200, 0, std::nullopt }; const auto st = std::chrono::steady_clock::now(); try { model::RefCounts rf; rf.references.reserve( refs.size() ); int64_t time{ 0 }; for ( const auto& ref : refs ) { auto req = spt::mongoservice::api::model::request::Count<bsoncxx::document::value>{ document{} << ref.field << id << finalize }; req.database = model::database( ref.type ); req.collection = model::collection( ref.type ); const auto resp = spt::mongoservice::api::repository::count( req ); if ( !resp.has_value() ) { LOG_WARN << "Error refcounting document " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << " in collection " << model::collection( ref.type ) << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); time += delta.count(); return { 417, time, std::nullopt }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); time += delta.count(); rf.references.emplace_back( model::RefCount{ static_cast<int32_t>( resp.value().count ), ref.type } ); } return { 200, time, std::move( rf ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error retrieving reference counts for " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error retrieving document " << M::Database() << ':' << M::Collection() << ':' << id.to_string(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t> remove( bsoncxx::oid id ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto st = std::chrono::steady_clock::now(); try { auto [rstatus, rtime, rc] = refcounts<M>( id ); if ( rstatus != 200 ) return { rstatus, rtime }; if ( rc ) { for ( const auto& r : rc->references ) { if ( r.count > 0 ) { LOG_INFO << "Rejecting delete document " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << " as it is being referenced. " << spt::util::json::str( *rc ); return { 412, rtime }; } } } auto now = std::chrono::system_clock::now(); auto req = spt::mongoservice::api::model::request::Delete<filter::Id, Metadata>( filter::Id{} ); req.database = M::Database(); req.collection = M::Collection(); req.document->id = id; if ( !customer.empty() ) req.document->customer = customer; req.metadata.emplace(); req.metadata->year = std::format( "{:%Y}", now ); req.metadata->month = std::format( "{:%m}", now ); auto resp = spt::mongoservice::api::repository::remove( req ); if ( !resp.has_value() ) { LOG_WARN << "Error deleting document " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count() }; } auto iter = ranges::find_if( resp.value().success, [&id]( const bsoncxx::oid& oid ) { return oid == id; } ); if ( iter == ranges::end( resp.value().success ) ) { LOG_WARN << "No document deleted for " << M::Database() << ':' << M::Collection() << ':' << id.to_string(); const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count() }; } util::Configuration::instance().remove( cacheKey( M::EntityType(), id ) ); const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count() }; } catch ( const std::exception& ex ) { LOG_WARN << "Error removing document " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error removing document " << M::Database() << ':' << M::Collection() << ':' << id.to_string(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count() }; } template <Model M> std::tuple<int16_t, int64_t> remove( bsoncxx::document::value&& query ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto st = std::chrono::steady_clock::now(); try { const auto now = std::chrono::system_clock::now(); auto req = spt::mongoservice::api::model::request::Delete<bsoncxx::document::value, Metadata>{ std::move( query ) }; req.database = M::Database(); req.collection = M::Collection(); req.metadata.emplace(); req.metadata->year = std::format( "{:%Y}", now ); req.metadata->month = std::format( "{:%m}", now ); auto resp = spt::mongoservice::api::repository::remove( req ); if ( !resp.has_value() ) { LOG_WARN << "Error deleting document(s) in " << M::Database() << ':' << M::Collection() << ". " << bsoncxx::to_json( req.document->view() ) << ". " << magic_enum::enum_name( resp.error().cause ) << ". " << resp.error().message; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count() }; } if ( resp.value().success.empty() ) { LOG_WARN << "No documents deleted for " << M::Database() << ':' << M::Collection() << bsoncxx::to_json( req.document->view() ); const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 204, delta.count() }; } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count() }; } catch ( const std::exception& ex ) { LOG_WARN << "Error removing document(s) in " << M::Database() << ':' << M::Collection() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error removing document(s) in " << M::Database() << ':' << M::Collection() << LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count() }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<model::Entities<model::VersionHistorySummary>>> versionHistorySummary( bsoncxx::oid id ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto st = std::chrono::steady_clock::now(); try { auto req = Request::retrieve( std::string{ model::VersionHistorySummary::Database() }, std::string{ model::VersionHistorySummary::Collection() }, document{} << "database" << M::Database() << "collection" << M::Collection() << "entity._id" << id << finalize ); req.options = document{} << "projection" << open_document << "_id" << 1 << "action" << 1 << "created" << 1 << "metadata" << 1 << close_document << "sort" << open_document << "_id" << 1 << close_document << finalize; const auto [type, opt] = execute( req ); if ( !opt ) { LOG_WARN << "Unable to retrieve version history summary documents for " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << req.document << ". " << *req.options; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 424, delta.count(), std::nullopt }; } const auto view = opt->view(); if ( const auto err = spt::util::bsonValueIfExists<std::string>( "error", view ); err ) { LOG_WARN << "Error retrieving version history summary documents for " << M::Database() << ':' << M::Collection() << ':' << id << ". " << req.document << ". " << *req.options << ". " << view; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } auto arr = spt::util::bsonValueIfExists<bsoncxx::array::view>( "results", view ); if ( !arr ) { LOG_WARN << "No version history summary documents for " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << req.document << ". " << *req.options << ". " << view; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 404, delta.count(), std::nullopt }; } model::Entities<model::VersionHistorySummary> ms; ms.entities.reserve( 8 ); std::for_each( std::cbegin( *arr ), std::cend( *arr ), [&ms]( const auto& d ) { ms.entities.emplace_back( d.get_document().value ); } ); ms.page = std::size( ms.entities ); ms.total = ms.page; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( ms ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error retrieving version history summary documents for " << M::Database() << ':' << M::Collection() << ':' << id.to_string() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error retrieving version history documents for " << M::Database() << ':' << M::Collection() << ':' << id.to_string(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } template <Model M> std::tuple<int16_t, int64_t, std::optional<model::VersionHistoryDocument<M>>> versionHistoryDocument( bsoncxx::oid id ) { using bsoncxx::builder::stream::document; using bsoncxx::builder::stream::open_document; using bsoncxx::builder::stream::close_document; using bsoncxx::builder::stream::finalize; using spt::mongoservice::api::execute; using spt::mongoservice::api::Request; const auto st = std::chrono::steady_clock::now(); try { auto req = Request::retrieve( std::string{ model::VersionHistorySummary::Database() }, std::string{ model::VersionHistorySummary::Collection() }, document{} << "_id" << id << "database" << M::Database() << "collection" << M::Collection() << finalize ); const auto [type, opt] = execute( req ); if ( !opt ) { LOG_WARN << "Unable to retrieve version history document for " << M::Database() << ':' << M::Collection() << " with id: " << id << ". " << req.document; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 424, delta.count(), std::nullopt }; } const auto view = opt->view(); if ( const auto err = spt::util::bsonValueIfExists<std::string>( "error", view ); err ) { LOG_WARN << "Error retrieving version history document for " << M::Database() << ':' << M::Collection() << " with id: " << id << ". " << req.document << ". " << view; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 417, delta.count(), std::nullopt }; } auto doc = spt::util::bsonValueIfExists<bsoncxx::document::view>( "result", view ); if ( !doc ) { LOG_WARN << "No version history document for " << M::Database() << ':' << M::Collection() << " with id: " << id << ". " << req.document << ". " << view; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 404, delta.count(), std::nullopt }; } model::VersionHistoryDocument<M> ms{ *doc }; const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 200, delta.count(), std::move( ms ) }; } catch ( const std::exception& ex ) { LOG_WARN << "Error retrieving version history document for " << M::Database() << ':' << M::Collection() << " with id: " << id.to_string() << ". " << ex.what(); LOG_WARN << util::stacktrace(); } catch ( ... ) { LOG_WARN << "Unknown error retrieving version history document for " << M::Database() << ':' << M::Collection() << " with id: " << id.to_string(); LOG_WARN << util::stacktrace(); } const auto et = std::chrono::steady_clock::now(); const auto delta = std::chrono::duration_cast<std::chrono::nanoseconds>( et - st ); return { 422, delta.count(), std::nullopt }; } }

A simple structure representing a BSON ObjectId that is to be used as a query filter. Also illustrates using customer serialisation to specify nested fields using dot notation.

// // Created by Rakesh on 13/01/2025. // #pragma once #include "model/defaultoid.hpp" #include <boost/json/object.hpp> #include <bsoncxx/builder/stream/document.hpp> #include <mongo-service/common/visit_struct/visit_struct_intrusive.hpp> namespace spt::http2::rest::db::filter { struct Id { Id( bsoncxx::oid customer, bsoncxx::oid id ) : id{ id }, customer{ customer } {} explicit Id( bsoncxx::oid id ) : id{ id } {} Id() = default; ~Id() = default; Id(const Id&) = default; Id& operator=(const Id&) = default; Id(Id&&) = default; Id& operator=(Id&&) = default; BEGIN_VISITABLES(Id); VISITABLE_DIRECT_INIT(bsoncxx::oid, id, {model::DEFAULT_OID}); std::optional<bsoncxx::oid> customer; END_VISITABLES; }; inline void populate( const Id& filter, bsoncxx::builder::stream::document& builder ) { if ( filter.customer ) builder << "customer._id" << *filter.customer; } inline void populate( const Id& filter, boost::json::object& object ) { if ( filter.customer ) object.emplace( "customer.id", filter.customer->to_string() ); } }

A simple structure representing a field in a document and its value to use as a query filter.

// // Created by Rakesh on 14/01/2025. // #pragma once #include <boost/json/object.hpp> #include <bsoncxx/builder/stream/document.hpp> #include <mongo-service/common/visit_struct/visit_struct_intrusive.hpp> #include <mongo-service/common/util/json.hpp> namespace spt::http2::rest::db::filter { template <typename ValueType> struct Property { Property( std::string_view property, ValueType value, std::string_view customer = {} ) : property( property ), customer( customer ), value{ std::move( value ) } {} Property() = default; ~Property() = default; Property(Property&&) = default; Property& operator=(Property&&) = default; Property(const Property&) = delete; Property& operator=(const Property&) = delete; BEGIN_VISITABLES(Property); std::string property; std::string customer; ValueType value; END_VISITABLES; }; template <typename ValueType> void populate( const Property<ValueType>& filter, bsoncxx::builder::stream::document& builder ) { builder << filter.property << filter.value; if ( !filter.customer.empty() ) builder << "customer.code" << filter.customer; } template <typename ValueType> void populate( const Property<ValueType>& filter, boost::json::object& object ) { object.emplace( filter.property, spt::util::json::json( filter.value ) ); if ( !filter.customer.empty() ) object.emplace( "customer.code", filter.customer ); } }

A structure representing a range of dates along with some additional criterion to use as a query filter.

// // Created by Rakesh on 15/01/2025. // #pragma once #include <chrono> #include <boost/json/object.hpp> #include <bsoncxx/builder/stream/document.hpp> #include <mongo-service/common/visit_struct/visit_struct_intrusive.hpp> namespace spt::http2::rest::db::filter { struct Between { Between() = default; ~Between() = default; Between(Between&&) = default; Between& operator=(Between&&) = default; Between(const Between&) = delete; Between& operator=(const Between&) = delete; BEGIN_VISITABLES(Between); std::string field; std::chrono::time_point<std::chrono::system_clock> from; std::chrono::time_point<std::chrono::system_clock> to; std::optional<std::chrono::time_point<std::chrono::system_clock>> after; std::optional<bool> ignore; bool descending{ false }; END_VISITABLES; }; void populate( const Between& filter, bsoncxx::builder::stream::document& builder ); void populate( const Between& filter, boost::json::object& object ); }
Last modified: 23 January 2025