|
|
@@ -0,0 +1,340 @@
|
|
|
+#include "smartbotic/database/collection.hpp"
|
|
|
+
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
+
|
|
|
+namespace smartbotic::database {
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// SchemaValidator Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+SchemaValidator::SchemaValidator(nlohmann::json schema) : schema_(std::move(schema)) {}
|
|
|
+
|
|
|
+auto SchemaValidator::Validate(const nlohmann::json& data) const -> std::string {
|
|
|
+ if (schema_.is_null()) {
|
|
|
+ return ""; // No schema defined, all documents are valid
|
|
|
+ }
|
|
|
+
|
|
|
+ return ValidateType(data, schema_, "");
|
|
|
+}
|
|
|
+
|
|
|
+auto SchemaValidator::ValidateType(const nlohmann::json& value, const nlohmann::json& type_spec,
|
|
|
+ const std::string& path) const -> std::string {
|
|
|
+ // Get the expected type
|
|
|
+ std::string type = "object"; // Default type
|
|
|
+ if (type_spec.contains("type") && type_spec["type"].is_string()) {
|
|
|
+ type = type_spec["type"].get<std::string>();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check type matches
|
|
|
+ if (type == "object") {
|
|
|
+ if (!value.is_object()) {
|
|
|
+ return "Expected object at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ return ValidateObject(value, type_spec, path);
|
|
|
+ }
|
|
|
+ if (type == "array") {
|
|
|
+ if (!value.is_array()) {
|
|
|
+ return "Expected array at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ return ValidateArray(value, type_spec, path);
|
|
|
+ }
|
|
|
+ if (type == "string") {
|
|
|
+ if (!value.is_string()) {
|
|
|
+ return "Expected string at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ } else if (type == "number") {
|
|
|
+ if (!value.is_number()) {
|
|
|
+ return "Expected number at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ } else if (type == "integer") {
|
|
|
+ if (!value.is_number_integer()) {
|
|
|
+ return "Expected integer at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ } else if (type == "boolean") {
|
|
|
+ if (!value.is_boolean()) {
|
|
|
+ return "Expected boolean at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ } else if (type == "null") {
|
|
|
+ if (!value.is_null()) {
|
|
|
+ return "Expected null at " + (path.empty() ? "root" : path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
+auto SchemaValidator::ValidateObject(const nlohmann::json& value, const nlohmann::json& type_spec,
|
|
|
+ const std::string& path) const -> std::string {
|
|
|
+ // Check required fields
|
|
|
+ if (type_spec.contains("required") && type_spec["required"].is_array()) {
|
|
|
+ for (const auto& req : type_spec["required"]) {
|
|
|
+ if (req.is_string()) {
|
|
|
+ std::string field_name = req.get<std::string>();
|
|
|
+ if (!value.contains(field_name)) {
|
|
|
+ std::string field_path = path.empty() ? field_name : path + "." + field_name;
|
|
|
+ return "Missing required field: " + field_path;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate properties
|
|
|
+ if (type_spec.contains("properties") && type_spec["properties"].is_object()) {
|
|
|
+ for (const auto& [prop_name, prop_spec] : type_spec["properties"].items()) {
|
|
|
+ if (value.contains(prop_name)) {
|
|
|
+ std::string prop_path = path.empty() ? prop_name : path + "." + prop_name;
|
|
|
+ std::string error = ValidateType(value[prop_name], prop_spec, prop_path);
|
|
|
+ if (!error.empty()) {
|
|
|
+ return error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
+auto SchemaValidator::ValidateArray(const nlohmann::json& value, const nlohmann::json& type_spec,
|
|
|
+ const std::string& path) const -> std::string {
|
|
|
+ // Check min/max items
|
|
|
+ if (type_spec.contains("minItems") && type_spec["minItems"].is_number_integer()) {
|
|
|
+ size_t min_items = type_spec["minItems"].get<size_t>();
|
|
|
+ if (value.size() < min_items) {
|
|
|
+ return "Array at " + (path.empty() ? "root" : path) + " has fewer than " + std::to_string(min_items) +
|
|
|
+ " items";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type_spec.contains("maxItems") && type_spec["maxItems"].is_number_integer()) {
|
|
|
+ size_t max_items = type_spec["maxItems"].get<size_t>();
|
|
|
+ if (value.size() > max_items) {
|
|
|
+ return "Array at " + (path.empty() ? "root" : path) + " has more than " + std::to_string(max_items) +
|
|
|
+ " items";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Validate items
|
|
|
+ if (type_spec.contains("items") && type_spec["items"].is_object()) {
|
|
|
+ size_t index = 0;
|
|
|
+ for (const auto& item : value) {
|
|
|
+ std::string item_path = path + "[" + std::to_string(index) + "]";
|
|
|
+ std::string error = ValidateType(item, type_spec["items"], item_path);
|
|
|
+ if (!error.empty()) {
|
|
|
+ return error;
|
|
|
+ }
|
|
|
+ ++index;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Collection Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+Collection::Collection(std::string name)
|
|
|
+ : name_(std::move(name)), created_at_(Timestamp::Now()), updated_at_(created_at_) {}
|
|
|
+
|
|
|
+Collection::Collection(std::string name, nlohmann::json schema)
|
|
|
+ : name_(std::move(name)), created_at_(Timestamp::Now()), updated_at_(created_at_), validator_(std::move(schema)) {}
|
|
|
+
|
|
|
+Collection::Collection(Collection&& other) noexcept {
|
|
|
+ std::unique_lock lock(other.mutex_);
|
|
|
+ name_ = std::move(other.name_);
|
|
|
+ created_at_ = other.created_at_;
|
|
|
+ updated_at_ = other.updated_at_;
|
|
|
+ validator_ = std::move(other.validator_);
|
|
|
+ documents_ = std::move(other.documents_);
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::operator=(Collection&& other) noexcept -> Collection& {
|
|
|
+ if (this != &other) {
|
|
|
+ std::scoped_lock lock(mutex_, other.mutex_);
|
|
|
+ name_ = std::move(other.name_);
|
|
|
+ created_at_ = other.created_at_;
|
|
|
+ updated_at_ = other.updated_at_;
|
|
|
+ validator_ = std::move(other.validator_);
|
|
|
+ documents_ = std::move(other.documents_);
|
|
|
+ }
|
|
|
+ return *this;
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::GetSchema() const -> const nlohmann::json& {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+ return validator_.GetSchema();
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::HasSchema() const -> bool {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+ return validator_.HasSchema();
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Count() const -> size_t {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+ return documents_.size();
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Create(const nlohmann::json& data, std::string_view id) -> Result<Document> {
|
|
|
+ // Validate against schema if defined
|
|
|
+ if (validator_.HasSchema()) {
|
|
|
+ std::string error = validator_.Validate(data);
|
|
|
+ if (!error.empty()) {
|
|
|
+ spdlog::warn("Document validation failed in collection '{}': {}", name_, error);
|
|
|
+ return Result<Document>::Error("Validation failed: " + error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ std::unique_lock lock(mutex_);
|
|
|
+
|
|
|
+ // Create document with ID
|
|
|
+ std::string doc_id = id.empty() ? Document::GenerateUuid() : std::string(id);
|
|
|
+
|
|
|
+ // Check for duplicate ID
|
|
|
+ if (documents_.contains(doc_id)) {
|
|
|
+ return Result<Document>::Error("Document with ID '" + doc_id + "' already exists");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create and insert document
|
|
|
+ Document doc(doc_id, data);
|
|
|
+ auto [it, inserted] = documents_.emplace(doc_id, std::move(doc));
|
|
|
+
|
|
|
+ TouchUnsafe();
|
|
|
+
|
|
|
+ spdlog::debug("Created document '{}' in collection '{}'", doc_id, name_);
|
|
|
+ return Result<Document>::Ok(it->second);
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Get(std::string_view id) const -> Result<Document> {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+
|
|
|
+ auto it = documents_.find(std::string(id));
|
|
|
+ if (it == documents_.end()) {
|
|
|
+ return Result<Document>::Error("Document with ID '" + std::string(id) + "' not found");
|
|
|
+ }
|
|
|
+
|
|
|
+ return Result<Document>::Ok(it->second);
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Update(std::string_view id, const nlohmann::json& data, bool merge, int64_t expected_version)
|
|
|
+ -> Result<Document> {
|
|
|
+ // Validate against schema if defined
|
|
|
+ if (validator_.HasSchema()) {
|
|
|
+ std::string error = validator_.Validate(data);
|
|
|
+ if (!error.empty()) {
|
|
|
+ spdlog::warn("Document validation failed in collection '{}': {}", name_, error);
|
|
|
+ return Result<Document>::Error("Validation failed: " + error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ std::unique_lock lock(mutex_);
|
|
|
+
|
|
|
+ auto it = documents_.find(std::string(id));
|
|
|
+ if (it == documents_.end()) {
|
|
|
+ return Result<Document>::Error("Document with ID '" + std::string(id) + "' not found");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check version for optimistic locking
|
|
|
+ if (expected_version != 0 && it->second.GetVersion() != expected_version) {
|
|
|
+ return Result<Document>::Error("Version mismatch: expected " + std::to_string(expected_version) + ", got " +
|
|
|
+ std::to_string(it->second.GetVersion()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update document
|
|
|
+ if (merge) {
|
|
|
+ it->second.MergeData(data);
|
|
|
+ } else {
|
|
|
+ it->second.SetData(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ TouchUnsafe();
|
|
|
+
|
|
|
+ spdlog::debug("Updated document '{}' in collection '{}'", id, name_);
|
|
|
+ return Result<Document>::Ok(it->second);
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Delete(std::string_view id, int64_t expected_version) -> Result<void> {
|
|
|
+ std::unique_lock lock(mutex_);
|
|
|
+
|
|
|
+ auto it = documents_.find(std::string(id));
|
|
|
+ if (it == documents_.end()) {
|
|
|
+ return Result<void>::Error("Document with ID '" + std::string(id) + "' not found");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check version for optimistic locking
|
|
|
+ if (expected_version != 0 && it->second.GetVersion() != expected_version) {
|
|
|
+ return Result<void>::Error("Version mismatch: expected " + std::to_string(expected_version) + ", got " +
|
|
|
+ std::to_string(it->second.GetVersion()));
|
|
|
+ }
|
|
|
+
|
|
|
+ documents_.erase(it);
|
|
|
+ TouchUnsafe();
|
|
|
+
|
|
|
+ spdlog::debug("Deleted document '{}' from collection '{}'", id, name_);
|
|
|
+ return Result<void>::Ok();
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Exists(std::string_view id) const -> bool {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+ return documents_.contains(std::string(id));
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::GetAll() const -> std::vector<Document> {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+
|
|
|
+ std::vector<Document> result;
|
|
|
+ result.reserve(documents_.size());
|
|
|
+
|
|
|
+ for (const auto& [_, doc] : documents_) {
|
|
|
+ result.push_back(doc);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::GetMany(const std::vector<std::string>& ids) const
|
|
|
+ -> std::pair<std::vector<Document>, std::vector<std::string>> {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+
|
|
|
+ std::vector<Document> found;
|
|
|
+ std::vector<std::string> missing;
|
|
|
+
|
|
|
+ for (const auto& id : ids) {
|
|
|
+ auto it = documents_.find(id);
|
|
|
+ if (it != documents_.end()) {
|
|
|
+ found.push_back(it->second);
|
|
|
+ } else {
|
|
|
+ missing.push_back(id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {std::move(found), std::move(missing)};
|
|
|
+}
|
|
|
+
|
|
|
+auto Collection::Find(const std::function<bool(const Document&)>& predicate) const -> std::vector<Document> {
|
|
|
+ std::shared_lock lock(mutex_);
|
|
|
+
|
|
|
+ std::vector<Document> result;
|
|
|
+
|
|
|
+ for (const auto& [_, doc] : documents_) {
|
|
|
+ if (predicate(doc)) {
|
|
|
+ result.push_back(doc);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+void Collection::Clear() {
|
|
|
+ std::unique_lock lock(mutex_);
|
|
|
+ documents_.clear();
|
|
|
+ TouchUnsafe();
|
|
|
+ spdlog::debug("Cleared all documents from collection '{}'", name_);
|
|
|
+}
|
|
|
+
|
|
|
+void Collection::TouchUnsafe() {
|
|
|
+ updated_at_ = Timestamp::Now();
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::database
|