#pragma once #include "adminive/http.hpp" #include "adminive/status.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace adminive { struct Collection_Query { std::size_t page{1}; std::size_t per_page{20}; std::string order_by; std::string order_dir; std::map fields; }; template requires Table_View_Described_Type && std::default_initializable && std::copy_constructible && std::assignable_from class Collection_Service { public: explicit Collection_Service(std::string path, std::vector initial = {}) : path_(std::move(path)) { for(auto& value : initial) { entries_.push_back(Entry{next_id_++, std::move(value)}); } } template void register_overview_status(std::string api, std::uint64_t interval = 2000) { overview_status_api_ = std::move(api); overview_status_descriptor_ = to_status_descriptor_json(); overview_status_interval_ = interval; } template void register_status(std::uint64_t interval = 2000) { using Function_Type = decltype(Function); static_assert(std::is_member_function_pointer_v); static_assert(std::is_invocable_v); using Status = std::remove_cvref_t>; static_assert(Described_Type); status_descriptor_ = to_status_descriptor_json(); status_interval_ = interval; status_reader_ = [](T& value) { return to_status_json(std::invoke(Function, value)); }; } const std::string& path() const noexcept { return path_; } bool has_status() const noexcept { return static_cast(status_reader_); } bool user_reorderable() const { return describe_table_view().user_reorderable; } std::size_t size() const { std::scoped_lock lock(mutex_); return entries_.size(); } Json items_json() const { std::scoped_lock lock(mutex_); Json items = json_array(); for(const auto& entry : entries_) { json_append(items, encode_entry(entry)); } return items; } Json view_schema() const { return to_view_json(describe_table_view()); } Json amis_schema() const { const Json* item_status = status_reader_ ? &status_descriptor_ : nullptr; const Json* overview_status = overview_status_api_.empty() ? nullptr : &overview_status_descriptor_; return to_amis_table_schema(path_, describe_table_view(), item_status, status_interval_, overview_status, overview_status_api_, overview_status_interval_); } Http_Response descriptor_response() const noexcept { return safe_response([&] { return make_http_success(to_descriptor_json()); }); } Http_Response view_response() const noexcept { return safe_response([&] { return make_http_success(view_schema()); }); } Http_Response amis_response() const noexcept { return safe_response([&] { return make_http_success(amis_schema()); }); } Http_Response status_descriptor_response() const noexcept { return safe_response([&] { if(!status_reader_) { return make_http_error(404, "item status is not configured"); } return make_http_success(status_descriptor_); }); } Http_Response status_response(std::uint64_t id) noexcept { return safe_response([&] { if(!status_reader_) { return make_http_error(404, "item status is not configured"); } std::scoped_lock lock(mutex_); const auto iterator = find_entry(id); if(iterator == entries_.end()) { return make_http_error(404, "record not found"); } return make_http_success(status_reader_(iterator->value)); }); } Http_Response list_response(Collection_Query query = {}) const noexcept { return safe_response([&] { const std::string error = query_error(query); if(!error.empty()) { return make_http_error(400, error); } std::scoped_lock lock(mutex_); std::vector ordered_entries; ordered_entries.reserve(entries_.size()); for(const auto& entry : entries_) { ordered_entries.push_back(&entry); } apply_filters(query, ordered_entries); apply_sort(query, ordered_entries); const std::size_t page = query.page == 0 ? 1 : query.page; const std::size_t per_page = query.per_page == 0 ? 20 : query.per_page; const std::size_t page_index = page - 1; const std::size_t offset = page_index > ordered_entries.size() / per_page ? ordered_entries.size() : page_index * per_page; const std::size_t end = offset + std::min(per_page, ordered_entries.size() - offset); Json items = json_array(); for(std::size_t index = offset; index < end; ++index) { json_append(items, encode_entry(*ordered_entries[index])); } Json data = json_object(); json_set(data, "items", std::move(items)); json_set(data, "total", ordered_entries.size()); return make_http_success(std::move(data)); }); } Http_Response item_response(std::uint64_t id) const noexcept { return safe_response([&] { std::scoped_lock lock(mutex_); const auto iterator = find_entry(id); if(iterator == entries_.end()) { return make_http_error(404, "record not found"); } return make_http_success(encode_entry(*iterator)); }); } Http_Response create_response(std::string_view body) noexcept { return safe_response([&] { Json input; try { input = parse_json(body); } catch(const std::exception& error) { return make_http_error(400, error.what()); } Json_Adapter::erase(input, "id"); T value{}; const auto result = apply_frontend_create(value, input); if(!result.success) { return make_http_error(422, result); } std::scoped_lock lock(mutex_); entries_.push_back(Entry{next_id_++, std::move(value)}); return make_http_success(encode_entry(entries_.back()), "created"); }); } Http_Response update_response(std::uint64_t id, std::string_view body) noexcept { return safe_response([&] { Json patch; try { patch = parse_json(body); } catch(const std::exception& error) { return make_http_error(400, error.what()); } Json_Adapter::erase(patch, "id"); std::scoped_lock lock(mutex_); const auto iterator = find_entry(id); if(iterator == entries_.end()) { return make_http_error(404, "record not found"); } const auto result = apply_frontend_patch(iterator->value, patch); if(!result.success) { return make_http_error(422, result); } return make_http_success(encode_entry(*iterator), result.message); }); } Http_Response delete_response(std::uint64_t id) noexcept { return safe_response([&] { std::scoped_lock lock(mutex_); const auto iterator = find_entry(id); if(iterator == entries_.end()) { return make_http_error(404, "record not found"); } entries_.erase(iterator); return make_http_success(json_object(), "deleted"); }); } Http_Response reorder_response(std::string_view body) noexcept { return safe_response([&] { std::vector ids; try { const Json input = parse_json(body); ids = read_order_ids(Json_Adapter::at(input, "ids")); } catch(const std::exception& error) { return make_http_error(400, error.what()); } std::scoped_lock lock(mutex_); reorder(ids); return make_http_success(json_object(), "reordered"); }); } private: struct Entry { std::uint64_t id{}; T value; }; using Iterator = typename std::vector::iterator; using Const_Iterator = typename std::vector::const_iterator; template static Http_Response safe_response(Function&& function) noexcept { try { return std::forward(function)(); } catch(const Json_Assignment_Error& error) { return make_http_error(422, error.result()); } catch(const Field_Validation_Error& error) { return make_http_error(422, object_commit_error(error)); } catch(const std::exception& error) { return make_http_error(500, error.what()); } catch(...) { return make_http_error(500, "unknown server error"); } } static int compare_json_values(const Json& left, const Json& right) { if(Json_Adapter::is_number(left) && Json_Adapter::is_number(right)) { const long double left_value = Json_Adapter::number(left); const long double right_value = Json_Adapter::number(right); return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; } if(Json_Adapter::is_string(left) && Json_Adapter::is_string(right)) { const auto left_value = json_get(left); const auto right_value = json_get(right); return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; } if(Json_Adapter::is_boolean(left) && Json_Adapter::is_boolean(right)) { const bool left_value = json_get(left); const bool right_value = json_get(right); return left_value == right_value ? 0 : left_value ? 1 : -1; } const std::string left_value = dump_json(left); const std::string right_value = dump_json(right); return left_value < right_value ? -1 : left_value > right_value ? 1 : 0; } static const Table_Column* find_table_column(const Table_View& view, std::string_view name) { const auto iterator = std::find_if(view.columns.begin(), view.columns.end(), [&](const Table_Column& column) { return column.field == name; }); return iterator == view.columns.end() ? nullptr : &*iterator; } static bool is_sortable_field(const Table_View& view, const std::string& name) { if(name == "id") { return true; } const auto* column = find_table_column(view, name); return column && column->sortable; } static std::string query_error(const Collection_Query& query) { const auto view = describe_table_view(); for(const auto& [name, value] : query.fields) { static_cast(value); const auto* column = find_table_column(view, name); if(!column || (!column->searchable && !column->filterable)) { return "table query field is not searchable or filterable: " + name; } } if(!query.order_by.empty() && !is_sortable_field(view, query.order_by)) { return "table query field is not sortable: " + query.order_by; } if(!query.order_by.empty() && !query.order_dir.empty() && query.order_dir != "asc" && query.order_dir != "desc") { return "table order direction must be asc or desc"; } return {}; } static std::string query_text(const Json& value) { if(Json_Adapter::is_string(value)) { return json_get(value); } if(Json_Adapter::is_boolean(value)) { return json_get(value) ? "true" : "false"; } if(Json_Adapter::is_number(value)) { char buffer[64]; const auto [end, error] = std::to_chars(buffer, buffer + sizeof(buffer), Json_Adapter::number(value), std::chars_format::general); if(error == std::errc{}) { return std::string(buffer, end); } } return dump_json(value); } static std::string lowercase(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) { return static_cast(std::tolower(character)); }); return value; } static bool matches_query(const Json& value, std::string_view query, const Table_Column& column) { const std::string text = query_text(value); if(column.searchable) { return lowercase(text).find(lowercase(std::string(query))) != std::string::npos; } return text == query; } static void apply_filters(const Collection_Query& query, std::vector& entries) { if(query.fields.empty()) { return; } const auto view = describe_table_view(); for(const auto& [name, value] : query.fields) { const auto* column = find_table_column(view, name); entries.erase(std::remove_if(entries.begin(), entries.end(), [&](const Entry* entry) { const Json encoded = to_frontend_json(entry->value); if(!Json_Adapter::contains(encoded, name)) { return true; } return !matches_query(Json_Adapter::at(encoded, name), value, *column); }), entries.end()); } } static std::vector read_order_ids(const Json& value) { std::vector result; if(Json_Adapter::is_array(value)) { for(std::size_t index = 0; index < Json_Adapter::size(value); ++index) { const auto& item = Json_Adapter::at(value, index); result.push_back(Json_Adapter::is_string(item) ? std::stoull(json_get(item)) : json_get(item)); } return result; } const std::string text = json_get(value); std::size_t begin{}; while(begin < text.size()) { const auto end = text.find(',', begin); result.push_back(std::stoull(text.substr(begin, end == std::string::npos ? text.size() - begin : end - begin))); if(end == std::string::npos) { break; } begin = end + 1; } return result; } void apply_sort(const Collection_Query& query, std::vector& entries) const { const auto view = describe_table_view(); const std::string order_by = query.order_by.empty() ? view.default_order_by : query.order_by; const std::string order_dir = query.order_by.empty() ? view.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir; if(order_by.empty()) { return; } if(!is_sortable_field(view, order_by)) { return; } const bool descending = order_dir == "desc"; std::stable_sort(entries.begin(), entries.end(), [&](const Entry* left, const Entry* right) { int comparison{}; if(order_by == "id") { comparison = left->id < right->id ? -1 : left->id > right->id ? 1 : 0; } else { const auto left_json = to_frontend_json(left->value); const auto right_json = to_frontend_json(right->value); comparison = compare_json_values(Json_Adapter::at(left_json, order_by), Json_Adapter::at(right_json, order_by)); } return descending ? comparison > 0 : comparison < 0; }); } Iterator find_entry(std::uint64_t id) { return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) { return entry.id == id; }); } Const_Iterator find_entry(std::uint64_t id) const { return std::find_if(entries_.begin(), entries_.end(), [id](const Entry& entry) { return entry.id == id; }); } static Json encode_entry(const Entry& entry) { Json result = to_frontend_json(entry.value); json_set(result, "id", entry.id); return result; } void reorder(const std::vector& ids) { std::vector remaining = std::move(entries_); std::vector reordered; reordered.reserve(remaining.size()); for(const auto id : ids) { const auto iterator = std::find_if(remaining.begin(), remaining.end(), [id](const Entry& entry) { return entry.id == id; }); if(iterator != remaining.end()) { reordered.push_back(std::move(*iterator)); remaining.erase(iterator); } } for(auto& entry : remaining) { reordered.push_back(std::move(entry)); } entries_ = std::move(reordered); } std::string path_; std::string overview_status_api_; Json overview_status_descriptor_{}; std::uint64_t overview_status_interval_{2000}; Json status_descriptor_{}; std::function status_reader_; std::uint64_t status_interval_{2000}; std::vector entries_; std::uint64_t next_id_{1}; mutable Lock mutex_; }; }