|
|
@@ -480,7 +480,14 @@ public:
|
|
|
return {response.id(), response.inserted()};
|
|
|
}
|
|
|
|
|
|
- bool remove(const std::string& collection, const std::string& id) {
|
|
|
+ // errorOut is optional: nullptr keeps the old single-value remove()'s
|
|
|
+ // behaviour (log at spdlog::error and swallow the message). Non-null is
|
|
|
+ // what lets a caller distinguish "already gone" from "blocked" - a
|
|
|
+ // relation with on_delete=restrict/no_action refuses via a real gRPC
|
|
|
+ // FAILED_PRECONDITION, not deleted=false, and that message is the only
|
|
|
+ // way an operator learns WHAT blocked the delete.
|
|
|
+ bool removeImpl(const std::string& collection, const std::string& id,
|
|
|
+ std::string* errorOut) {
|
|
|
smartbotic::databasepb::DeleteRequest request;
|
|
|
request.set_collection(qualify(collection));
|
|
|
request.set_id(id);
|
|
|
@@ -507,12 +514,22 @@ public:
|
|
|
}
|
|
|
if (!status.ok()) {
|
|
|
spdlog::error("Client::remove failed after retries: {}", status.error_message());
|
|
|
+ if (errorOut != nullptr) *errorOut = status.error_message();
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
return response.deleted();
|
|
|
}
|
|
|
|
|
|
+ bool remove(const std::string& collection, const std::string& id) {
|
|
|
+ return removeImpl(collection, id, nullptr);
|
|
|
+ }
|
|
|
+
|
|
|
+ bool remove(const std::string& collection, const std::string& id, std::string& errorOut) {
|
|
|
+ errorOut.clear();
|
|
|
+ return removeImpl(collection, id, &errorOut);
|
|
|
+ }
|
|
|
+
|
|
|
bool exists(const std::string& collection, const std::string& id) {
|
|
|
smartbotic::databasepb::ExistsRequest request;
|
|
|
request.set_collection(qualify(collection));
|
|
|
@@ -1091,6 +1108,50 @@ public:
|
|
|
return response.found();
|
|
|
}
|
|
|
|
|
|
+ // v2.11.0 T6b — a METHOD, not a CollectionConfig member (see the header
|
|
|
+ // doc comment for why). Sends a partial update touching ONLY
|
|
|
+ // relations_enforced: timestamp_precision is left unset (empty string,
|
|
|
+ // the server's "leave unchanged" sentinel) and versioning_enabled is
|
|
|
+ // left absent (proto3 field presence), so neither is disturbed.
|
|
|
+ bool setRelationsEnforced(const std::string& collection, bool enforced) {
|
|
|
+ smartbotic::databasepb::ConfigureCollectionRequest request;
|
|
|
+ request.set_collection(qualify(collection));
|
|
|
+ request.mutable_config()->set_relations_enforced(enforced);
|
|
|
+
|
|
|
+ smartbotic::databasepb::ConfigureCollectionResponse response;
|
|
|
+ grpc::ClientContext context;
|
|
|
+ setDeadline(context);
|
|
|
+
|
|
|
+ auto status = stub_->ConfigureCollection(&context, request, &response);
|
|
|
+ if (!status.ok()) {
|
|
|
+ spdlog::error("Client::setRelationsEnforced failed: {}", status.error_message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!response.success()) {
|
|
|
+ spdlog::error("Client::setRelationsEnforced rejected: {}", response.error());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool getRelationsEnforced(const std::string& collection) {
|
|
|
+ smartbotic::databasepb::GetCollectionConfigRequest request;
|
|
|
+ request.set_collection(qualify(collection));
|
|
|
+ smartbotic::databasepb::GetCollectionConfigResponse response;
|
|
|
+ grpc::ClientContext context;
|
|
|
+ setDeadline(context);
|
|
|
+
|
|
|
+ auto status = stub_->GetCollectionConfig(&context, request, &response);
|
|
|
+ if (!status.ok()) {
|
|
|
+ spdlog::error("Client::getRelationsEnforced failed: {}", status.error_message());
|
|
|
+ return true; // fail toward the safe (enforced) default
|
|
|
+ }
|
|
|
+ // Defaults to true (enforced) when never explicitly configured -
|
|
|
+ // mirrors the server's CollectionCfg default.
|
|
|
+ return !response.config().has_relations_enforced()
|
|
|
+ || response.config().relations_enforced();
|
|
|
+ }
|
|
|
+
|
|
|
Client::TimestampMigrationResult migrateCollectionTimestamps(
|
|
|
const std::string& collection,
|
|
|
const std::string& fromPrecision,
|
|
|
@@ -1356,6 +1417,109 @@ public:
|
|
|
return v;
|
|
|
}
|
|
|
|
|
|
+ // ===== v2.11.0 T6b — relation (referential integrity) management =====
|
|
|
+
|
|
|
+ Client::RelationDefinition relationFromProto(
|
|
|
+ const smartbotic::databasepb::RelationDefinition& pb) const {
|
|
|
+ Client::RelationDefinition r;
|
|
|
+ // Round-trip like listViews(): the caller declared "posts_by_user",
|
|
|
+ // it should list back as "posts_by_user", not "myproject:posts_by_user".
|
|
|
+ r.name = unqualify(pb.name());
|
|
|
+ r.child = unqualify(pb.child());
|
|
|
+ r.childField = pb.child_field();
|
|
|
+ r.parent = unqualify(pb.parent());
|
|
|
+ r.onDelete = pb.on_delete().empty() ? "restrict" : pb.on_delete();
|
|
|
+ r.validateOnWrite = pb.validate_on_write();
|
|
|
+ r.createdAt = pb.created_at();
|
|
|
+ r.updatedAt = pb.updated_at();
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool createRelation(const std::string& name, const std::string& childCollection,
|
|
|
+ const std::string& childField, const std::string& parentCollection,
|
|
|
+ const std::string& onDelete, bool validateOnWrite) {
|
|
|
+ smartbotic::databasepb::CreateRelationRequest request;
|
|
|
+ // name, child AND parent all need qualifying - qualifying only
|
|
|
+ // `child`/`parent` but not `name` is exactly the v2.4.2 createView
|
|
|
+ // bug (registry keyed on the bare name, every lookup sent the
|
|
|
+ // qualified one, the keys never met).
|
|
|
+ request.set_name(qualify(name));
|
|
|
+ request.set_child(qualify(childCollection));
|
|
|
+ request.set_child_field(childField);
|
|
|
+ request.set_parent(qualify(parentCollection));
|
|
|
+ request.set_on_delete(onDelete);
|
|
|
+ request.set_validate_on_write(validateOnWrite);
|
|
|
+
|
|
|
+ smartbotic::databasepb::CreateRelationResponse response;
|
|
|
+ grpc::ClientContext context;
|
|
|
+ setDeadline(context);
|
|
|
+
|
|
|
+ auto status = stub_->CreateRelation(&context, request, &response);
|
|
|
+ if (!status.ok()) {
|
|
|
+ spdlog::error("Client::createRelation failed: {}", status.error_message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!response.success()) {
|
|
|
+ spdlog::error("Client::createRelation rejected: {}", response.error());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool dropRelation(const std::string& name) {
|
|
|
+ smartbotic::databasepb::DropRelationRequest request;
|
|
|
+ request.set_name(qualify(name));
|
|
|
+ smartbotic::databasepb::DropRelationResponse response;
|
|
|
+ grpc::ClientContext context;
|
|
|
+ setDeadline(context);
|
|
|
+
|
|
|
+ auto status = stub_->DropRelation(&context, request, &response);
|
|
|
+ if (!status.ok()) {
|
|
|
+ spdlog::error("Client::dropRelation failed: {}", status.error_message());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!response.success()) {
|
|
|
+ spdlog::error("Client::dropRelation rejected: {}", response.error());
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<Client::RelationDefinition> listRelations() {
|
|
|
+ smartbotic::databasepb::ListRelationsRequest request;
|
|
|
+ // Scope the listing to this client's workspace, like listViews().
|
|
|
+ request.set_project(config_.project);
|
|
|
+ smartbotic::databasepb::ListRelationsResponse response;
|
|
|
+ grpc::ClientContext context;
|
|
|
+ setDeadline(context);
|
|
|
+
|
|
|
+ std::vector<Client::RelationDefinition> out;
|
|
|
+ auto status = stub_->ListRelations(&context, request, &response);
|
|
|
+ if (!status.ok()) {
|
|
|
+ spdlog::error("Client::listRelations failed: {}", status.error_message());
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+ out.reserve(response.relations_size());
|
|
|
+ for (const auto& pb : response.relations()) {
|
|
|
+ out.push_back(relationFromProto(pb));
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::optional<Client::RelationDefinition> getRelationInfo(const std::string& name) {
|
|
|
+ smartbotic::databasepb::GetRelationInfoRequest request;
|
|
|
+ request.set_name(qualify(name));
|
|
|
+ smartbotic::databasepb::GetRelationInfoResponse response;
|
|
|
+ grpc::ClientContext context;
|
|
|
+ setDeadline(context);
|
|
|
+
|
|
|
+ auto status = stub_->GetRelationInfo(&context, request, &response);
|
|
|
+ if (!status.ok() || !response.found()) {
|
|
|
+ return std::nullopt;
|
|
|
+ }
|
|
|
+ return relationFromProto(response.relation());
|
|
|
+ }
|
|
|
+
|
|
|
// ===== Event Subscription =====
|
|
|
|
|
|
class SubscriptionHandle {
|
|
|
@@ -2039,6 +2203,10 @@ bool Client::remove(const std::string& collection, const std::string& id) {
|
|
|
return impl_->remove(collection, id);
|
|
|
}
|
|
|
|
|
|
+bool Client::remove(const std::string& collection, const std::string& id, std::string& errorOut) {
|
|
|
+ return impl_->remove(collection, id, errorOut);
|
|
|
+}
|
|
|
+
|
|
|
bool Client::exists(const std::string& collection, const std::string& id) {
|
|
|
return impl_->exists(collection, id);
|
|
|
}
|
|
|
@@ -2205,6 +2373,33 @@ std::optional<Client::ViewDefinition> Client::getViewInfo(const std::string& nam
|
|
|
return impl_->getViewInfo(name);
|
|
|
}
|
|
|
|
|
|
+bool Client::createRelation(const std::string& name, const std::string& childCollection,
|
|
|
+ const std::string& childField, const std::string& parentCollection,
|
|
|
+ const std::string& onDelete, bool validateOnWrite) {
|
|
|
+ return impl_->createRelation(name, childCollection, childField, parentCollection,
|
|
|
+ onDelete, validateOnWrite);
|
|
|
+}
|
|
|
+
|
|
|
+bool Client::dropRelation(const std::string& name) {
|
|
|
+ return impl_->dropRelation(name);
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<Client::RelationDefinition> Client::listRelations() {
|
|
|
+ return impl_->listRelations();
|
|
|
+}
|
|
|
+
|
|
|
+std::optional<Client::RelationDefinition> Client::getRelationInfo(const std::string& name) {
|
|
|
+ return impl_->getRelationInfo(name);
|
|
|
+}
|
|
|
+
|
|
|
+bool Client::setRelationsEnforced(const std::string& collection, bool enforced) {
|
|
|
+ return impl_->setRelationsEnforced(collection, enforced);
|
|
|
+}
|
|
|
+
|
|
|
+bool Client::getRelationsEnforced(const std::string& collection) {
|
|
|
+ return impl_->getRelationsEnforced(collection);
|
|
|
+}
|
|
|
+
|
|
|
std::shared_ptr<void> Client::subscribe(const std::vector<std::string>& collections, EventCallback callback) {
|
|
|
return impl_->subscribe(collections, std::move(callback));
|
|
|
}
|