Skip to content

Commit

Permalink
Add compensating write error information
Browse files Browse the repository at this point in the history
  • Loading branch information
dacharyc committed Nov 17, 2023
1 parent f3f892c commit 29f56b8
Show file tree
Hide file tree
Showing 9 changed files with 537 additions and 1 deletion.
182 changes: 181 additions & 1 deletion examples/cpp/beta/sync/sync-errors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
#include <cpprealm/sdk.hpp>
#include <unistd.h>
#include <cpprealm/experimental/sdk.hpp>
#include <string>

// :replace-start: {
// "terms": {
// "SyncError_": ""
// "SyncError_": "",
// "Beta_SyncError_": ""
// }
// }

Expand All @@ -32,6 +34,16 @@ struct Beta_SyncError_Dog {
};
REALM_SCHEMA(Beta_SyncError_Dog, _id, name, age)

// :snippet-start: compensating-write-model
struct Beta_SyncError_Item {
primary_key<realm::object_id> _id{realm::object_id::generate()};
std::string ownerId;
std::string itemName;
int64_t complexity;
};
REALM_SCHEMA(Beta_SyncError_Item, _id, ownerId, itemName, complexity)
// :snippet-end:

TEST_CASE("set a sync error handler", "[error]") {
// :snippet-start: create-error-handler
auto appConfig = realm::App::configuration();
Expand Down Expand Up @@ -108,4 +120,172 @@ TEST_CASE("beta set a sync error handler", "[error]") {
});
sleep(5);
}

TEST_CASE("beta compensating write error outside query", "[error]") {
// :snippet-start: compensating-write-setup
auto appConfig = realm::App::configuration();
appConfig.app_id = APP_ID;
auto app = realm::App(appConfig);
auto user = app.login(realm::App::credentials::anonymous()).get();
auto dbConfig = user.flexible_sync_configuration();
// :remove-start:
realm::object_id primaryKey = { realm::object_id::generate() };

std::promise<realm::internal::bridge::sync_error> errorPromise;
std::future<realm::internal::bridge::sync_error> future = errorPromise.get_future();

dbConfig.sync_config().set_error_handler([&](const realm::sync_session& session, std::optional<realm::internal::bridge::sync_error> error) {
std::cerr << "A sync error occurred. Message: " << error->message() << std::endl;
errorPromise.set_value(*error); // :remove:
});
// :remove-end:
auto syncRealm = realm::experimental::db(dbConfig);
// :remove-start:
auto updateSubscriptionSuccess = syncRealm.subscriptions().update([](realm::mutable_sync_subscription_set &subs) {
subs.clear();
}).get();
CHECK(updateSubscriptionSuccess == true);
CHECK(syncRealm.subscriptions().size() == 0);
sleep(5);
// :remove-end:
// Add subscription
auto subscriptionUpdateSuccess = syncRealm.subscriptions().update([](realm::mutable_sync_subscription_set &subs) {
// Get Items from Atlas that match this query.
// Uses the queryable field `complexity`.
// Sync Item objects with complexity less than or equal to 4.
subs.add<Beta_SyncError_Item>("simple items", [](auto &obj) {
return obj.complexity <= 4;
});
}).get();
// :snippet-end:
REQUIRE(subscriptionUpdateSuccess == true);
CHECK(syncRealm.subscriptions().size() == 1);

// :snippet-start: successful-write-example
// Per the Device Sync permissions, users can only read and write data
// where the `Item.ownerId` property matches their own user ID.
auto simpleItem = Beta_SyncError_Item {
.ownerId = user.identifier(),
.itemName = "This item meets sync criteria",
.complexity = 3
};

// `simpleItem` successfully writes to the realm and syncs to Atlas
// because its data matches the subscription query (complexity <= 4)
// and its `ownerId` field matches the user ID.
syncRealm.write([&] {
syncRealm.add(std::move(simpleItem));
});
// :snippet-end:

auto syncedItems = syncRealm.objects<Beta_SyncError_Item>();
CHECK(syncedItems.size() == 1);
auto specificItem = syncedItems[0];
syncRealm.write([&] {
syncRealm.remove(specificItem);
});
auto syncSession = syncRealm.get_sync_session();
syncSession->wait_for_upload_completion().get();

// :snippet-start: compensating-write-example
// The complexity of this item is `7`. This is outside the bounds
// of the subscription query, which triggers a compensating write.
auto complexItem = Beta_SyncError_Item {
._id = primaryKey,
.ownerId = user.identifier(),
.itemName = "Test compensating writes",
.complexity = 7
};

// This should trigger a compensating write error when it tries to sync
// due to server-side permissions, which gets logged with the error handler.
syncRealm.write([&] {
syncRealm.add(std::move(complexItem));
});
// :snippet-end:
sleep(5);

std::string pkString = primaryKey.to_string();
auto receivedSyncError = future.get();
// :snippet-start: get-compensating-write-error-info
auto info = receivedSyncError.compensating_writes_info();
for (auto& v : info) {
std::cout << "A write was rejected with a compensating write error.\n";
std::cout << "An object of type " << v.object_name << "\n";
std::cout << "was rejected because " << v.reason << ".\n";
// :remove-start:
REQUIRE(v.primary_key == realm::object_id(primaryKey));
REQUIRE(v.object_name == "Beta_SyncError_Item");
REQUIRE(v.reason == "write to ObjectID(\"" + pkString + "\") in table \"Beta_SyncError_Item\" not allowed; object is outside of the current query view");
// :remove-end:
}
// :snippet-end:
}

TEST_CASE("beta compensating write error write doesn't match permissions", "[error]") {
auto appConfig = realm::App::configuration();
appConfig.app_id = APP_ID;
auto app = realm::App(appConfig);
auto user = app.login(realm::App::credentials::anonymous()).get();
auto dbConfig = user.flexible_sync_configuration();
realm::object_id primaryKey = { realm::object_id::generate() };

std::promise<realm::internal::bridge::sync_error> errorPromise;
std::future<realm::internal::bridge::sync_error> future = errorPromise.get_future();

dbConfig.sync_config().set_error_handler([&](const realm::sync_session& session, std::optional<realm::internal::bridge::sync_error> error) {
std::cerr << "A sync error occurred. Message: " << error->message() << std::endl;
errorPromise.set_value(*error); // :remove:
});
auto syncRealm = realm::experimental::db(dbConfig);
auto updateSubscriptionSuccess = syncRealm.subscriptions().update([](realm::mutable_sync_subscription_set &subs) {
subs.clear();
}).get();
CHECK(updateSubscriptionSuccess == true);
CHECK(syncRealm.subscriptions().size() == 0);
sleep(5);
// Add subscription
auto subscriptionUpdateSuccess = syncRealm.subscriptions().update([](realm::mutable_sync_subscription_set &subs) {
// Get Items from Atlas that match this query.
// Uses the queryable field `complexity`.
// Sync Item objects with complexity less than or equal to 4.
subs.add<Beta_SyncError_Item>("simple items", [](auto &obj) {
return obj.complexity <= 4;
});
}).get();
REQUIRE(subscriptionUpdateSuccess == true);
CHECK(syncRealm.subscriptions().size() == 1);

// :snippet-start: write-not-matching-permissions
// The `ownerId` of this item does not match the user ID of the logged-in
// user. The user does not have permissions to make this write, which
// triggers a compensating write.
auto itemWithWrongOwner = Beta_SyncError_Item {
._id = primaryKey, // :remove:
.ownerId = "not the current user",
.itemName = "Trigger an incorrect permissions compensating write",
.complexity = 1
};

syncRealm.write([&] {
syncRealm.add(std::move(itemWithWrongOwner));
});
// :snippet-end:
auto syncSession = syncRealm.get_sync_session();
syncSession->wait_for_upload_completion().get();

std::string pkString = primaryKey.to_string();
auto receivedSyncError = future.get();
auto info = receivedSyncError.compensating_writes_info();
for (auto& v : info) {
std::cout << "A write was rejected with a compensating write error.\n";
std::cout << "An object of type " << v.object_name << "\n";
std::cout << "was rejected because " << v.reason << ".\n";
// :remove-start:
REQUIRE(v.primary_key == realm::object_id(primaryKey));
REQUIRE(v.object_name == "Beta_SyncError_Item");
REQUIRE(v.reason == "write to ObjectID(\"" + pkString + "\") in table \"Beta_SyncError_Item\" not allowed");
// :remove-end:
}
}
// :replace-end:
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// The complexity of this item is `7`. This is outside the bounds
// of the subscription query, which triggers a compensating write.
auto complexItem = Item {
._id = primaryKey,
.ownerId = user.identifier(),
.itemName = "Test compensating writes",
.complexity = 7
};

// This should trigger a compensating write error when it tries to sync
// due to server-side permissions, which gets logged with the error handler.
syncRealm.write([&] {
syncRealm.add(std::move(complexItem));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
struct Item {
primary_key<realm::object_id> _id{realm::object_id::generate()};
std::string ownerId;
std::string itemName;
int64_t complexity;
};
REALM_SCHEMA(Item, _id, ownerId, itemName, complexity)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
auto appConfig = realm::App::configuration();
appConfig.app_id = APP_ID;
auto app = realm::App(appConfig);
auto user = app.login(realm::App::credentials::anonymous()).get();
auto dbConfig = user.flexible_sync_configuration();
auto syncRealm = realm::experimental::db(dbConfig);
// Add subscription
auto subscriptionUpdateSuccess = syncRealm.subscriptions().update([](realm::mutable_sync_subscription_set &subs) {
// Get Items from Atlas that match this query.
// Uses the queryable field `complexity`.
// Sync Item objects with complexity less than or equal to 4.
subs.add<Item>("simple items", [](auto &obj) {
return obj.complexity <= 4;
});
}).get();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
auto info = receivedSyncError.compensating_writes_info();
for (auto& v : info) {
std::cout << "A write was rejected with a compensating write error.\n";
std::cout << "An object of type " << v.object_name << "\n";
std::cout << "was rejected because " << v.reason << ".\n";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Per the Device Sync permissions, users can only read and write data
// where the `Item.ownerId` property matches their own user ID.
auto simpleItem = Item {
.ownerId = user.identifier(),
.itemName = "This item meets sync criteria",
.complexity = 3
};

// `simpleItem` successfully writes to the realm and syncs to Atlas
// because its data matches the subscription query (complexity <= 4)
// and its `ownerId` field matches the user ID.
syncRealm.write([&] {
syncRealm.add(std::move(simpleItem));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// The `ownerId` of this item does not match the user ID of the logged-in
// user. The user does not have permissions to make this write, which
// triggers a compensating write.
auto itemWithWrongOwner = Item {
.ownerId = "not the current user",
.itemName = "Trigger an incorrect permissions compensating write",
.complexity = 1
};

syncRealm.write([&] {
syncRealm.add(std::move(itemWithWrongOwner));
});
1 change: 1 addition & 0 deletions source/sdk/cpp/sync.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Sync Data Between Devices - C++ SDK Preview
:titlesonly:

Manage Sync Subscriptions </sdk/cpp/sync/sync-subscriptions>
Handle Sync Errors </sdk/cpp/sync/write-to-synced-realm>
Manage Sync Sessions </sdk/cpp/sync/manage-sync-session>
Handle Sync Errors </sdk/cpp/sync/handle-sync-errors>
Set the Sync Client Log Level </sdk/cpp/sync/log-level>
Expand Down
Loading

0 comments on commit 29f56b8

Please sign in to comment.