From 98fb5fe00a739be70137625489d1150cd9f35c35 Mon Sep 17 00:00:00 2001 From: Andrew Meyer Date: Wed, 15 Nov 2023 11:34:07 +0100 Subject: [PATCH 1/2] Add Complex Custom User Data Example * Replace simple custom user data example * Make all data show up in the same database * Create an example of custom user data using rules and triggers based on a storeId --- examples/rn-connection-and-error/README.md | 45 +++++- .../backend/auth/custom_user_data.json | 6 +- .../backend/auth/providers.json | 4 +- .../AuthExample/Users/rules.json | 22 --- .../Kiosk/relationships.json | 2 +- .../mongodb-atlas/StoreDemo/Kiosk/rules.json | 23 +++ .../{sync => StoreDemo}/Kiosk/schema.json | 14 +- .../Product/relationships.json | 0 .../StoreDemo/Product/rules.json | 23 +++ .../{sync => StoreDemo}/Product/schema.json | 20 +-- .../Store/relationships.json | 2 +- .../mongodb-atlas/StoreDemo/Store/rules.json | 23 +++ .../{sync => StoreDemo}/Store/schema.json | 12 +- .../mongodb-atlas/StoreDemo/Users/rules.json | 21 +++ .../mongodb-atlas/default_rule.json | 31 +--- .../mongodb-atlas/sync/Kiosk/rules.json | 19 --- .../mongodb-atlas/sync/Product/rules.json | 19 --- .../mongodb-atlas/sync/Store/rules.json | 19 --- .../backend/functions/config.json | 19 ++- .../backend/functions/createNewStore.js | 12 ++ .../backend/functions/getAllStores.js | 10 ++ .../backend/functions/onUserCreation.js | 19 --- .../functions/setUserDefaultStoreId.js | 39 +++++ .../backend/functions/switchStore.js | 43 +++++ .../backend/sync/config.json | 8 +- .../backend/triggers/onUserCreation.json | 18 +++ .../frontend/app/App.tsx | 45 +++--- .../frontend/app/atlas-app-services/config.ts | 3 - .../app/components/AuthResultBoundary.tsx | 4 +- .../frontend/app/hooks/useDemoSyncTriggers.ts | 43 ++++- .../frontend/app/providers/StoreProvider.tsx | 18 ++- .../frontend/app/screens/StoreScreen.tsx | 153 +++++++++--------- .../frontend/ios/Podfile.lock | 4 +- .../frontend/package-lock.json | 30 ++-- .../frontend/package.json | 4 +- 35 files changed, 475 insertions(+), 302 deletions(-) delete mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/AuthExample/Users/rules.json rename examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/{sync => StoreDemo}/Kiosk/relationships.json (64%) create mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/rules.json rename examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/{sync => StoreDemo}/Kiosk/schema.json (86%) rename examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/{sync => StoreDemo}/Product/relationships.json (100%) create mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/rules.json rename examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/{sync => StoreDemo}/Product/schema.json (81%) rename examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/{sync => StoreDemo}/Store/relationships.json (64%) create mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/rules.json rename examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/{sync => StoreDemo}/Store/schema.json (83%) create mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Users/rules.json delete mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json delete mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json delete mode 100644 examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json create mode 100644 examples/rn-connection-and-error/backend/functions/createNewStore.js create mode 100644 examples/rn-connection-and-error/backend/functions/getAllStores.js delete mode 100644 examples/rn-connection-and-error/backend/functions/onUserCreation.js create mode 100644 examples/rn-connection-and-error/backend/functions/setUserDefaultStoreId.js create mode 100644 examples/rn-connection-and-error/backend/functions/switchStore.js create mode 100644 examples/rn-connection-and-error/backend/triggers/onUserCreation.json diff --git a/examples/rn-connection-and-error/README.md b/examples/rn-connection-and-error/README.md index dc73283497b..1a36e985593 100644 --- a/examples/rn-connection-and-error/README.md +++ b/examples/rn-connection-and-error/README.md @@ -88,6 +88,7 @@ It specifically addresses the following points: * Initial subscriptions are added to allow syncing of a subset of data to the device (i.e. the kiosks and products belonging to a specific store). * The Realm is opened immediately without waiting for downloads from the server. * See [Offline Support](#note-offline-support) below. +* Tying custom user data to document permissions. ### Note: Offline Support @@ -142,8 +143,6 @@ The server will [reset the client](https://www.mongodb.com/docs/atlas/app-servic In this demo app, a client reset is triggered by calling a [custom Atlas Function](#add-an-atlas-function) that deletes the client files for the current user. Another way to simulate a client reset is to terminate and re-enable Device Sync. -> ⚠️ At the time of writing (Realm JS version 12.2.0), pre and post client reset listeners are not fired as expected. Instead, the sync error callback is invoked with an error named `ClientReset`. This will be fixed as soon as possible. - ### Logging and App Activity Monitoring App Services logs all incoming requests and application events such as Device Sync operations and user authentication. In this demo app, we log messages to the `console` when certain changes and activities are detected, but you can replace the logger used with your preferred logging mechanism or service. @@ -152,6 +151,13 @@ To modify the [log level and logger](https://www.mongodb.com/docs/realm/sdk/reac For the App Services logs, you can also choose to [forward the logs to a service](https://www.mongodb.com/docs/atlas/app-services/activity/forward-logs/). To read more about monitoring app activity, please see the [docs](https://www.mongodb.com/docs/atlas/app-services/activity/). +### Tying Custom User Data to Schema Rules +When a new user is created, they are automatically attached to the first store in the collection (or a new one if it doesn't exist). The schemas for Store, Kiosk and Products have rules set to only be accessible if the users `storeId` field matches with the appropriate field in each model. There is a function that can be triggered to switch the store for the current user. Updating the custom user data and refreshing the session will create a client reset, which will automatically update the UI with the newly selected Store. + +To perform this in the application, after creating a new user and viewing the store page, one can press `Trigger Store Change`, which will call the `switchStore` function. At this point the UI will not be updated, as a refresh of both the session and the current users custom data must be performed. To do this, press `Refresh Access Token/User Data`. The refresh of the session will update the rules in the backend and the refresh of the custom user data will update the UI with the correct store Id. + +If the refresh session does not happen automatically, one can either press `Refresh Session` or click `Disconnect` followed by `Reconnect`. If this is done without refreshing the user data, no store will be shown as the rules will be updated to the new store, but the UI will not be updated with the new store Id. + ## Getting Started ### Prerequisites @@ -204,19 +210,46 @@ To sync data used in this app you must first: > If you set up your App Services App [via a CLI](#via-a-cli-recommended), you can **skip this step** as these functions should already be defined for you. -We will add a function for forcing a client reset. The function is solely used for demo purposes and should not be used in production. We will also add a function to be run on user creation that adds fields to the user's [custom user data](https://www.mongodb.com/docs/atlas/app-services/users/custom-metadata/) document. +We will add functions for the following: + * Forcing a client reset. + * This function is solely used for demo purposes and should not be used in production. + * Setting the default `storeId` for a user on creation on the associated [custom user data](https://www.mongodb.com/docs/atlas/app-services/users/custom-metadata/) document. + * An [authentication trigger](https://www.mongodb.com/docs/atlas/app-services/triggers/authentication-triggers/) will need to be created and configured to call this function. + * Switching the associated Store for the current user. + * Creating a new store document and getting all store documents. + * These are used to check if the demo stores already exist and create them if not. + * The functions need to be system calls, as the associated user will not have permissions to read or write any other store. To set this up via the App Services UI: -1. [Define two functions](https://www.mongodb.com/docs/atlas/app-services/functions/#define-a-function) with the following configurations: +1. [Define five functions](https://www.mongodb.com/docs/atlas/app-services/functions/#define-a-function) with the following configurations: * Function name: `triggerClientReset` * Authentication: `System` * Private: `false` * Code: See [backend function](./backend/functions/triggerClientReset.js) - * Function name: `onUserCreation` + * Function name: `setUserDefaultStoreId` * Authentication: `Application Authentication` * Private: `false` - * Code: See [backend function](./backend/functions/onUserCreation.js) + * Code: See [backend function](./backend/functions/setUserDefaultStoreId.js) + * Function name: `switchStore` + * Authentication: `Application Authentication` + * Private: `false` + * Code: See [backend function](./backend/functions/switchStore.js) + * Function name: `createNewStore` + * Authentication: `System` + * Private: `true` + * Code: See [backend function](./backend/functions/createNewStore.js) + * Function name: `getAllStores` + * Authentication: `Application Authentication` + * Private: `true` + * Code: See [backend function](./backend/functions/getAllStores.js) +2. [Define an authentication trigger](https://www.mongodb.com/docs/atlas/app-services/triggers/authentication-triggers/#create-an-authentication-trigger) + * Trigger type: `Authentication` + * Trigger name: `onUserCreation` + * Action type: `Create` + * Providers: `Email/Password` + * EventType: `Function` + * Function: `setUserDefaultStoreId` ### Install Dependencies diff --git a/examples/rn-connection-and-error/backend/auth/custom_user_data.json b/examples/rn-connection-and-error/backend/auth/custom_user_data.json index ed81514166e..e21c2807330 100644 --- a/examples/rn-connection-and-error/backend/auth/custom_user_data.json +++ b/examples/rn-connection-and-error/backend/auth/custom_user_data.json @@ -1,8 +1,8 @@ { "enabled": true, "mongo_service_name": "mongodb-atlas", - "database_name": "AuthExample", + "database_name": "StoreDemo", "collection_name": "Users", - "user_id_field": "user_id", - "on_user_creation_function_name": "onUserCreation" + "user_id_field": "userId", + "on_user_creation_function_name": "setUserDefaultStoreId" } diff --git a/examples/rn-connection-and-error/backend/auth/providers.json b/examples/rn-connection-and-error/backend/auth/providers.json index 2472aae8f56..12017c94b8a 100644 --- a/examples/rn-connection-and-error/backend/auth/providers.json +++ b/examples/rn-connection-and-error/backend/auth/providers.json @@ -12,12 +12,12 @@ "local-userpass": { "name": "local-userpass", "type": "local-userpass", + "disabled": false, "config": { "autoConfirm": true, "resetPasswordUrl": "https://", "runConfirmationFunction": false, "runResetFunction": false - }, - "disabled": false + } } } diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/AuthExample/Users/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/AuthExample/Users/rules.json deleted file mode 100644 index 0176ecaac03..00000000000 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/AuthExample/Users/rules.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "collection": "Users", - "database": "AuthExample", - "roles": [ - { - "name": "ThisUser", - "apply_when": { - "user_id": "%%user.id%%" - }, - "document_filters": { - "write": {}, - "read": {} - }, - "read": true, - "write": true, - "insert": false, - "delete": false, - "search": false - } - ], - "filters": [] -} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/relationships.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/relationships.json similarity index 64% rename from examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/relationships.json rename to examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/relationships.json index c13e486f935..818b425af73 100644 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/relationships.json +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/relationships.json @@ -1,6 +1,6 @@ { "products": { - "ref": "#/relationship/mongodb-atlas/sync/Product", + "ref": "#/relationship/mongodb-atlas/StoreDemo/Product", "source_key": "products", "foreign_key": "_id", "is_list": true diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/rules.json new file mode 100644 index 00000000000..71c41e4d3cc --- /dev/null +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/rules.json @@ -0,0 +1,23 @@ +{ + "database": "StoreDemo", + "collection": "Kiosk", + "roles": [ + { + "name": "readAndWriteAll", + "apply_when": {}, + "document_filters": { + "read": { + "storeId": "%%user.custom_data.storeId" + }, + "write": { + "storeId": "%%user.custom_data.storeId" + } + }, + "insert": true, + "delete": true, + "search": true, + "read": true, + "write": true + } + ] +} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/schema.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/schema.json similarity index 86% rename from examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/schema.json rename to examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/schema.json index 499d3a1a0e4..501a8b4c297 100644 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/schema.json +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Kiosk/schema.json @@ -1,5 +1,10 @@ { - "bsonType": "object", + "title": "Kiosk", + "type": "object", + "required": [ + "_id", + "storeId" + ], "properties": { "_id": { "bsonType": "objectId" @@ -13,10 +18,5 @@ "storeId": { "bsonType": "objectId" } - }, - "required": [ - "_id", - "storeId" - ], - "title": "Kiosk" + } } diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/relationships.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/relationships.json similarity index 100% rename from examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/relationships.json rename to examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/relationships.json diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/rules.json new file mode 100644 index 00000000000..dcbfbbe4469 --- /dev/null +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/rules.json @@ -0,0 +1,23 @@ +{ + "database": "StoreDemo", + "collection": "Product", + "roles": [ + { + "name": "readAndWriteAll", + "apply_when": {}, + "document_filters": { + "read": { + "storeId": "%%user.custom_data.storeId" + }, + "write": { + "storeId": "%%user.custom_data.storeId" + } + }, + "insert": true, + "delete": true, + "search": true, + "read": true, + "write": true + } + ] +} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/schema.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/schema.json similarity index 81% rename from examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/schema.json rename to examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/schema.json index b8380b16219..9988ae089ad 100644 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/schema.json +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Product/schema.json @@ -1,5 +1,13 @@ { - "bsonType": "object", + "title": "Product", + "type": "object", + "required": [ + "_id", + "name", + "numInStock", + "price", + "storeId" + ], "properties": { "_id": { "bsonType": "objectId" @@ -16,13 +24,5 @@ "storeId": { "bsonType": "objectId" } - }, - "required": [ - "_id", - "storeId", - "name", - "price", - "numInStock" - ], - "title": "Product" + } } diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/relationships.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/relationships.json similarity index 64% rename from examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/relationships.json rename to examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/relationships.json index 32ce8a1afea..5d9bde10a9b 100644 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/relationships.json +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/relationships.json @@ -1,6 +1,6 @@ { "kiosks": { - "ref": "#/relationship/mongodb-atlas/sync/Kiosk", + "ref": "#/relationship/mongodb-atlas/StoreDemo/Kiosk", "source_key": "kiosks", "foreign_key": "_id", "is_list": true diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/rules.json new file mode 100644 index 00000000000..5c8c3aaae68 --- /dev/null +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/rules.json @@ -0,0 +1,23 @@ +{ + "database": "StoreDemo", + "collection": "Store", + "roles": [ + { + "name": "readAndWriteAll", + "apply_when": {}, + "document_filters": { + "read": { + "_id": "%%user.custom_data.storeId" + }, + "write": { + "_id": "%%user.custom_data.storeId" + } + }, + "insert": true, + "delete": true, + "search": true, + "read": true, + "write": true + } + ] +} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/schema.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/schema.json similarity index 83% rename from examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/schema.json rename to examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/schema.json index 0e8eaba3cb6..296d36492e1 100644 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/schema.json +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Store/schema.json @@ -1,5 +1,9 @@ { - "bsonType": "object", + "title": "Store", + "type": "object", + "required": [ + "_id" + ], "properties": { "_id": { "bsonType": "objectId" @@ -10,9 +14,5 @@ "bsonType": "objectId" } } - }, - "required": [ - "_id" - ], - "title": "Store" + } } diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Users/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Users/rules.json new file mode 100644 index 00000000000..77268e56679 --- /dev/null +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/StoreDemo/Users/rules.json @@ -0,0 +1,21 @@ +{ + "database": "StoreDemo", + "collection": "Users", + "roles": [ + { + "name": "ThisUser", + "apply_when": { + "userId": "%%user.id" + }, + "document_filters": { + "read": true, + "write": true + }, + "insert": true, + "delete": true, + "search": true, + "read": true, + "write": true + } + ] +} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json index ea0a3112bac..0967ef424bc 100644 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json +++ b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json @@ -1,30 +1 @@ -{ - "roles": [ - { - "name": "readAndWriteAll", - "apply_when": {}, - "document_filters": { - "write": true, - "read": true - }, - "read": true, - "write": true, - "insert": true, - "delete": true, - "search": true - }, - { - "name": "readAll", - "apply_when": {}, - "document_filters": { - "write": false, - "read": true - }, - "read": true, - "write": false, - "insert": false, - "delete": false, - "search": true - } - ] -} +{} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json deleted file mode 100644 index ca8d2610daa..00000000000 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "collection": "Kiosk", - "database": "sync", - "roles": [ - { - "name": "readAndWriteAll", - "apply_when": {}, - "document_filters": { - "write": true, - "read": true - }, - "read": true, - "write": true, - "insert": true, - "delete": true, - "search": true - } - ] -} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json deleted file mode 100644 index e5a9cce81e7..00000000000 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "collection": "Product", - "database": "sync", - "roles": [ - { - "name": "readAndWriteAll", - "apply_when": {}, - "document_filters": { - "write": true, - "read": true - }, - "read": true, - "write": true, - "insert": true, - "delete": true, - "search": true - } - ] -} diff --git a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json b/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json deleted file mode 100644 index 55f8ff97634..00000000000 --- a/examples/rn-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "collection": "Store", - "database": "sync", - "roles": [ - { - "name": "readAndWriteAll", - "apply_when": {}, - "document_filters": { - "write": true, - "read": true - }, - "read": true, - "write": true, - "insert": true, - "delete": true, - "search": true - } - ] -} diff --git a/examples/rn-connection-and-error/backend/functions/config.json b/examples/rn-connection-and-error/backend/functions/config.json index 526d5d60f2c..283b7fcdb60 100644 --- a/examples/rn-connection-and-error/backend/functions/config.json +++ b/examples/rn-connection-and-error/backend/functions/config.json @@ -6,8 +6,25 @@ "disable_arg_logs": true }, { - "name": "onUserCreation", + "name": "setUserDefaultStoreId", "private": false, "disable_arg_logs": true + }, + { + "name": "switchStore", + "private": false, + "disable_arg_logs": true + }, + { + "name": "createNewStore", + "private": false, + "run_as_system": true, + "disable_arg_logs": true + }, + { + "name": "getAllStores", + "private": true, + "run_as_system": true, + "disable_arg_logs": true } ] diff --git a/examples/rn-connection-and-error/backend/functions/createNewStore.js b/examples/rn-connection-and-error/backend/functions/createNewStore.js new file mode 100644 index 00000000000..31cbeefb341 --- /dev/null +++ b/examples/rn-connection-and-error/backend/functions/createNewStore.js @@ -0,0 +1,12 @@ +/* eslint-disable */ + +/** + * Creates a new store and returns its associated document. + */ +exports = async function(){ + const storeCollection = context.services.get("mongodb-atlas").db("StoreDemo").collection("Store"); + + const result = await storeCollection.insertOne({kiosks:[]}); + const store = await storeCollection.findOne({_id: result.insertedId}); + return store; +}; diff --git a/examples/rn-connection-and-error/backend/functions/getAllStores.js b/examples/rn-connection-and-error/backend/functions/getAllStores.js new file mode 100644 index 00000000000..2f94c320bec --- /dev/null +++ b/examples/rn-connection-and-error/backend/functions/getAllStores.js @@ -0,0 +1,10 @@ +/* eslint-disable */ + +/** + * Returns an array of all store documents. + */ +exports = async function(){ + const storeCollection = context.services.get("mongodb-atlas").db("StoreDemo").collection("Store"); + + return await storeCollection.find().toArray(); +}; diff --git a/examples/rn-connection-and-error/backend/functions/onUserCreation.js b/examples/rn-connection-and-error/backend/functions/onUserCreation.js deleted file mode 100644 index dc8b3633351..00000000000 --- a/examples/rn-connection-and-error/backend/functions/onUserCreation.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable */ - -/** - * Adds custom user data to the Users collection when a new user is created. - */ -exports = async function onUserCreation(user) { - const customUserDataCollection = context.services.get("mongodb-atlas").db("AuthExample").collection("Users"); - try { - await customUserDataCollection.insertOne({ - // Save the user's account ID to your configured user_id field. - user_id: user.id, - // Store any other user data you want. - team: "service", - }); - } catch (e) { - console.error(`Failed to create custom user data document for user:${user.id}`); - throw e; - } -}; diff --git a/examples/rn-connection-and-error/backend/functions/setUserDefaultStoreId.js b/examples/rn-connection-and-error/backend/functions/setUserDefaultStoreId.js new file mode 100644 index 00000000000..d987b2309d8 --- /dev/null +++ b/examples/rn-connection-and-error/backend/functions/setUserDefaultStoreId.js @@ -0,0 +1,39 @@ +/* eslint-disable */ + +/** + * Upserts custom user data to the Users collection when a user is authorized. + * Will create a default store if one does not already exist. + */ +exports = async function setUserDefaultStoreId(user) { + const customUserDataCollection = context.services.get("mongodb-atlas").db("StoreDemo").collection("Users"); + const storesCollection = context.services.get("mongodb-atlas").db("StoreDemo").collection("Store"); + + try { + const defaultStore = await getDefaultStore(storesCollection); + + await customUserDataCollection.updateOne( + { userId: user.id}, + {$set: { + // Save the user's account ID to your configured user_id field. + userId: user.id, + // Group Id + storeId: defaultStore._id + }}, + {upsert: true} + ); + } catch (e) { + console.error(`Failed to create custom user data document for user:${user.id}`); + throw e; + } +}; + +async function getDefaultStore(storesCollection){ + // attempt to get the default store if it exists and create it if it doesn't + const store = await storesCollection.findOne({}); + + if (!store) { + const store = await context.functions.execute('createNewStore') + return store; + } + return store; +} diff --git a/examples/rn-connection-and-error/backend/functions/switchStore.js b/examples/rn-connection-and-error/backend/functions/switchStore.js new file mode 100644 index 00000000000..cbf3f00d2d5 --- /dev/null +++ b/examples/rn-connection-and-error/backend/functions/switchStore.js @@ -0,0 +1,43 @@ +/* eslint-disable */ + +/** + * Toggle between two stores. This is run as the system user, since it will need to create + * a second store if one does not exist. The userId is provided as a string. + */ +exports = async function switchStore() { + const userId = context.user.id; + const customUserDataCollection = context.services.get("mongodb-atlas").db("StoreDemo").collection("Users"); + const storeCollection = context.services.get("mongodb-atlas").db("StoreDemo").collection("Store"); + + try { + + const userDoc = await customUserDataCollection.findOne({userId: userId}); + const userStore = userDoc.storeId; + const store = await getOrCreateStore(storeCollection, userStore); + await customUserDataCollection.updateOne({ + userId, + }, + {$set: { + storeId:store._id, + }}); + } catch (e) { + console.error(`Failed to create custom user data document for user:${userId}`); + throw e; + } +}; + +// Returns a different store than the current `userStore`. +// This will help toggle between stores. It will create a second store +// if one does not already exist. +async function getOrCreateStore(storeCollection, userStore){ + const stores = await context.functions.execute('getAllStores') + if(userStore.toString() === stores[0]._id.toString()){ + if(!stores[1]){ + const store = await context.functions.execute('createNewStore') + return store; + } + return stores[1]; + } + + return stores[0]; +} diff --git a/examples/rn-connection-and-error/backend/sync/config.json b/examples/rn-connection-and-error/backend/sync/config.json index c8bbb1a2854..a3da6ffaa32 100644 --- a/examples/rn-connection-and-error/backend/sync/config.json +++ b/examples/rn-connection-and-error/backend/sync/config.json @@ -1,15 +1,11 @@ { "type": "flexible", "state": "enabled", - "development_mode_enabled": false, + "development_mode_enabled": true, "service_name": "mongodb-atlas", - "database_name": "sync", + "database_name": "StoreDemo", "client_max_offline_days": 30, "is_recovery_mode_disabled": false, - "permissions": { - "rules": {}, - "defaultRoles": [] - }, "queryable_fields_names": [ "storeId" ] diff --git a/examples/rn-connection-and-error/backend/triggers/onUserCreation.json b/examples/rn-connection-and-error/backend/triggers/onUserCreation.json new file mode 100644 index 00000000000..18b2d07154e --- /dev/null +++ b/examples/rn-connection-and-error/backend/triggers/onUserCreation.json @@ -0,0 +1,18 @@ +{ + "name": "onUserCreation", + "type": "AUTHENTICATION", + "disabled": false, + "config": { + "operation_type": "CREATE", + "providers": [ + "local-userpass" + ] + }, + "event_processors": { + "FUNCTION": { + "config": { + "function_name": "setUserDefaultStoreId" + } + } + } +} diff --git a/examples/rn-connection-and-error/frontend/app/App.tsx b/examples/rn-connection-and-error/frontend/app/App.tsx index 4be6c98e9af..788bd0b4735 100644 --- a/examples/rn-connection-and-error/frontend/app/App.tsx +++ b/examples/rn-connection-and-error/frontend/app/App.tsx @@ -21,7 +21,7 @@ import {SafeAreaView, StatusBar, StyleSheet} from 'react-native'; import Realm, {ClientResetMode, OpenRealmBehaviorType, SyncError} from 'realm'; import {AppProvider, RealmProvider, UserProvider} from '@realm/react'; -import {ATLAS_APP_ID, SYNC_STORE_ID} from './atlas-app-services/config'; +import {ATLAS_APP_ID} from './atlas-app-services/config'; import {AuthResultBoundary} from './components/AuthResultBoundary'; import {Kiosk} from './models/Kiosk'; import {Loading} from './components/Loading'; @@ -105,41 +105,36 @@ function App() { not been authenticated. In this case, we will show the login screen. */} {/* Define the Realm configuration as props passed to `RealmProvider`. - Note that `user` does not need to be defined in the `sync` config - since the `RealmProvider` will set it for you once authenticated. */} + Note that `user` does not need to be defined in the `sync` config + since the `RealmProvider` will set it for you once authenticated. */} { - // Subscribe to the store with the given ID. - mutableSubs.add( - realm.objects(Store).filtered('_id = $0', SYNC_STORE_ID), - {name: 'storeA'}, - ); - // Subscribe to all kiosks in the store with the given ID. - mutableSubs.add( - realm - .objects(Kiosk) - .filtered('storeId = $0', SYNC_STORE_ID), - {name: 'kiosksInStoreA'}, - ); - // Subscribe to all products in the store with the given ID. - mutableSubs.add( - realm - .objects(Product) - .filtered('storeId = $0', SYNC_STORE_ID), - {name: 'productsInStoreA'}, - ); + // Subscribe to the all stores (access rules will determine what is sent down to the device). + mutableSubs.add(realm.objects(Store), {name: 'storeA'}); + // Subscribe to all kiosks. + mutableSubs.add(realm.objects(Kiosk), { + name: 'kiosksInStoreA', + }); + // Subscribe to all products. + mutableSubs.add(realm.objects(Product), { + name: 'productsInStoreA', + }); }, + rerunOnOpen: true, }, // The `ClientResetMode.RecoverOrDiscardUnsyncedChanges` will download a fresh copy // from the server if recovery of unsynced changes is not possible. For read-only diff --git a/examples/rn-connection-and-error/frontend/app/atlas-app-services/config.ts b/examples/rn-connection-and-error/frontend/app/atlas-app-services/config.ts index 3eca3af8b97..d4f935476a9 100644 --- a/examples/rn-connection-and-error/frontend/app/atlas-app-services/config.ts +++ b/examples/rn-connection-and-error/frontend/app/atlas-app-services/config.ts @@ -16,7 +16,4 @@ // //////////////////////////////////////////////////////////////////////////// -import {BSON} from 'realm'; - export const ATLAS_APP_ID = 'YOUR_APP_ID'; -export const SYNC_STORE_ID = new BSON.ObjectId('6426106cb0ad9713140883ed'); diff --git a/examples/rn-connection-and-error/frontend/app/components/AuthResultBoundary.tsx b/examples/rn-connection-and-error/frontend/app/components/AuthResultBoundary.tsx index b5f2d158320..ed5b217054e 100644 --- a/examples/rn-connection-and-error/frontend/app/components/AuthResultBoundary.tsx +++ b/examples/rn-connection-and-error/frontend/app/components/AuthResultBoundary.tsx @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import {useEffect} from 'react'; +import React, {useEffect} from 'react'; import type {PropsWithChildren} from 'react'; import {useAuth} from '@realm/react'; @@ -45,5 +45,5 @@ export function AuthResultBoundary({children}: PropsWithChildren) { } }, [result.error, result.operation, result.success]); - return children; + return <>{children}; } diff --git a/examples/rn-connection-and-error/frontend/app/hooks/useDemoSyncTriggers.ts b/examples/rn-connection-and-error/frontend/app/hooks/useDemoSyncTriggers.ts index 91fbb088592..b8e689826c0 100644 --- a/examples/rn-connection-and-error/frontend/app/hooks/useDemoSyncTriggers.ts +++ b/examples/rn-connection-and-error/frontend/app/hooks/useDemoSyncTriggers.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import {useCallback, useEffect, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {BSON, ConnectionState, UserState} from 'realm'; import {useApp, useRealm, useUser} from '@realm/react'; @@ -42,6 +42,11 @@ export function useDemoSyncTriggers() { const currentUser = useUser(); const [isConnected, setIsConnected] = useState(true); + // This ref is used by the refresh method, which contains a promise that + // would otherwise view a closure of stale state from isConnected and not know + // when to call `resume()`. + const isConnectedRef = useRef(isConnected); + /** * Trigger the connection listener by reconnecting to the sync session. */ @@ -97,6 +102,7 @@ export function useDemoSyncTriggers() { } setIsConnected(connected); + isConnectedRef.current = connected; }; realm.syncSession?.addConnectionNotification(handleConnectionChange); @@ -148,6 +154,40 @@ export function useDemoSyncTriggers() { await currentUser.refreshCustomData(); }, [currentUser]); + /** + * Pause and resume the session in order to apply new rules to schemas. + */ + const refreshSession = useCallback(async () => { + logger.info('Pausing Session...'); + realm.syncSession?.pause(); + + // Before calling resume, we should ensure we have disconnected. + // Poll the isConnectedRef until it's false. + try { + await new Promise((resolve, reject) => { + let attempts = 0; + const checkStatus = () => { + attempts++; + if (!isConnectedRef.current) { + clearInterval(intervalId); + resolve(); + } + if (attempts > 100) { + clearInterval(intervalId); + reject(); + } + }; + const intervalId = setInterval(checkStatus, 100); + }); + } catch { + logger.error('Pausing session failed'); + return; + } + + logger.info('Resuming Session...'); + realm.syncSession?.resume(); + }, [realm]); + /** * Trigger the user event listener by removing the user from the app. */ @@ -209,6 +249,7 @@ export function useDemoSyncTriggers() { triggerSyncError, triggerClientReset, refreshAccessToken, + refreshSession, deleteUser, }; } diff --git a/examples/rn-connection-and-error/frontend/app/providers/StoreProvider.tsx b/examples/rn-connection-and-error/frontend/app/providers/StoreProvider.tsx index c784a95c752..b06c947cbb0 100644 --- a/examples/rn-connection-and-error/frontend/app/providers/StoreProvider.tsx +++ b/examples/rn-connection-and-error/frontend/app/providers/StoreProvider.tsx @@ -19,9 +19,8 @@ import React, {createContext, useCallback, useContext, useEffect} from 'react'; import type {PropsWithChildren} from 'react'; import {BSON, CollectionChangeCallback} from 'realm'; -import {useQuery, useRealm} from '@realm/react'; +import {useObject, useQuery, useRealm, useUser} from '@realm/react'; -import {SYNC_STORE_ID} from '../atlas-app-services/config'; import {Kiosk} from '../models/Kiosk'; import {Product, getRandomProductName} from '../models/Product'; import {Store} from '../models/Store'; @@ -59,9 +58,12 @@ const StoreContext = createContext({ */ export function StoreProvider({children}: PropsWithChildren) { const realm = useRealm(); - const store = useQuery(Store)[0]; + const user = useUser<{}, {storeId: BSON.ObjectId}, {}>(); + const store = useObject(Store, user.customData.storeId); const products = useQuery(Product); + logger.info(`Users current store Id: ${user.customData.storeId}`); + /** * Adds a store. This demo app is syncing and using only 1 store with a * specific store ID (see `app/atlas-app-services/config.ts`). Thus, if this @@ -72,7 +74,7 @@ export function StoreProvider({children}: PropsWithChildren) { return; } realm.write(() => { - realm.create(Store, {_id: SYNC_STORE_ID}); + realm.create(Store, {}); }); }, [realm, store]); @@ -83,12 +85,12 @@ export function StoreProvider({children}: PropsWithChildren) { realm.write(() => { const kiosk = realm.create(Kiosk, { _id: new BSON.ObjectId(), - storeId: SYNC_STORE_ID, + storeId: user.customData.storeId, products: [...products], }); store?.kiosks.push(kiosk); }); - }, [realm, store, products]); + }, [realm, store, products, user]); /** * Adds a product and then adds it to all kiosks in the store. @@ -97,7 +99,7 @@ export function StoreProvider({children}: PropsWithChildren) { realm.write(() => { const product = realm.create(Product, { _id: new BSON.ObjectId(), - storeId: SYNC_STORE_ID, + storeId: user.customData.storeId, name: getRandomProductName(), price: parseFloat(getFloatBetween(3, 15).toFixed(2)), numInStock: getIntBetween(0, 100), @@ -106,7 +108,7 @@ export function StoreProvider({children}: PropsWithChildren) { kiosk.products.push(product); } }); - }, [realm, store]); + }, [realm, store, user]); /** * Updates a product by changing the number in stock. diff --git a/examples/rn-connection-and-error/frontend/app/screens/StoreScreen.tsx b/examples/rn-connection-and-error/frontend/app/screens/StoreScreen.tsx index 5bc4b0feb72..e43db248353 100644 --- a/examples/rn-connection-and-error/frontend/app/screens/StoreScreen.tsx +++ b/examples/rn-connection-and-error/frontend/app/screens/StoreScreen.tsx @@ -26,17 +26,11 @@ import {colors} from '../styles/colors'; import {fonts} from '../styles/fonts'; import {useDemoSyncTriggers} from '../hooks/useDemoSyncTriggers'; import {useStore} from '../providers/StoreProvider'; +import {AppServicesFunction} from 'realm'; +import {Loading} from '../components/Loading'; -/** - * The properties used as custom user data. - * - * @note - * Our backend function `onUserCreation()` adds these fields - * when the user registers. - */ -type CustomUserData = { - user_id: string; - team: string; +type UserFunctions = { + switchStore: AppServicesFunction; }; /** @@ -44,7 +38,7 @@ type CustomUserData = { * as well as buttons for triggering various listeners. */ export function StoreScreen() { - const {store, addStore, addKiosk, addProduct, updateProduct, removeProduct} = + const {store, addKiosk, addProduct, updateProduct, removeProduct} = useStore(); const { isConnected, @@ -54,9 +48,10 @@ export function StoreScreen() { triggerClientReset, refreshAccessToken, deleteUser, + refreshSession, } = useDemoSyncTriggers(); const {logOut} = useAuth(); - const user = useUser<{}, CustomUserData, {}>(); + const user = useUser(); return ( @@ -67,8 +62,8 @@ export function StoreScreen() {