diff --git a/examples/electron-todo-list/README.md b/examples/electron-todo-list/README.md index 9f0e21ffe8..93f766535f 100644 --- a/examples/electron-todo-list/README.md +++ b/examples/electron-todo-list/README.md @@ -1,50 +1,101 @@ -# Example Electron Todo App +# An Offline-First Todo List App Using Atlas Device SDK for Electron -This example Electron Todo/Task app shows how to use [Device Sync](https://www.mongodb.com/atlas/app-services/device-sync) and [@realm/react](https://www.npmjs.com/package/@realm/react). +A todo list (task manager) app showcasing how to create, read, update, and delete data while offline using [MongoDB's Atlas Device SDK for Electron](https://www.mongodb.com/docs/realm/sdk/node/integrations/electron-cra/) (fka Realm). + +> **TIP:** This app can be run together with the corresponding [React Native example app](../rn-todo-list/) using the same [App Services App](./backend/). ## Screenshots -![Tasks Page](./src/renderer/assets/screenshot-realm-web-sync-tasks.png) +![Tasks Page](./frontend/src/renderer/assets/screenshot-electron-tasks.png) ## Project Structure The following shows the project structure and the most relevant files. +> To learn more about the backend file structure, see [App Configuration](https://www.mongodb.com/docs/atlas/app-services/reference/config/). + ``` -├── src -│ ├── main - The main process -│ │ └── index.js -│ └── render - The rendering process -│ ├── atlas-app-services -│ │ └── config.json - Set Atlas App ID -│ │ -│ ├── components -│ │ ├── AddTaskForm.tsx - Trigger create task -│ │ ├── NavBar.tsx - Trigger logout -│ │ ├── TaskItem.tsx - Trigger update/delete task -│ │ └── TaskList.tsx - Render all tasks -│ │ -│ ├── hooks -│ │ └── useTaskManager.ts - Handle CRUD task -│ │ -│ ├── models -│ │ └── Task.ts - Data model -│ │ -│ ├── pages -│ │ ├── LoginPage.tsx - Trigger login/register -│ │ └── TaskPage.tsx - Pass CRUD ops to children -│ │ -│ ├── App.tsx - Get and provide Atlas App -│ ├── AuthenticatedApp.tsx - Open and provide Realm & User -│ └── index.tsx - Entry point -├── public - The folder of static contents to the web app -│ └── electron.cjs - The bootstrap script for electron -└── craco.config.cjs - The bundler config for the rendering process +├── backend - App Services App +│ └── (see link above) +│ +├── frontend - Electron App +│ ├── public +│ │ ├── electron.cjs - Creates the browser window +│ │ └── index.html - File served to client +│ │ +│ ├── src +│ │ ├── main - The main process +│ │ │ └── index.js +│ │ │ +│ │ ├── render - The rendering process +│ │ │ ├── atlas-app-services +│ │ │ │ └── config.json - Set Atlas App ID +│ │ │ │ +│ │ │ ├── components +│ │ │ │ ├── AddTaskForm.tsx - Creates a task +│ │ │ │ ├── NavBar.tsx - Provides logout option +│ │ │ │ ├── TaskItem.tsx - Updates or deletes a task +│ │ │ │ └── TaskList.tsx - Displays all tasks +│ │ │ │ +│ │ │ ├── hooks +│ │ │ │ └── useTaskManager.ts - Functions for managing (CRUD) tasks +│ │ │ │ +│ │ │ ├── models +│ │ │ │ └── Task.ts - Data model +│ │ │ │ +│ │ │ ├── pages +│ │ │ │ ├── LoginPage.tsx - Login and registration +│ │ │ │ └── TaskPage.tsx - Task page with sync-related ops +│ │ │ │ +│ │ │ ├── App.tsx - Get and provide Atlas App +│ │ │ └── AuthenticatedApp.tsx - Open and provide Realm & User +│ │ │ +│ │ └── index.tsx - Entry point +│ │ +│ ├── craco.config.cjs - The bundler config for the rendering process +│ └── package.json - Dependencies +│ +└── README.md - Instructions and info ``` -### Realm Details -* RealmJS version: ^12.1.0 -* Device Sync type: [Flexible](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) +## Use Cases + +This app focuses on showing how to work with data no matter the network connection. + +It specifically addresses the following points: + +* Registering and logging in to an App Services App using [Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/). +* Accessing and updating data: + * Create + * Query/Read + * Sort and filter the query + * Update + * Delete +* Using offline/local-first reads and writes. + * Shows configuration for opening a synced Realm. + * Realms are opened immediately without waiting for downloads from the server. + * See [Offline Support](#note-offline-support) below. +* Allowing users to only read and write to their own tasks via [data access rules/permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions). + * See [Set Data Access Permissions](#set-data-access-permissions) further below. + +### Note: Offline Support + +Users who have logged in at least once will have their credentials cached on the client. Thus, a logged in user who restarts the app will remain logged in. [@realm/react's](https://www.npmjs.com/package/@realm/react) `UserProvider` automatically handles this for you by checking if the `app.currentUser` already exists. + +Data that was previously synced to the device will also exist locally in the Realm database. From this point on, users can be offline and still query and update data. Any changes made offline will be synced automatically to Atlas and any other devices once a network connection is established. If multiple users modify the same data either while online or offline, those conflicts are [automatically resolved](https://www.mongodb.com/docs/atlas/app-services/sync/details/conflict-resolution/) before being synced. + +#### Realm Configuration + +When [opening a Realm](https://www.mongodb.com/docs/realm/sdk/react-native/sync-data/configure-a-synced-realm/), we can specify the behavior in the Realm configuration when opening it for the first time (via `newRealmFileBehavior`) and for subsequent ones (via `existingRealmFileBehavior`). We can either: +* `OpenRealmBehaviorType.OpenImmediately` + * Opens the Realm file immediately if it exists, otherwise it first creates a new empty Realm file then opens it. + * This lets users use the app with the existing data, while syncing any changes to the device in the background. +* `OpenRealmBehaviorType.DownloadBeforeOpen` + * If there is data to be downloaded, this waits for the data to be fully synced before opening the Realm. + +This app opens a Realm via `RealmProvider` (see [AuthenticatedApp.tsx](./frontend/src/renderer/AuthenticatedApp.tsx)) and passes the configuration as props. We use `OpenImmediately` for new and existing Realm files in order to use the app while offline. + +> See [OpenRealmBehaviorConfiguration](https://www.mongodb.com/docs/realm-sdks/js/latest/types/OpenRealmBehaviorConfiguration.html) for possible configurations of new and existing Realm file behaviors. ## Getting Started @@ -52,20 +103,51 @@ The following shows the project structure and the most relevant files. * [Node.js](https://nodejs.org/) -### Set Up an Atlas App Services App +### Set up an Atlas Database + +Start by [deploying a free Atlas cluster](https://www.mongodb.com/docs/atlas/getting-started/#get-started-with-atlas) and create an Atlas database. + +### Set up an Atlas App Services App + +You can either choose to set up your App via a CLI (this has fewer steps and is much faster since all configurations are already provided in the [backend directory](./backend/)), or via the App Services UI (steps provided below). + +#### Via a CLI (recommended) + +To import and deploy changes from your local directory to App Services you can use the command line interface: + +1. [Set up Realm CLI](https://www.mongodb.com/docs/atlas/app-services/cli/). +2. In the provided [backend directory](./backend/) (the App Services App), update the following: + * Cluster Name + * Update the `"clusterName"` in [data_sources/mongodb-atlas/config.json](./backend/data_sources/mongodb-atlas/config.json) to the name of your cluster. + * (The default name is `Cluster0`.) + * App ID + * There is no `"app_id"` defined in [realm_config.json](./backend/realm_config.json) since we will create a brand new App. **If** you for some reason are updating an existing app, add an `"app_id"` field and its value. +3. [Push and deploy](https://www.mongodb.com/docs/atlas/app-services/cli/realm-cli-push/#std-label-realm-cli-push) the local directory to App Services: +```sh +realm-cli push --local +``` +4. Once pushed, verify that your App shows up in the App Services UI. +5. 🥳 You can now go ahead and [install dependencies and run the Electron app](#install-dependencies). + +#### Via the App Services UI To sync data used in this app you must first: -1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/) -2. Enable [Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/#std-label-email-password-authentication) +1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/). +2. [Enable Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/). * For this example app, we automatically confirm users' emails. -3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **Development Mode** on. +3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **Development Mode** enabled. * When Development Mode is enabled, [queryable fields](https://www.mongodb.com/docs/atlas/app-services/sync/configure/sync-settings/#queryable-fields) will be added **automatically**, and schemas will be inferred based on the client Realm data models. - -After running the client and seeing the available collections in Atlas, [set read/write permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions) for all collections. + * For information, queryable fields used in this app include: + * Global (all collections): `_id` + * `Task` collection: `isComplete`, `userId` + * (Development Mode should be turned off in production.) +4. Don't forget to click `Review Draft and Deploy`. ### Install Dependencies +From the [frontend directory](./frontend/), run: + ```sh npm install ``` @@ -73,21 +155,51 @@ npm install ### Run the App 1. Copy your [Atlas App ID](https://www.mongodb.com/docs/atlas/app-services/reference/find-your-project-or-app-id/#std-label-find-your-app-id) from the App Services UI. -2. Paste the copied ID as the value of the existing variable `ATLAS_APP_ID` in [src/renderer/atlas-app-services/config.json](./src/renderer/atlas-app-services/config.ts): +2. Paste the copied ID as the value of the existing field `ATLAS_APP_ID` in [src/renderer/atlas-app-services/config.json](./frontend/src/renderer/atlas-app-services/config.json): ```js { "ATLAS_APP_ID": "YOUR_APP_ID" } ``` - -3. Build the application - +3. Build the application: ```sh npm run build ``` +4. Start Electron: +```sh +npm start +``` + +### Set Data Access Permissions + +> If you set up your App Services App [via a CLI](#via-a-cli-recommended), you can **skip this step** as the permissions should already be defined for you. -4. Start electron +After running the client app for the first time, [modify the rules](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions) for the collection in the App Services UI. + +* Collection: `Task` + * Permissions: `readOwnWriteOwn` (see [corresponding json](./backend/data_sources/mongodb-atlas/TodoList/Task/rules.json)) + * Explanation: + * A user should be able to read and write to their own document (i.e. when `Task.userId === `), but not anyone else's. + +> To learn more and see examples of permissions depending on a certain use case, see [Device Sync Permissions Guide](https://www.mongodb.com/docs/atlas/app-services/sync/app-builder/device-sync-permissions-guide/#std-label-flexible-sync-permissions-guide) and [Data Access Role Examples](https://www.mongodb.com/docs/atlas/app-services/rules/examples/). + +## Troubleshooting + +A great help when troubleshooting is to look at the [Application Logs](https://www.mongodb.com/docs/atlas/app-services/activity/view-logs/) in the App Services UI. + +### Permissions + +If permission is denied: + * Make sure your IP address is on the [IP Access List](https://www.mongodb.com/docs/atlas/app-services/security/network/#ip-access-list) for your App. + * Make sure you have the correct data access permissions for the collections. + * See [Set Data Access Permissions](#set-data-access-permissions) further above. + +### Removing the Local Realm Database + +Removing the local database can be useful for certain errors. When running the app, the local database will exist in the directory `mongodb-realm/`. + +From the [frontend directory](./frontend/), run: ```sh -npm start +npm run rm-local-db ``` diff --git a/examples/electron-todo-list/backend/README.md b/examples/electron-todo-list/backend/README.md new file mode 100644 index 0000000000..07508bebf5 --- /dev/null +++ b/examples/electron-todo-list/backend/README.md @@ -0,0 +1,7 @@ +# Backend + +This contains the Atlas App Services App and its configurations for the example app. + +Please see the [main README](../README.md) for all instructions. + +> To learn more about the backend file structure, see [App Configuration](https://www.mongodb.com/docs/atlas/app-services/reference/config/). diff --git a/examples/electron-todo-list/backend/auth/custom_user_data.json b/examples/electron-todo-list/backend/auth/custom_user_data.json new file mode 100644 index 0000000000..a82d0fb255 --- /dev/null +++ b/examples/electron-todo-list/backend/auth/custom_user_data.json @@ -0,0 +1,3 @@ +{ + "enabled": false +} diff --git a/examples/electron-todo-list/backend/auth/providers.json b/examples/electron-todo-list/backend/auth/providers.json new file mode 100644 index 0000000000..352f51d7d1 --- /dev/null +++ b/examples/electron-todo-list/backend/auth/providers.json @@ -0,0 +1,22 @@ +{ + "anon-user": { + "name": "anon-user", + "type": "anon-user", + "disabled": false + }, + "api-key": { + "name": "api-key", + "type": "api-key", + "disabled": true + }, + "local-userpass": { + "name": "local-userpass", + "type": "local-userpass", + "config": { + "autoConfirm": true, + "resetPasswordUrl": "https://", + "runConfirmationFunction": false + }, + "disabled": false + } +} diff --git a/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/relationships.json b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/relationships.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/relationships.json @@ -0,0 +1 @@ +{} diff --git a/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/rules.json b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/rules.json new file mode 100644 index 0000000000..6ca1b3345c --- /dev/null +++ b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/rules.json @@ -0,0 +1,23 @@ +{ + "collection": "Task", + "database": "TodoList", + "roles": [ + { + "name": "readOwnWriteOwn", + "apply_when": {}, + "document_filters": { + "write": { + "userId": "%%user.id" + }, + "read": { + "userId": "%%user.id" + } + }, + "read": true, + "write": true, + "insert": true, + "delete": true, + "search": true + } + ] +} diff --git a/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/schema.json b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/schema.json new file mode 100644 index 0000000000..08ef6db636 --- /dev/null +++ b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/TodoList/Task/schema.json @@ -0,0 +1,28 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "createdAt": { + "bsonType": "date" + }, + "description": { + "bsonType": "string" + }, + "isComplete": { + "bsonType": "bool" + }, + "userId": { + "bsonType": "string" + } + }, + "required": [ + "_id", + "createdAt", + "description", + "isComplete", + "userId" + ], + "title": "Task", + "type": "object" +} diff --git a/examples/electron-todo-list/backend/data_sources/mongodb-atlas/config.json b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/config.json new file mode 100644 index 0000000000..9913676dd9 --- /dev/null +++ b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/config.json @@ -0,0 +1,10 @@ +{ + "name": "mongodb-atlas", + "type": "mongodb-atlas", + "config": { + "clusterName": "Cluster0", + "readPreference": "primary", + "wireProtocolEnabled": false + }, + "version": 1 +} diff --git a/examples/electron-todo-list/backend/data_sources/mongodb-atlas/default_rule.json b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/default_rule.json new file mode 100644 index 0000000000..86c55c8767 --- /dev/null +++ b/examples/electron-todo-list/backend/data_sources/mongodb-atlas/default_rule.json @@ -0,0 +1,17 @@ +{ + "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/electron-todo-list/backend/environments/development.json b/examples/electron-todo-list/backend/environments/development.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/electron-todo-list/backend/environments/development.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/electron-todo-list/backend/environments/no-environment.json b/examples/electron-todo-list/backend/environments/no-environment.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/electron-todo-list/backend/environments/no-environment.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/electron-todo-list/backend/environments/production.json b/examples/electron-todo-list/backend/environments/production.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/electron-todo-list/backend/environments/production.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/electron-todo-list/backend/environments/qa.json b/examples/electron-todo-list/backend/environments/qa.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/electron-todo-list/backend/environments/qa.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/electron-todo-list/backend/environments/testing.json b/examples/electron-todo-list/backend/environments/testing.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/electron-todo-list/backend/environments/testing.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/electron-todo-list/backend/functions/config.json b/examples/electron-todo-list/backend/functions/config.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/examples/electron-todo-list/backend/functions/config.json @@ -0,0 +1 @@ +[] diff --git a/examples/electron-todo-list/backend/graphql/config.json b/examples/electron-todo-list/backend/graphql/config.json new file mode 100644 index 0000000000..406b1abcde --- /dev/null +++ b/examples/electron-todo-list/backend/graphql/config.json @@ -0,0 +1,4 @@ +{ + "use_natural_pluralization": true, + "disable_schema_introspection": false +} diff --git a/examples/electron-todo-list/backend/http_endpoints/config.json b/examples/electron-todo-list/backend/http_endpoints/config.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/examples/electron-todo-list/backend/http_endpoints/config.json @@ -0,0 +1 @@ +[] diff --git a/examples/electron-todo-list/backend/realm_config.json b/examples/electron-todo-list/backend/realm_config.json new file mode 100644 index 0000000000..aff50a7db3 --- /dev/null +++ b/examples/electron-todo-list/backend/realm_config.json @@ -0,0 +1,7 @@ +{ + "config_version": 20210101, + "name": "Todo-List", + "location": "IE", + "provider_region": "aws-eu-west-1", + "deployment_model": "GLOBAL" +} diff --git a/examples/electron-todo-list/backend/sync/config.json b/examples/electron-todo-list/backend/sync/config.json new file mode 100644 index 0000000000..78f4a71c74 --- /dev/null +++ b/examples/electron-todo-list/backend/sync/config.json @@ -0,0 +1,19 @@ +{ + "type": "flexible", + "state": "enabled", + "development_mode_enabled": true, + "service_name": "mongodb-atlas", + "database_name": "TodoList", + "client_max_offline_days": 30, + "is_recovery_mode_disabled": false, + "permissions": { + "rules": {}, + "defaultRoles": [] + }, + "collection_queryable_fields_names": { + "Task": [ + "isComplete", + "userId" + ] + } +} diff --git a/examples/electron-todo-list/.gitignore b/examples/electron-todo-list/frontend/.gitignore similarity index 100% rename from examples/electron-todo-list/.gitignore rename to examples/electron-todo-list/frontend/.gitignore diff --git a/examples/electron-todo-list/frontend/README.md b/examples/electron-todo-list/frontend/README.md new file mode 100644 index 0000000000..97c45eac17 --- /dev/null +++ b/examples/electron-todo-list/frontend/README.md @@ -0,0 +1,5 @@ +# Frontend + +This contains the React Native code base for the example app. + +Please see the [main README](../README.md) for all instructions. diff --git a/examples/electron-todo-list/craco.config.cjs b/examples/electron-todo-list/frontend/craco.config.cjs similarity index 91% rename from examples/electron-todo-list/craco.config.cjs rename to examples/electron-todo-list/frontend/craco.config.cjs index 085f7b9d40..ffc9d09b8d 100644 --- a/examples/electron-todo-list/craco.config.cjs +++ b/examples/electron-todo-list/frontend/craco.config.cjs @@ -1,5 +1,5 @@ -const path = require("path"); +const path = require("node:path"); const nodeExternals = require("webpack-node-externals"); module.exports = { diff --git a/examples/electron-todo-list/package-lock.json b/examples/electron-todo-list/frontend/package-lock.json similarity index 99% rename from examples/electron-todo-list/package-lock.json rename to examples/electron-todo-list/frontend/package-lock.json index 769983e638..cd9c5f8ed4 100644 --- a/examples/electron-todo-list/package-lock.json +++ b/examples/electron-todo-list/frontend/package-lock.json @@ -1,15 +1,15 @@ { - "name": "realm-electron", + "name": "@realm/example-electron-todo-list", "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "realm-electron", + "name": "@realm/example-electron-todo-list", "version": "0.1.0", "dependencies": { "@craco/craco": "^7.1.0", - "@realm/react": "^0.6.0", + "@realm/react": "^0.6.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -17,10 +17,12 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", - "realm": "^12.1.0" + "realm": "^12.2.0" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "electron": "^26.1.0", + "electron-reloader": "^1.2.3", "typescript": "^4.9.5", "webpack-node-externals": "^3.0.0" } @@ -705,9 +707,17 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -1995,6 +2005,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4976,9 +4997,9 @@ } }, "node_modules/@realm/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.0.tgz", - "integrity": "sha512-gggNChqj3J2ImgIf3Q6I++DEAo2KW+52Dh0ndv7QWhek0CLCHKIGiWYXBikDmW1bqGsj8gbLVr7mxbOshnRkKg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.1.tgz", + "integrity": "sha512-+W16jgjqXpNzLsQvOW294yqffZw36uvk3257tuk4A9a9JyO4RdX1kEYxondleV8jDAqpeyYf5ajyZuZeDiYErw==", "dependencies": { "lodash": "^4.17.21" }, @@ -4988,7 +5009,7 @@ }, "peerDependencies": { "react": ">=17.0.2", - "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0-rc || ^11.0.0" + "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0" } }, "node_modules/@remix-run/router": { @@ -8990,6 +9011,18 @@ "node": ">=10" } }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/dayjs": { "version": "1.11.9", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", @@ -9471,6 +9504,98 @@ "node": ">= 12.20.55" } }, + "node_modules/electron-is-dev": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz", + "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==", + "dev": true + }, + "node_modules/electron-reloader": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/electron-reloader/-/electron-reloader-1.2.3.tgz", + "integrity": "sha512-aDnACAzNg0QvQhzw7LYOx/nVS10mEtbuG6M0QQvNQcLnJEwFs6is+EGRCnM+KQlQ4KcTbdwnt07nd7ZjHpY4iw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "chokidar": "^3.5.0", + "date-time": "^3.1.0", + "electron-is-dev": "^1.2.0", + "find-up": "^5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-reloader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/electron-reloader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-reloader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/electron-reloader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/electron-reloader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-reloader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.508", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz", @@ -19845,9 +19970,9 @@ "optional": true }, "node_modules/realm": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.1.0.tgz", - "integrity": "sha512-uX2txyh4kWmH/rorWmsS9FZ4AM4vBh2TXDbwFn6z3b9z48kaqjCLLCcSlFN47DwHbOttCPnARg/xLHbPj5rynA==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.2.0.tgz", + "integrity": "sha512-4MFmWWl5eARaU0toMGX6cjgSX53/4jLNoHyc3UN30bXoTPKw3JVAsL070GM+Ywuhl4D5ubFZOU4y+x+r9vDgew==", "hasInstallScript": true, "dependencies": { "bson": "^4.7.2", @@ -21726,6 +21851,15 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -23786,10 +23920,16 @@ } }, "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "requires": {} + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -24611,6 +24751,12 @@ "semver": "^6.3.1" }, "dependencies": { + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "requires": {} + }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -26780,9 +26926,9 @@ } }, "@realm/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.0.tgz", - "integrity": "sha512-gggNChqj3J2ImgIf3Q6I++DEAo2KW+52Dh0ndv7QWhek0CLCHKIGiWYXBikDmW1bqGsj8gbLVr7mxbOshnRkKg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.1.tgz", + "integrity": "sha512-+W16jgjqXpNzLsQvOW294yqffZw36uvk3257tuk4A9a9JyO4RdX1kEYxondleV8jDAqpeyYf5ajyZuZeDiYErw==", "requires": { "@babel/runtime": ">=7", "lodash": "^4.17.21", @@ -29783,6 +29929,15 @@ "whatwg-url": "^8.0.0" } }, + "date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "requires": { + "time-zone": "^1.0.0" + } + }, "dayjs": { "version": "1.11.9", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", @@ -30147,6 +30302,76 @@ } } }, + "electron-is-dev": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-1.2.0.tgz", + "integrity": "sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==", + "dev": true + }, + "electron-reloader": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/electron-reloader/-/electron-reloader-1.2.3.tgz", + "integrity": "sha512-aDnACAzNg0QvQhzw7LYOx/nVS10mEtbuG6M0QQvNQcLnJEwFs6is+EGRCnM+KQlQ4KcTbdwnt07nd7ZjHpY4iw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "chokidar": "^3.5.0", + "date-time": "^3.1.0", + "electron-is-dev": "^1.2.0", + "find-up": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "electron-to-chromium": { "version": "1.4.508", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz", @@ -37633,9 +37858,9 @@ "optional": true }, "realm": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.1.0.tgz", - "integrity": "sha512-uX2txyh4kWmH/rorWmsS9FZ4AM4vBh2TXDbwFn6z3b9z48kaqjCLLCcSlFN47DwHbOttCPnARg/xLHbPj5rynA==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.2.0.tgz", + "integrity": "sha512-4MFmWWl5eARaU0toMGX6cjgSX53/4jLNoHyc3UN30bXoTPKw3JVAsL070GM+Ywuhl4D5ubFZOU4y+x+r9vDgew==", "requires": { "bson": "^4.7.2", "debug": "^4.3.4", @@ -39031,6 +39256,12 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/examples/electron-todo-list/package.json b/examples/electron-todo-list/frontend/package.json similarity index 64% rename from examples/electron-todo-list/package.json rename to examples/electron-todo-list/frontend/package.json index 7ee0d10700..455ae5878a 100644 --- a/examples/electron-todo-list/package.json +++ b/examples/electron-todo-list/frontend/package.json @@ -7,7 +7,7 @@ "homepage": "./", "dependencies": { "@craco/craco": "^7.1.0", - "@realm/react": "^0.6.0", + "@realm/react": "^0.6.1", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -15,11 +15,12 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", - "realm": "^12.1.0" + "realm": "^12.2.0" }, "scripts": { "build": "craco build", - "start": "electron ." + "start": "electron .", + "rm-local-db": "rm -rf mongodb-realm/" }, "eslintConfig": { "extends": [ @@ -28,9 +29,22 @@ ] }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "electron": "^26.1.0", "electron-reloader": "^1.2.3", "typescript": "^4.9.5", "webpack-node-externals": "^3.0.0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] } } diff --git a/examples/electron-todo-list/frontend/public/electron.cjs b/examples/electron-todo-list/frontend/public/electron.cjs new file mode 100644 index 0000000000..bffeb2f81c --- /dev/null +++ b/examples/electron-todo-list/frontend/public/electron.cjs @@ -0,0 +1,44 @@ +const { app, BrowserWindow } = require("electron"); +const path = require("node:path"); + +let mainWindow; + +function createWindow() { + // Create the browser window. + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + // In order to use Realm and `@realm/react` in the rendering process, it is + // required to enable the `nodeIntegration` and disable `contextIsolation`. + webPreferences: { nodeIntegration: true, contextIsolation: false }, + }); + // and load the index.html of the app. + mainWindow.loadFile(path.join(__dirname, "../build/index.html")); + mainWindow?.webContents.openDevTools(); +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + createWindow(); + + // MacOS apps generally continue running even without any windows open, and + // activating the app when no windows are available should open a new one. + app.on('activate', () => { + const noWindowsOpened = BrowserWindow.getAllWindows().length === 0; + if (noWindowsOpened) { + createWindow(); + } + }) +}) + +// On Windows and Linux, exiting all windows generally quits an application +// entirely. Because windows cannot be created before the `ready` event, you +// should only listen for activate events after your app is initialized. Thus, +// we attach the event listener from within this `whenReady()` callback. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); diff --git a/examples/electron-todo-list/public/favicon.ico b/examples/electron-todo-list/frontend/public/favicon.ico similarity index 100% rename from examples/electron-todo-list/public/favicon.ico rename to examples/electron-todo-list/frontend/public/favicon.ico diff --git a/examples/electron-todo-list/public/index.html b/examples/electron-todo-list/frontend/public/index.html similarity index 92% rename from examples/electron-todo-list/public/index.html rename to examples/electron-todo-list/frontend/public/index.html index aa069f27cb..6171358380 100644 --- a/examples/electron-todo-list/public/index.html +++ b/examples/electron-todo-list/frontend/public/index.html @@ -7,7 +7,7 @@ - React App + Electron App Using Atlas Device SDK diff --git a/examples/electron-todo-list/public/logo192.png b/examples/electron-todo-list/frontend/public/logo192.png similarity index 100% rename from examples/electron-todo-list/public/logo192.png rename to examples/electron-todo-list/frontend/public/logo192.png diff --git a/examples/electron-todo-list/public/logo512.png b/examples/electron-todo-list/frontend/public/logo512.png similarity index 100% rename from examples/electron-todo-list/public/logo512.png rename to examples/electron-todo-list/frontend/public/logo512.png diff --git a/examples/electron-todo-list/public/manifest.json b/examples/electron-todo-list/frontend/public/manifest.json similarity index 100% rename from examples/electron-todo-list/public/manifest.json rename to examples/electron-todo-list/frontend/public/manifest.json diff --git a/examples/electron-todo-list/public/robots.txt b/examples/electron-todo-list/frontend/public/robots.txt similarity index 100% rename from examples/electron-todo-list/public/robots.txt rename to examples/electron-todo-list/frontend/public/robots.txt diff --git a/examples/electron-todo-list/src/index.tsx b/examples/electron-todo-list/frontend/src/index.tsx similarity index 100% rename from examples/electron-todo-list/src/index.tsx rename to examples/electron-todo-list/frontend/src/index.tsx diff --git a/examples/electron-todo-list/src/main/index.js b/examples/electron-todo-list/frontend/src/main/index.js similarity index 100% rename from examples/electron-todo-list/src/main/index.js rename to examples/electron-todo-list/frontend/src/main/index.js diff --git a/examples/electron-todo-list/src/renderer/App.tsx b/examples/electron-todo-list/frontend/src/renderer/App.tsx similarity index 92% rename from examples/electron-todo-list/src/renderer/App.tsx rename to examples/electron-todo-list/frontend/src/renderer/App.tsx index e3b3420017..efc552ae1b 100644 --- a/examples/electron-todo-list/src/renderer/App.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/App.tsx @@ -43,6 +43,10 @@ const router = createHashRouter([ } ]); +/** + * The root React component which renders `@realm/react`'s `AppProvider` + * for instantiation an Atlas App Services App. + */ function App() { return (
diff --git a/examples/electron-todo-list/frontend/src/renderer/AuthenticatedApp.tsx b/examples/electron-todo-list/frontend/src/renderer/AuthenticatedApp.tsx new file mode 100644 index 0000000000..2996c48a46 --- /dev/null +++ b/examples/electron-todo-list/frontend/src/renderer/AuthenticatedApp.tsx @@ -0,0 +1,73 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { Navigate, Outlet } from 'react-router-dom'; +import { OpenRealmBehaviorType } from 'realm'; +import { RealmProvider, UserProvider } from '@realm/react'; + +import { Task } from './models/Task'; +import { PageLayout } from './components/PageLayout'; + +/** + * The part of the React tree having access to an authenticated user. It + * renders `@realm/react`'s `UserProvider` for providing the App User once + * authenticated and `RealmProvider` for opening a Realm. + */ +export function AuthenticatedApp() { + return ( + // The component set as the `fallback` prop will be rendered if a user has + // not been authenticated. In this case, we will navigate the user to the + // unauthenticated route via the `Navigate` component. Once authenticated, + // `RealmProvider` will have access to the user and set it in the Realm + // configuration; therefore, you don't have to explicitly provide it here. + }> + { + mutableSubs.add(realm.objects(Task), { name: 'allTasks' }); + }), + }, + // We can specify the behavior when opening a Realm for the first time + // (`newRealmFileBehavior`) and for subsequent ones (`existingRealmFileBehavior`). + // If the user has logged in at least 1 time before, the Realm and its data will + // exist on disk and can be opened even when offline. We can either (a) open the + // Realm immediately (or first create a new empty Realm file if it does not + // exist before opening it) and sync the data to the device in the background + // (`OpenRealmBehaviorType.OpenImmediately`), or (b) wait for any non-synced + // data to be fully downloaded (`OpenRealmBehaviorType.DownloadBeforeOpen`). + // For more possible configurations of new and existing Realm file behaviors, see: + // https://www.mongodb.com/docs/realm-sdks/js/latest/types/OpenRealmBehaviorConfiguration.html + newRealmFileBehavior: { + type: OpenRealmBehaviorType.OpenImmediately, + }, + existingRealmFileBehavior: { + type: OpenRealmBehaviorType.OpenImmediately, + }, + }} + > + + + + + + ); +} diff --git a/examples/electron-todo-list/frontend/src/renderer/assets/atlas-app-services.png b/examples/electron-todo-list/frontend/src/renderer/assets/atlas-app-services.png new file mode 100644 index 0000000000..3b38c79bd8 Binary files /dev/null and b/examples/electron-todo-list/frontend/src/renderer/assets/atlas-app-services.png differ diff --git a/examples/electron-todo-list/frontend/src/renderer/assets/screenshot-electron-login.png b/examples/electron-todo-list/frontend/src/renderer/assets/screenshot-electron-login.png new file mode 100644 index 0000000000..8bab83761f Binary files /dev/null and b/examples/electron-todo-list/frontend/src/renderer/assets/screenshot-electron-login.png differ diff --git a/examples/electron-todo-list/frontend/src/renderer/assets/screenshot-electron-tasks.png b/examples/electron-todo-list/frontend/src/renderer/assets/screenshot-electron-tasks.png new file mode 100644 index 0000000000..95bfcb06b9 Binary files /dev/null and b/examples/electron-todo-list/frontend/src/renderer/assets/screenshot-electron-tasks.png differ diff --git a/examples/electron-todo-list/src/renderer/atlas-app-services/config.json b/examples/electron-todo-list/frontend/src/renderer/atlas-app-services/config.json similarity index 100% rename from examples/electron-todo-list/src/renderer/atlas-app-services/config.json rename to examples/electron-todo-list/frontend/src/renderer/atlas-app-services/config.json diff --git a/examples/electron-todo-list/src/renderer/components/AddTaskForm.tsx b/examples/electron-todo-list/frontend/src/renderer/components/AddTaskForm.tsx similarity index 97% rename from examples/electron-todo-list/src/renderer/components/AddTaskForm.tsx rename to examples/electron-todo-list/frontend/src/renderer/components/AddTaskForm.tsx index 4f0f4e9d23..42dc6e44ea 100644 --- a/examples/electron-todo-list/src/renderer/components/AddTaskForm.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/components/AddTaskForm.tsx @@ -24,6 +24,9 @@ type AddTaskFormProps = { onSubmit: (description: string) => void; }; +/** + * Form for adding a new task. + */ export function AddTaskForm({ onSubmit }: AddTaskFormProps) { const [description, setDescription] = useState(''); @@ -36,12 +39,12 @@ export function AddTaskForm({ onSubmit }: AddTaskFormProps) { return (
setDescription(event.currentTarget.value)} placeholder='Add a new task' + type='text' value={description} - onChange={(event) => setDescription(event.currentTarget.value)} - autoCapitalize='none' // Safari only /> ); diff --git a/examples/electron-todo-list/src/renderer/components/PageLayout.tsx b/examples/electron-todo-list/frontend/src/renderer/components/PageLayout.tsx similarity index 93% rename from examples/electron-todo-list/src/renderer/components/PageLayout.tsx rename to examples/electron-todo-list/frontend/src/renderer/components/PageLayout.tsx index ebfba025cf..68ce3230f9 100644 --- a/examples/electron-todo-list/src/renderer/components/PageLayout.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/components/PageLayout.tsx @@ -23,6 +23,9 @@ type PageLayoutProps = { children: React.ReactNode; }; +/** + * Wrapper around the `Outlet` for providing a consistent layout. + */ export function PageLayout({ children }: PageLayoutProps) { return (
diff --git a/examples/electron-todo-list/src/renderer/components/TaskItem.tsx b/examples/electron-todo-list/frontend/src/renderer/components/TaskItem.tsx similarity index 93% rename from examples/electron-todo-list/src/renderer/components/TaskItem.tsx rename to examples/electron-todo-list/frontend/src/renderer/components/TaskItem.tsx index 303aaa29c6..0593f99b4d 100644 --- a/examples/electron-todo-list/src/renderer/components/TaskItem.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/components/TaskItem.tsx @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { Task } from '../models/Task'; +import type { Task } from '../models/Task'; import styles from '../styles/TaskItem.module.css'; type TaskItemProps = { @@ -25,6 +25,9 @@ type TaskItemProps = { onDelete: (task: Task) => void; }; +/** + * Displays a task list item with options to update or delete it. + */ export function TaskItem({ task, onToggleStatus, onDelete }: TaskItemProps) { return (
diff --git a/examples/electron-todo-list/src/renderer/components/TaskList.tsx b/examples/electron-todo-list/frontend/src/renderer/components/TaskList.tsx similarity index 94% rename from examples/electron-todo-list/src/renderer/components/TaskList.tsx rename to examples/electron-todo-list/frontend/src/renderer/components/TaskList.tsx index 782f2597bd..9d811b7675 100644 --- a/examples/electron-todo-list/src/renderer/components/TaskList.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/components/TaskList.tsx @@ -18,7 +18,7 @@ import Realm from 'realm'; -import { Task } from '../models/Task'; +import type { Task } from '../models/Task'; import { TaskItem } from './TaskItem'; import styles from '../styles/TaskList.module.css'; @@ -28,6 +28,9 @@ type TaskListProps = { onDeleteTask: (task: Task) => void; }; +/** + * Displays a list of tasks. + */ export function TaskList({ tasks, onToggleTaskStatus, onDeleteTask }: TaskListProps) { return (
diff --git a/examples/electron-todo-list/src/renderer/hooks/useTaskManager.ts b/examples/electron-todo-list/frontend/src/renderer/hooks/useTaskManager.ts similarity index 53% rename from examples/electron-todo-list/src/renderer/hooks/useTaskManager.ts rename to examples/electron-todo-list/frontend/src/renderer/hooks/useTaskManager.ts index 0638ba53a4..eae0087b5c 100644 --- a/examples/electron-todo-list/src/renderer/hooks/useTaskManager.ts +++ b/examples/electron-todo-list/frontend/src/renderer/hooks/useTaskManager.ts @@ -22,7 +22,8 @@ import { useQuery, useRealm, useUser } from '@realm/react'; import { Task } from '../models/Task'; /** - * Manages changes to the tasks in the realm. + * Provides functions for managing changes to the tasks in the Realm, + * such as adding, updating, and deleting tasks. */ export function useTaskManager() { const realm = useRealm(); @@ -36,18 +37,46 @@ export function useTaskManager() { setRequeryFlag(true); }, []); + /** + * Adds a task to the database. + * + * @note + * Everything in the function passed to `realm.write()` is a transaction and will + * hence succeed or fail together. A transaction is the smallest unit of transfer + * in Realm so we want to be mindful of how much we put into one single transaction + * and split them up if appropriate (more commonly seen server side). Since clients + * may occasionally be online during short time spans we want to increase the probability + * of sync participants to successfully sync everything in the transaction, otherwise + * no changes propagate and the transaction needs to start over when connectivity allows. + */ const addTask = useCallback((description: string) => { realm.write(() => { - realm.create(Task, { description, userId: user.id } as Task); + realm.create(Task, { description, userId: user.id }); }); }, [realm, user.id]); + /** + * Updates a task by toggling its `isComplete` status. + * + * @note + * Normally when updating a record in a NoSQL or SQL database, we have to type + * a statement that will later be interpreted and used as instructions for how + * to update the record. But in Realm, the objects are "live" because they are + * actually referencing the object's location in memory on the device (memory mapping). + * So rather than typing a statement, we modify the object directly by changing + * the property values. If the changes adhere to the schema, Realm will accept + * this new version of the object and wherever this object is being referenced + * locally will also see the changes "live". + */ const toggleTaskStatus = useCallback((task: Task) => { realm.write(() => { task.isComplete = !task.isComplete; }); }, [realm]); + /** + * Deletes a task from the database. + */ const deleteTask = useCallback((task: Task) => { realm.write(() => { realm.delete(task); diff --git a/examples/electron-todo-list/src/renderer/models/Task.ts b/examples/electron-todo-list/frontend/src/renderer/models/Task.ts similarity index 85% rename from examples/electron-todo-list/src/renderer/models/Task.ts rename to examples/electron-todo-list/frontend/src/renderer/models/Task.ts index 969205c3ad..ee8f02fb93 100644 --- a/examples/electron-todo-list/src/renderer/models/Task.ts +++ b/examples/electron-todo-list/frontend/src/renderer/models/Task.ts @@ -18,7 +18,13 @@ import Realm, { BSON, ObjectSchema } from 'realm'; -export class Task extends Realm.Object { +/** + * The `Task` data model. + * + * @see + * - Define a model: {@link https://www.mongodb.com/docs/realm/sdk/react-native/model-data/define-a-realm-object-model/} + */ +export class Task extends Realm.Object { _id!: BSON.ObjectId; description!: string; isComplete!: boolean; diff --git a/examples/electron-todo-list/src/renderer/pages/ErrorPage.tsx b/examples/electron-todo-list/frontend/src/renderer/pages/ErrorPage.tsx similarity index 95% rename from examples/electron-todo-list/src/renderer/pages/ErrorPage.tsx rename to examples/electron-todo-list/frontend/src/renderer/pages/ErrorPage.tsx index cd0626f03e..938ae5ee3b 100644 --- a/examples/electron-todo-list/src/renderer/pages/ErrorPage.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/pages/ErrorPage.tsx @@ -20,6 +20,9 @@ import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router-d import styles from '../styles/ErrorPage.module.css'; +/** + * Page shown when navigating to a non-existent route. + */ export function ErrorPage() { const navigate = useNavigate(); const error = useRouteError(); diff --git a/examples/electron-todo-list/src/renderer/pages/LoginPage.tsx b/examples/electron-todo-list/frontend/src/renderer/pages/LoginPage.tsx similarity index 91% rename from examples/electron-todo-list/src/renderer/pages/LoginPage.tsx rename to examples/electron-todo-list/frontend/src/renderer/pages/LoginPage.tsx index af6ca092aa..93a799f88c 100644 --- a/examples/electron-todo-list/src/renderer/pages/LoginPage.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/pages/LoginPage.tsx @@ -20,9 +20,12 @@ import { FormEvent, useEffect, useState } from 'react'; import { Navigate } from 'react-router-dom'; import { AuthOperationName, useApp, useEmailPasswordAuth } from '@realm/react'; -import logo from '../assets/logo.png'; +import logo from '../assets/atlas-app-services.png'; import styles from '../styles/LoginPage.module.css'; +/** + * Screen for registering and/or logging in to the App Services App. + */ export function LoginPage() { const atlasApp = useApp(); const { register, logIn, result } = useEmailPasswordAuth(); @@ -30,6 +33,7 @@ export function LoginPage() { const [password, setPassword] = useState(''); const [authRequest, setAuthRequest] = useState<'login' | 'register'>('login'); + // Automatically log in the user after successful registration. useEffect(() => { if (result.operation === AuthOperationName.Register && result.success) { logIn({ email, password }); @@ -58,28 +62,29 @@ export function LoginPage() { return (
Atlas Device Sync

- Log in to try out Realm in Electron & Atlas Device Sync + Atlas Device SDK for Electron

setEmail(event.currentTarget.value)} placeholder='Email' + type='text' value={email} - onChange={(event) => setEmail(event.currentTarget.value)} - autoCorrect='off' // Safari only - autoCapitalize='none' // Safari only /> setPassword(event.currentTarget.value)} placeholder='Password (min. 6 chars)' + type='password' value={password} - onChange={(event) => setPassword(event.currentTarget.value)} /> {result.error && (

@@ -89,17 +94,17 @@ export function LoginPage() {

diff --git a/examples/electron-todo-list/src/renderer/pages/TaskPage.tsx b/examples/electron-todo-list/frontend/src/renderer/pages/TaskPage.tsx similarity index 93% rename from examples/electron-todo-list/src/renderer/pages/TaskPage.tsx rename to examples/electron-todo-list/frontend/src/renderer/pages/TaskPage.tsx index 73306779d0..c08ac018ab 100644 --- a/examples/electron-todo-list/src/renderer/pages/TaskPage.tsx +++ b/examples/electron-todo-list/frontend/src/renderer/pages/TaskPage.tsx @@ -22,6 +22,10 @@ import { TaskList } from '../components/TaskList'; import { useTaskManager } from '../hooks/useTaskManager'; import styles from '../styles/TaskPage.module.css'; +/** + * Displays the list of tasks as well as buttons for performing + * sync-related operations. + */ export function TaskPage() { const { tasks, diff --git a/examples/electron-todo-list/src/renderer/react-app-env.d.ts b/examples/electron-todo-list/frontend/src/renderer/react-app-env.d.ts similarity index 100% rename from examples/electron-todo-list/src/renderer/react-app-env.d.ts rename to examples/electron-todo-list/frontend/src/renderer/react-app-env.d.ts diff --git a/examples/electron-todo-list/src/renderer/setupTests.ts b/examples/electron-todo-list/frontend/src/renderer/setupTests.ts similarity index 100% rename from examples/electron-todo-list/src/renderer/setupTests.ts rename to examples/electron-todo-list/frontend/src/renderer/setupTests.ts diff --git a/examples/electron-todo-list/src/renderer/styles/AddTaskForm.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/AddTaskForm.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/AddTaskForm.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/AddTaskForm.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/App.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/App.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/App.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/App.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/ErrorPage.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/ErrorPage.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/ErrorPage.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/ErrorPage.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/IntroText.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/IntroText.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/IntroText.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/IntroText.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/LoginPage.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/LoginPage.module.css similarity index 97% rename from examples/electron-todo-list/src/renderer/styles/LoginPage.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/LoginPage.module.css index e1385095e9..d8206c9298 100644 --- a/examples/electron-todo-list/src/renderer/styles/LoginPage.module.css +++ b/examples/electron-todo-list/frontend/src/renderer/styles/LoginPage.module.css @@ -7,6 +7,10 @@ justify-content: center; } +.logo { + height: 150px; +} + h1 { margin-top: 40px; margin-bottom: 70px; diff --git a/examples/electron-todo-list/src/renderer/styles/NavBar.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/NavBar.module.css similarity index 65% rename from examples/electron-todo-list/src/renderer/styles/NavBar.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/NavBar.module.css index 48053cdd79..899fb363e2 100644 --- a/examples/electron-todo-list/src/renderer/styles/NavBar.module.css +++ b/examples/electron-todo-list/frontend/src/renderer/styles/NavBar.module.css @@ -1,16 +1,29 @@ .nav { width: 100%; height: 80px; - padding: 0 20px; + padding: 0 30px; display: flex; - align-items: center; justify-content: space-between; + align-items: center; border-bottom: 1px solid var(--light-gray); background-color: white; } -.logo { - object-fit: none; +.titleContainer { + padding-left: 10px; + border-left: 2px solid var(--primary-color-dark); +} + +.title { + margin-bottom: 8px; + font-size: 16px; + font-weight: bold; + color: var(--gray); +} + +.info { + font-size: 13px; + color: var(--gray); } .button { diff --git a/examples/electron-todo-list/src/renderer/styles/PageLayout.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/PageLayout.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/PageLayout.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/PageLayout.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/TaskItem.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/TaskItem.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/TaskItem.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/TaskItem.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/TaskList.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/TaskList.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/TaskList.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/TaskList.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/TaskPage.module.css b/examples/electron-todo-list/frontend/src/renderer/styles/TaskPage.module.css similarity index 100% rename from examples/electron-todo-list/src/renderer/styles/TaskPage.module.css rename to examples/electron-todo-list/frontend/src/renderer/styles/TaskPage.module.css diff --git a/examples/electron-todo-list/src/renderer/styles/global.css b/examples/electron-todo-list/frontend/src/renderer/styles/global.css similarity index 95% rename from examples/electron-todo-list/src/renderer/styles/global.css rename to examples/electron-todo-list/frontend/src/renderer/styles/global.css index f608ddf2a1..ff1771b751 100644 --- a/examples/electron-todo-list/src/renderer/styles/global.css +++ b/examples/electron-todo-list/frontend/src/renderer/styles/global.css @@ -17,7 +17,7 @@ h1 { font-weight: 700; - font-size: 2.5rem; + font-size: 2.2rem; font-family: var(--primary-font); text-align: center; color: #2b0676; diff --git a/examples/electron-todo-list/tsconfig.json b/examples/electron-todo-list/frontend/tsconfig.json similarity index 100% rename from examples/electron-todo-list/tsconfig.json rename to examples/electron-todo-list/frontend/tsconfig.json diff --git a/examples/electron-todo-list/public/electron.cjs b/examples/electron-todo-list/public/electron.cjs deleted file mode 100644 index bc1c9896f1..0000000000 --- a/examples/electron-todo-list/public/electron.cjs +++ /dev/null @@ -1,26 +0,0 @@ -const electron = require("electron"); -const path = require("path"); - -const app = electron.app; -const BrowserWindow = electron.BrowserWindow; - -let mainWindow; - -function createWindow() { - // Create the browser window. - mainWindow = new BrowserWindow({ - width: 800, - height: 600, - // In order to use Realm and `@realm/react` in the rendering process, it is - // required to enable the `nodeIntegration` and disable `contextIsolation`. - webPreferences: { nodeIntegration: true, contextIsolation: false }, - }); - // and load the index.html of the app. - mainWindow.loadFile(path.join(__dirname, "../build/index.html")); - mainWindow?.webContents.openDevTools(); -} - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.on("ready", createWindow); diff --git a/examples/electron-todo-list/src/renderer/AuthenticatedApp.tsx b/examples/electron-todo-list/src/renderer/AuthenticatedApp.tsx deleted file mode 100644 index 8ab5059871..0000000000 --- a/examples/electron-todo-list/src/renderer/AuthenticatedApp.tsx +++ /dev/null @@ -1,50 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2023 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -import { Navigate, Outlet } from 'react-router-dom'; -import { RealmProvider, UserProvider } from '@realm/react'; - -import { Task } from './models/Task'; -import { PageLayout } from './components/PageLayout'; - -export function AuthenticatedApp() { - // The component set as the `fallback` prop will be rendered if a user has - // not been authenticated. In this case, we will navigate the user to the - // unauthenticated route via the `Navigate` component. Once authenticated, - // `RealmProvider` will have access to the user and set it in the Realm - // configuration; therefore, you don't have to explicitly provide it here. - return ( - }> - { - mutableSubs.add(realm.objects(Task), { name: 'allTasks' }); - }), - }, - }} - > - - - - - - ); -} diff --git a/examples/electron-todo-list/src/renderer/assets/logo.png b/examples/electron-todo-list/src/renderer/assets/logo.png deleted file mode 100644 index 4f5790bb82..0000000000 Binary files a/examples/electron-todo-list/src/renderer/assets/logo.png and /dev/null differ diff --git a/examples/electron-todo-list/src/renderer/assets/screenshot-realm-web-sync-login.png b/examples/electron-todo-list/src/renderer/assets/screenshot-realm-web-sync-login.png deleted file mode 100644 index 61e7147dc7..0000000000 Binary files a/examples/electron-todo-list/src/renderer/assets/screenshot-realm-web-sync-login.png and /dev/null differ diff --git a/examples/electron-todo-list/src/renderer/assets/screenshot-realm-web-sync-tasks.png b/examples/electron-todo-list/src/renderer/assets/screenshot-realm-web-sync-tasks.png deleted file mode 100644 index 98d25e1ab3..0000000000 Binary files a/examples/electron-todo-list/src/renderer/assets/screenshot-realm-web-sync-tasks.png and /dev/null differ diff --git a/examples/node-connection-and-error/README.md b/examples/node-connection-and-error/README.md index f02b408489..4376604786 100644 --- a/examples/node-connection-and-error/README.md +++ b/examples/node-connection-and-error/README.md @@ -1,52 +1,93 @@ -# Connection State Change & Error Handling in Realm Node.js SDK +# Connection State Change & Error Handling Using Atlas Device SDK for Node.js -A skeleton app to be used as a reference for how to use the [Realm Node.js SDK](https://www.mongodb.com/docs/realm/sdk/node/) specifically around detecting various changes in e.g. connection state, user state, and sync errors, in order to better guide developers. +An example app showcasing how to detect various changes in connection state, user state, sync errors, and product inventory in [MongoDB's Atlas Device SDK for React Native](https://www.mongodb.com/docs/realm/sdk/node/) (fka Realm). ## Project Structure The following shows the project structure and the most relevant files. +> To learn more about the backend file structure, see [App Configuration](https://www.mongodb.com/docs/atlas/app-services/reference/config/). + ``` -├── src -│ ├── atlas-app-services - Configure Atlas App -│ │ ├── config.ts -│ │ └── getAtlasApp.ts -│ ├── models - Simplified data model -│ │ ├── Kiosk.ts -│ │ ├── Product.ts -│ │ └── Store.ts -│ ├── index.ts - Entry point -│ ├── logger.ts - Replaceable logger -│ ├── realm-auth.ts - Main Realm auth usage examples -│ └── realm-query.ts - Data access/manipulation helper -└── other.. +├── backend - App Services App +│ └── (see link above) +│ +├── node - Node App +│ ├── src +│ │ ├── atlas-app-services - Configure Atlas App +│ │ │ ├── config.ts +│ │ │ └── getAtlasApp.ts +│ │ │ +│ │ ├── models - Simplified data model +│ │ │ ├── Kiosk.ts +│ │ │ ├── Product.ts +│ │ │ └── Store.ts +│ │ │ +│ │ ├── utils +│ │ │ └── logger.ts - Replaceable logger +│ │ │ +│ │ ├── app.ts - Entry point +│ │ ├── demo-auth-triggers.ts - Triggers for various auth listeners +│ │ ├── demo-sync-triggers.ts - Triggers for various sync listeners +│ │ └── store-manager.ts - Queries and updates store data +│ │ +│ └── package.json - Dependencies +│ +└── README.md - Instructions and info ``` -Main file for showcasing Realm usage pertaining to connection and error handling: -* [src/realm-auth.ts](./src/realm-auth.ts) - ## Use Cases -This app focuses on showing where and when you can (a) perform logging and (b) handle specific scenarios based on observed changes. It specifically addresses the following points: - -* Logging in using email/password authentication. -* Listening when a user is logged out or removed. -* Listening when a user's tokens are refreshed. -* Listening when the underlying sync session: +This app focuses on showing where and when you can (a) perform logging and (b) handle specific scenarios based on observed changes. The app itself is a store with kiosks displaying all the products in that store. + +It specifically addresses the following points: + +* Registering and logging in using [Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/). + * The following scenarios can be triggered: + * Registering + * Successfully + * With invalid password + * With email already in use + * Logging in + * Successfully + * With invalid password + * With non-existent email +* Listening (and triggering) when a user gets logged out. +* Listening (and triggering) when a user's tokens are refreshed. +* Listening (and triggering) when the underlying sync session: * Tries to connect * Gets connected * Disconnects * Fails to reconnect -* Listening for sync errors. -* Listening for pre and post client resets. -* Generally providing best practices for the surrounding Realm usage such as opening and closing of realms, configurations, adding subscriptions, etc. -* Includes useful comments around the use of Realm. -* Note that an over-simplified data model is used. This app also writes data to confirm the functionality. +* Listening (and triggering) for sync errors. +* Listening (and triggering) for pre and post client resets. +* Listening (and triggering) for changes in product inventory: + * Deletions + * Insertions + * Modifications +* Configurations for opening a synced Realm. + * 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. + +### Note: Offline Support + +Users who have logged in at least once will have their credentials cached on the client. Thus, a logged in user who restarts the app will remain logged in. This app handles that in `logIn()` in [demo-auth-triggers.ts](./node/src/demo-auth-triggers.ts) by checking if the `app.currentUser` already exists. + +Data that was previously synced to the device will also exist locally in the Realm database. From this point on, users can be offline and still query and update data. Any changes made offline will be synced automatically to Atlas and any other devices once a network connection is established. If multiple users modify the same data either while online or offline, those conflicts are [automatically resolved](https://www.mongodb.com/docs/atlas/app-services/sync/details/conflict-resolution/) before being synced. + +#### Realm Configuration -### Realm Details +When [opening a Realm](https://www.mongodb.com/docs/realm/sdk/node/sync/configure-and-open-a-synced-realm/#open-a-synced-realm-while-offline), we can specify the behavior in the Realm configuration when opening it for the first time (via `newRealmFileBehavior`) and for subsequent ones (via `existingRealmFileBehavior`). We can either: +* `OpenRealmBehaviorType.OpenImmediately` + * Opens the Realm file immediately if it exists, otherwise it first creates a new empty Realm file then opens it. + * This lets users use the app with the existing data, while syncing any changes to the device in the background. +* `OpenRealmBehaviorType.DownloadBeforeOpen` + * If there is data to be downloaded, this waits for the data to be fully synced before opening the Realm. -* RealmJS version: ^12.0.0 -* Device Sync type: Flexible +This app opens a Realm in [store-manager.ts](./node/src/store-manager.ts). We use `OpenImmediately` for new and existing Realm files in order to use the app while offline. + +> See [OpenRealmBehaviorConfiguration](https://www.mongodb.com/docs/realm-sdks/js/latest/types/OpenRealmBehaviorConfiguration.html) for possible configurations of new and existing Realm file behaviors. ## Background @@ -56,14 +97,17 @@ This app focuses on showing where and when you can (a) perform logging and (b) h Device Sync will automatically recover from most of the errors; however, in a few cases, the exceptions might be fatal and will require some user interaction. +In this demo app, a sync error is triggered by trying to create a `Store` document that is outside of the query filter subscribed to. Since the Realm subscribes to a store with a specific ID when being opened, attempting to create one with a different ID will generate a sync error. + +You can also trigger sync errors by [modifying the permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions) of fields and/or collections, and then try to perform non-permitted operations from the client. + ### Connection Changes Connection changes can be detected by adding a listener callback to the Realm's sync session. The callback will be invoked whenever the underlying sync session changes its connection state. Since retries will start automatically when disconnected, there is no need to manually reconnect. -> Convenience method: -> * Check if the app is connected: `app.syncSession?.isConnected()` +Be aware of that there may be a delay from the time of actual disconnect until the listener is invoked. ### User Event Changes and Tokens @@ -73,13 +117,21 @@ Access tokens are created once a user logs in and are refreshed automatically by By default, refresh tokens expire 60 days after they are issued. In the Admin UI, you can [configure](https://www.mongodb.com/docs/atlas/app-services/users/sessions/#configure-refresh-token-expiration) this time for your App's refresh tokens to be anywhere between 30 minutes and 180 days, whereafter you can observe the relevant client listeners being fired. -> Convenience methods: -> * Get the user's access token: `app.currentUser?.accessToken` -> * Get the user's refresh token: `app.currentUser?.refreshToken` - ### Client Reset -The server will [reset the client](https://www.mongodb.com/docs/atlas/app-services/sync/error-handling/client-resets/) whenever there is a discrepancy in the data history that cannot be resolved. By default, Realm will try to recover any unsynced changes from the client while resetting. However, there are other strategies available: You can discard the changes or do a [manual recovery](https://www.mongodb.com/docs/realm/sdk/node/advanced/client-reset-data-recovery/). +The server will [reset the client](https://www.mongodb.com/docs/atlas/app-services/sync/error-handling/client-resets/) whenever there is a discrepancy in the data history that cannot be resolved. By default, Realm will try to recover any unsynced changes from the client while resetting. However, there are other [strategies available](https://www.mongodb.com/docs/realm/sdk/node/sync/handle-sync-errors/#handle-client-reset-errors): You can discard the changes or do a manual recovery. + +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. + +To modify the [log level and logger](https://www.mongodb.com/docs/realm/sdk/react-native/logging/#std-label-react-native-logging) used by Realm, we use `Realm.setLogLevel()` and `Realm.setLogger()` in [app.ts](./node/src/app.ts). + +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/). ## Getting Started @@ -87,20 +139,64 @@ The server will [reset the client](https://www.mongodb.com/docs/atlas/app-servic * [Node.js](https://nodejs.org/) -### Set Up an Atlas App Services App +### Set up an Atlas Database + +Start by [deploying a free Atlas cluster](https://www.mongodb.com/docs/atlas/getting-started/#get-started-with-atlas) and create an Atlas database. + +### Set up an Atlas App Services App + +You can either choose to set up your App via a CLI (this has fewer steps and is much faster since all configurations are already provided in the [backend directory](./backend/)), or via the App Services UI (steps provided below). + +#### Via a CLI (recommended) + +To import and deploy changes from your local directory to App Services you can use the command line interface: + +1. [Set up Realm CLI](https://www.mongodb.com/docs/atlas/app-services/cli/). +2. In the provided [backend directory](./backend/) (the App Services App), update the following: + * Cluster Name + * Update the `"clusterName"` in [data_sources/mongodb-atlas/config.json](./backend/data_sources/mongodb-atlas/config.json) to the name of your cluster. + * (The default name is `Cluster0`.) + * App ID + * There is no `"app_id"` defined in [realm_config.json](./backend/realm_config.json) since we will create a brand new App. **If** you for some reason are updating an existing app, add an `"app_id"` field and its value. +3. [Push and deploy](https://www.mongodb.com/docs/atlas/app-services/cli/realm-cli-push/#std-label-realm-cli-push) the local directory to App Services: +```sh +realm-cli push --local +``` +4. Once pushed, verify that your App shows up in the App Services UI. +5. 🥳 You can now go ahead and [install dependencies and run the React Native app](#install-dependencies). -To sync Realm data you must first: +#### Via the App Services UI -1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/) -2. Enable [Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/#std-label-email-password-authentication) -3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **Development Mode** on. - * When Development Mode is enabled, queryable fields will be added automatically. - * Queryable fields used in this app: `_id`, `storeId` +To sync data used in this app you must first: -After running the client and seeing the available collections in Atlas, [set read/write permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#with-device-sync) for all collections. +1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/). +2. [Enable Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/). + * For this example app, we automatically confirm users' emails. +3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **Development Mode** enabled. + * When Development Mode is enabled, [queryable fields](https://www.mongodb.com/docs/atlas/app-services/sync/configure/sync-settings/#queryable-fields) will be added **automatically**, and schemas will be inferred based on the client Realm data models. + * For information, queryable fields used in this app include: + * Global (all collections): `_id`, `storeId` + * (Development Mode should be turned off in production.) +4. Don't forget to click `Review Draft and Deploy`. + +### Add an Atlas Function + +> If you set up your App Services App [via a CLI](#via-a-cli-recommended), you can **skip this step** as the function 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. + +To set this up via the App Services UI: + +1. [Define a function](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) ### Install Dependencies +From the [node directory](./node/), run: + ```sh npm install ``` @@ -108,22 +204,61 @@ npm install ### Run the App 1. Copy your [Atlas App ID](https://www.mongodb.com/docs/atlas/app-services/reference/find-your-project-or-app-id/#std-label-find-your-app-id) from the App Services UI. -2. Paste the copied ID as the value of the existing variable `ATLAS_APP_ID` in [src/atlas-app-services/config.ts](./src/atlas-app-services/config.ts): +2. Paste the copied ID as the value of the existing variable `ATLAS_APP_ID` in [src/atlas-app-services/config.ts](./node/src/atlas-app-services/config.ts): ```js export const ATLAS_APP_ID = "YOUR_APP_ID"; ``` -3. Start the script. +3. Invoke various scenarios: ```sh -npm start +# Successfully register and log in a user and run the app. +npm run success + +# Register a user with invalid credentials. +npm run register-invalid + +# Register a user with an email already in use. +npm run register-email-in-use + +# Log in a user with invalid credentials. +npm run login-invalid + +# Log in a user with an email that does not exist. +npm run login-non-existent-email + +# Log in a user and trigger a sync error. +npm run sync-error + +# Log in a user and trigger a client reset. +npm run client-reset ``` -### Troubleshooting +### Set Data Access Permissions -* If permission is denied: - * Whitelist your IP address via the Atlas UI. - * Make sure you have [read/write permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#with-device-sync) for all collections. -* Removing the local database can be useful for certain errors. - * When running the app, the local database will exist in the directory `mongodb-realm/`. - * To remove it, run: `npm run rm-local-db` +> If you set up your App Services App [via a CLI](#via-a-cli-recommended), you can **skip this step** as the permissions should already be defined for you. + +After running the client app for the first time, [check the rules](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions) for the collections in the App Services UI and make sure all collections have `readAndWriteAll` permissions (see [corresponding json](./backend/data_sources/mongodb-atlas/sync/Product/rules.json)). + +> To learn more and see examples of permissions depending on a certain use case, see [Device Sync Permissions Guide](https://www.mongodb.com/docs/atlas/app-services/sync/app-builder/device-sync-permissions-guide/#std-label-flexible-sync-permissions-guide) and [Data Access Role Examples](https://www.mongodb.com/docs/atlas/app-services/rules/examples/). + +## Troubleshooting + +A great help when troubleshooting is to look at the [Application Logs](https://www.mongodb.com/docs/atlas/app-services/activity/view-logs/) in the App Services UI. + +### Permissions + +If permission is denied: + * Make sure your IP address is on the [IP Access List](https://www.mongodb.com/docs/atlas/app-services/security/network/#ip-access-list) for your App. + * Make sure you have the correct data access permissions for the collections. + * See [Set Data Access Permissions](#set-data-access-permissions) further above. + +### Removing the Local Realm Database + +Removing the local database can be useful for certain errors. When running the app, the local database will exist in the directory `mongodb-realm/`. + +From the [node directory](./node/), run: + +```sh +npm run rm-local-db +``` diff --git a/examples/node-connection-and-error/backend/README.md b/examples/node-connection-and-error/backend/README.md new file mode 100644 index 0000000000..07508bebf5 --- /dev/null +++ b/examples/node-connection-and-error/backend/README.md @@ -0,0 +1,7 @@ +# Backend + +This contains the Atlas App Services App and its configurations for the example app. + +Please see the [main README](../README.md) for all instructions. + +> To learn more about the backend file structure, see [App Configuration](https://www.mongodb.com/docs/atlas/app-services/reference/config/). diff --git a/examples/node-connection-and-error/backend/auth/custom_user_data.json b/examples/node-connection-and-error/backend/auth/custom_user_data.json new file mode 100644 index 0000000000..a82d0fb255 --- /dev/null +++ b/examples/node-connection-and-error/backend/auth/custom_user_data.json @@ -0,0 +1,3 @@ +{ + "enabled": false +} diff --git a/examples/node-connection-and-error/backend/auth/providers.json b/examples/node-connection-and-error/backend/auth/providers.json new file mode 100644 index 0000000000..2472aae8f5 --- /dev/null +++ b/examples/node-connection-and-error/backend/auth/providers.json @@ -0,0 +1,23 @@ +{ + "anon-user": { + "name": "anon-user", + "type": "anon-user", + "disabled": false + }, + "api-key": { + "name": "api-key", + "type": "api-key", + "disabled": true + }, + "local-userpass": { + "name": "local-userpass", + "type": "local-userpass", + "config": { + "autoConfirm": true, + "resetPasswordUrl": "https://", + "runConfirmationFunction": false, + "runResetFunction": false + }, + "disabled": false + } +} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/config.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/config.json new file mode 100644 index 0000000000..9913676dd9 --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/config.json @@ -0,0 +1,10 @@ +{ + "name": "mongodb-atlas", + "type": "mongodb-atlas", + "config": { + "clusterName": "Cluster0", + "readPreference": "primary", + "wireProtocolEnabled": false + }, + "version": 1 +} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json new file mode 100644 index 0000000000..ea0a3112ba --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/default_rule.json @@ -0,0 +1,30 @@ +{ + "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/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/relationships.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/relationships.json new file mode 100644 index 0000000000..c13e486f93 --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/relationships.json @@ -0,0 +1,8 @@ +{ + "products": { + "ref": "#/relationship/mongodb-atlas/sync/Product", + "source_key": "products", + "foreign_key": "_id", + "is_list": true + } +} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json new file mode 100644 index 0000000000..ca8d2610da --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/rules.json @@ -0,0 +1,19 @@ +{ + "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/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/schema.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/schema.json new file mode 100644 index 0000000000..499d3a1a0e --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Kiosk/schema.json @@ -0,0 +1,22 @@ +{ + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "products": { + "bsonType": "array", + "items": { + "bsonType": "objectId" + } + }, + "storeId": { + "bsonType": "objectId" + } + }, + "required": [ + "_id", + "storeId" + ], + "title": "Kiosk" +} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/relationships.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/relationships.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/relationships.json @@ -0,0 +1 @@ +{} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json new file mode 100644 index 0000000000..e5a9cce81e --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/rules.json @@ -0,0 +1,19 @@ +{ + "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/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/schema.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/schema.json new file mode 100644 index 0000000000..b8380b1621 --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Product/schema.json @@ -0,0 +1,28 @@ +{ + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "name": { + "bsonType": "string" + }, + "numInStock": { + "bsonType": "long" + }, + "price": { + "bsonType": "double" + }, + "storeId": { + "bsonType": "objectId" + } + }, + "required": [ + "_id", + "storeId", + "name", + "price", + "numInStock" + ], + "title": "Product" +} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/relationships.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/relationships.json new file mode 100644 index 0000000000..32ce8a1afe --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/relationships.json @@ -0,0 +1,8 @@ +{ + "kiosks": { + "ref": "#/relationship/mongodb-atlas/sync/Kiosk", + "source_key": "kiosks", + "foreign_key": "_id", + "is_list": true + } +} diff --git a/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json new file mode 100644 index 0000000000..55f8ff9763 --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/rules.json @@ -0,0 +1,19 @@ +{ + "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/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/schema.json b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/schema.json new file mode 100644 index 0000000000..0e8eaba3cb --- /dev/null +++ b/examples/node-connection-and-error/backend/data_sources/mongodb-atlas/sync/Store/schema.json @@ -0,0 +1,18 @@ +{ + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId" + }, + "kiosks": { + "bsonType": "array", + "items": { + "bsonType": "objectId" + } + } + }, + "required": [ + "_id" + ], + "title": "Store" +} diff --git a/examples/node-connection-and-error/backend/environments/development.json b/examples/node-connection-and-error/backend/environments/development.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-connection-and-error/backend/environments/development.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-connection-and-error/backend/environments/no-environment.json b/examples/node-connection-and-error/backend/environments/no-environment.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-connection-and-error/backend/environments/no-environment.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-connection-and-error/backend/environments/production.json b/examples/node-connection-and-error/backend/environments/production.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-connection-and-error/backend/environments/production.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-connection-and-error/backend/environments/qa.json b/examples/node-connection-and-error/backend/environments/qa.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-connection-and-error/backend/environments/qa.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-connection-and-error/backend/environments/testing.json b/examples/node-connection-and-error/backend/environments/testing.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-connection-and-error/backend/environments/testing.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-connection-and-error/backend/functions/config.json b/examples/node-connection-and-error/backend/functions/config.json new file mode 100644 index 0000000000..400650e335 --- /dev/null +++ b/examples/node-connection-and-error/backend/functions/config.json @@ -0,0 +1,8 @@ +[ + { + "name": "triggerClientReset", + "private": false, + "run_as_system": true, + "disable_arg_logs": true + } +] diff --git a/examples/node-connection-and-error/backend/functions/triggerClientReset.js b/examples/node-connection-and-error/backend/functions/triggerClientReset.js new file mode 100644 index 0000000000..16d8806029 --- /dev/null +++ b/examples/node-connection-and-error/backend/functions/triggerClientReset.js @@ -0,0 +1,29 @@ +/* eslint-disable */ + +/** + * Deletes the client files for the current App user which will trigger a client reset. + * + * @note + * WARNING: THIS FUNCTION EXISTS FOR DEMO PURPOSES AND SHOULD NOT BE USED IN PRODUCTION! + */ +exports = async function triggerClientReset(arg) { + // To find the name of the MongoDB service to use, see "Linked Data Sources" tab. + const serviceName = "mongodb-atlas"; + const databaseName = `__realm_sync_${context.app.id}`; + const collectionName = "clientfiles"; + + const clientFilesCollection = context + .services + .get(serviceName) + .db(databaseName) + .collection(collectionName); + + try { + return await clientFilesCollection.deleteMany({ ownerId: context.user.id }); + } catch (err) { + console.error( + `Failed to delete client file when executing \`triggerClientReset()\` for user \`${context.user.id}\`:`, + err.message + ); + } +}; diff --git a/examples/node-connection-and-error/backend/graphql/config.json b/examples/node-connection-and-error/backend/graphql/config.json new file mode 100644 index 0000000000..406b1abcde --- /dev/null +++ b/examples/node-connection-and-error/backend/graphql/config.json @@ -0,0 +1,4 @@ +{ + "use_natural_pluralization": true, + "disable_schema_introspection": false +} diff --git a/examples/node-connection-and-error/backend/http_endpoints/config.json b/examples/node-connection-and-error/backend/http_endpoints/config.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/examples/node-connection-and-error/backend/http_endpoints/config.json @@ -0,0 +1 @@ +[] diff --git a/examples/node-connection-and-error/backend/realm_config.json b/examples/node-connection-and-error/backend/realm_config.json new file mode 100644 index 0000000000..3d2b1ab86e --- /dev/null +++ b/examples/node-connection-and-error/backend/realm_config.json @@ -0,0 +1,7 @@ +{ + "config_version": 20210101, + "name": "Store-App", + "location": "US-VA", + "provider_region": "aws-us-east-1", + "deployment_model": "GLOBAL" +} diff --git a/examples/node-connection-and-error/backend/sync/config.json b/examples/node-connection-and-error/backend/sync/config.json new file mode 100644 index 0000000000..c8bbb1a285 --- /dev/null +++ b/examples/node-connection-and-error/backend/sync/config.json @@ -0,0 +1,16 @@ +{ + "type": "flexible", + "state": "enabled", + "development_mode_enabled": false, + "service_name": "mongodb-atlas", + "database_name": "sync", + "client_max_offline_days": 30, + "is_recovery_mode_disabled": false, + "permissions": { + "rules": {}, + "defaultRoles": [] + }, + "queryable_fields_names": [ + "storeId" + ] +} diff --git a/examples/node-connection-and-error/.gitignore b/examples/node-connection-and-error/node/.gitignore similarity index 100% rename from examples/node-connection-and-error/.gitignore rename to examples/node-connection-and-error/node/.gitignore diff --git a/examples/node-connection-and-error/node/README.md b/examples/node-connection-and-error/node/README.md new file mode 100644 index 0000000000..e5b2b7ba77 --- /dev/null +++ b/examples/node-connection-and-error/node/README.md @@ -0,0 +1,5 @@ +# Node + +This contains the Node code base for the example app. + +Please see the [main README](../README.md) for all instructions. diff --git a/examples/node-connection-and-error/node/package-lock.json b/examples/node-connection-and-error/node/package-lock.json new file mode 100644 index 0000000000..f3007c5e57 --- /dev/null +++ b/examples/node-connection-and-error/node/package-lock.json @@ -0,0 +1,878 @@ +{ + "name": "@realm/example-node-connection-and-error", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@realm/example-node-connection-and-error", + "version": "1.0.0", + "dependencies": { + "realm": "^12.2.0" + }, + "devDependencies": { + "@types/node": "^20.5.7", + "typescript": "^5.1.6" + } + }, + "node_modules/@types/node": { + "version": "20.5.7", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node_modules/node-abi": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", + "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/realm": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.2.0.tgz", + "integrity": "sha512-4MFmWWl5eARaU0toMGX6cjgSX53/4jLNoHyc3UN30bXoTPKw3JVAsL070GM+Ywuhl4D5ubFZOU4y+x+r9vDgew==", + "hasInstallScript": true, + "dependencies": { + "bson": "^4.7.2", + "debug": "^4.3.4", + "node-fetch": "^2.6.9", + "node-machine-id": "^1.1.12", + "prebuild-install": "^7.1.1" + }, + "peerDependencies": { + "react-native": ">=0.71.0" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + } + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@types/node": { + "version": "20.5.7", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "requires": { + "buffer": "^5.6.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "node-abi": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", + "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "realm": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.2.0.tgz", + "integrity": "sha512-4MFmWWl5eARaU0toMGX6cjgSX53/4jLNoHyc3UN30bXoTPKw3JVAsL070GM+Ywuhl4D5ubFZOU4y+x+r9vDgew==", + "requires": { + "bson": "^4.7.2", + "debug": "^4.3.4", + "node-fetch": "^2.6.9", + "node-machine-id": "^1.1.12", + "prebuild-install": "^7.1.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "typescript": { + "version": "5.2.2", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/examples/node-connection-and-error/node/package.json b/examples/node-connection-and-error/node/package.json new file mode 100644 index 0000000000..79f2e63db7 --- /dev/null +++ b/examples/node-connection-and-error/node/package.json @@ -0,0 +1,28 @@ +{ + "name": "@realm/example-node-connection-and-error", + "version": "1.0.0", + "description": "An example app using Atlas Device Sync for showcasing how to detect various changes in connection state, user state, sync errors, and product inventory.", + "main": "src/app.ts", + "scripts": { + "build": "tsc", + "start": "npm run success", + "success": "npm run build && node dist/app.js success", + "register-invalid": "npm run build && node dist/app.js register-invalid", + "register-email-in-use": "npm run build && node dist/app.js register-email-in-use", + "login-invalid": "npm run build && node dist/app.js login-invalid", + "login-non-existent-email": "npm run build && node dist/app.js login-non-existent-email", + "sync-error": "npm run build && node dist/app.js sync-error", + "client-reset": "npm run build && node dist/app.js client-reset", + "rm-local-db": "rm -rf mongodb-realm/", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "dependencies": { + "realm": "^12.2.0" + }, + "devDependencies": { + "@types/node": "^20.5.7", + "typescript": "^5.1.6" + } +} diff --git a/examples/node-connection-and-error/node/src/app.ts b/examples/node-connection-and-error/node/src/app.ts new file mode 100644 index 0000000000..ea8d2c6eef --- /dev/null +++ b/examples/node-connection-and-error/node/src/app.ts @@ -0,0 +1,180 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import Realm from "realm"; + +import { + addDummyData, + updateDummyData, + deleteDummyData, + getStore, + openRealm, +} from "./store-manager"; +import { + registerSuccessfully, + registerWithInvalidCredentials, + registerWithEmailAlreadyInUse, + logInSuccessfully, + logInWithInvalidCredentials, + logInWithNonExistentCredentials, + refreshAccessToken, +} from "./demo-auth-triggers"; +import { triggerClientReset, triggerSyncError } from "./demo-sync-triggers"; +import { logger } from "./utils/logger"; + +// To diagnose and troubleshoot errors while in development, set the log level to `debug` +// or `trace`. For production deployments, decrease the log level for improved performance. +// logLevels = ["all", "trace", "debug", "detail", "info", "warn", "error", "fatal", "off"]; +// You may import `NumericLogLevel` to get them as numbers starting from 0 (`all`). +Realm.setLogLevel("error"); +Realm.setLogger((logLevel, message) => { + const formattedMessage = `Log level: ${logLevel} - Log message: ${message}`; + if (logLevel === 'error' || logLevel === 'fatal') { + logger.error(formattedMessage); + } else { + logger.info(formattedMessage); + } +}); + +/** + * Command options for triggering various scenarios and error messages. + */ +const enum CommandOption { + /** + * Successfully register and log in a user and run the app. + */ + Success = "success", + /** + * Register a user with invalid credentials. + */ + RegisterInvalid = "register-invalid", + /** + * Register a user with an email already in use. + */ + RegisterEmailInUse = "register-email-in-use", + /** + * Log in a user with invalid credentials. + */ + LoginInvalid = "login-invalid", + /** + * Log in a user with an email that does not exist (but valid credentials). + */ + LoginNonExistentEmail = "login-non-existent-email", + /** + * Log in a user and trigger a sync error. + */ + SyncError = "sync-error", + /** + * Log in a user and trigger a client reset. + */ + ClientReset = "client-reset", +}; + +/** + * Entry point. + */ +async function main(): Promise { + let [,, action] = process.argv; + switch (action) { + case CommandOption.Success: + await successScenario(); + return; + case CommandOption.RegisterInvalid: + await registerWithInvalidCredentials(); + return exit(1); + case CommandOption.RegisterEmailInUse: + await registerWithEmailAlreadyInUse(); + return exit(1); + case CommandOption.LoginInvalid: + await logInWithInvalidCredentials(); + return exit(1); + case CommandOption.LoginNonExistentEmail: + await logInWithNonExistentCredentials(); + return exit(1); + case CommandOption.SyncError: + await syncErrorScenario(); + return; + case CommandOption.ClientReset: + await clientResetScenario(); + return; + default: + throw new Error(`Invalid option passed: ${action}.`); + } +} + +/** + * Illustrates the flow of successfully registering, logging in, + * and opening a Realm. + */ +async function logInAndOpenRealm(): Promise { + let success = await registerSuccessfully(); + if (!success) { + exit(1); + } + + success = await logInSuccessfully(); + if (!success) { + exit(1); + } + + await openRealm(); +} + +/** + * Invokes operations for modifying data and triggering listeners. + */ +async function successScenario(): Promise { + await logInAndOpenRealm(); + + // Cleaning the DB for this example before continuing. + deleteDummyData(); + addDummyData(); + + // Print a kiosk and its products. + const firstKiosk = getStore()?.kiosks[0]; + if (firstKiosk) { + logger.info("Printing the first Kiosk:"); + logger.info(JSON.stringify(firstKiosk, null, 2)); + } + + // Manually trigger specific listeners. + updateDummyData(); + await refreshAccessToken(); +} + +/** + * Triggers a sync error. + */ +async function syncErrorScenario(): Promise { + await logInAndOpenRealm(); + triggerSyncError(); +} + +/** + * Triggers a client reset. + */ +async function clientResetScenario(): Promise { + await logInAndOpenRealm(); + await triggerClientReset(); +} + +function exit(code: number): never { + return process.exit(code); +} + +main(); diff --git a/examples/node-connection-and-error/src/atlas-app-services/config.ts b/examples/node-connection-and-error/node/src/atlas-app-services/config.ts similarity index 100% rename from examples/node-connection-and-error/src/atlas-app-services/config.ts rename to examples/node-connection-and-error/node/src/atlas-app-services/config.ts diff --git a/examples/node-connection-and-error/src/atlas-app-services/getAtlasApp.ts b/examples/node-connection-and-error/node/src/atlas-app-services/getAtlasApp.ts similarity index 65% rename from examples/node-connection-and-error/src/atlas-app-services/getAtlasApp.ts rename to examples/node-connection-and-error/node/src/atlas-app-services/getAtlasApp.ts index 0395645162..0aadd67014 100644 --- a/examples/node-connection-and-error/src/atlas-app-services/getAtlasApp.ts +++ b/examples/node-connection-and-error/node/src/atlas-app-services/getAtlasApp.ts @@ -19,28 +19,23 @@ import Realm from "realm"; import { ATLAS_APP_ID } from "./config"; -import { logger } from "../logger"; let app: Realm.App | null = null; -export const getAtlasApp = function getAtlasApp() { +/** + * Get the Atlas App Services App. + * + * @returns The existing App if it already exists, otherwise it + * first instantiates a new one. + */ +export function getAtlasApp(): Realm.App { if (!app) { if (ATLAS_APP_ID === "YOUR_APP_ID") { throw new Error( "Please add your Atlas App ID to `src/atlas-app-services/config.ts`. Refer to `README.md` on how to find your ID.", ); } - app = new Realm.App({ id: ATLAS_APP_ID }); - - // Using log level "all", "trace", or "debug" is good for debugging during developing. - // Lower log levels are recommended in production for performance improvement. - // logLevels = ["all", "trace", "debug", "detail", "info", "warn", "error", "fatal", "off"]; - // You may import `NumericLogLevel` to get them as numbers starting from 0 (`all`). - Realm.setLogLevel("error"); - Realm.setLogger((logLevel, message) => { - logger.info(`Log level: ${logLevel} - Log message: ${message}`); - }); } return app; diff --git a/examples/node-connection-and-error/node/src/demo-auth-triggers.ts b/examples/node-connection-and-error/node/src/demo-auth-triggers.ts new file mode 100644 index 0000000000..3daf3139af --- /dev/null +++ b/examples/node-connection-and-error/node/src/demo-auth-triggers.ts @@ -0,0 +1,244 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import Realm, { Credentials, UserState } from "realm"; + +import { getAtlasApp } from "./atlas-app-services/getAtlasApp"; +import { getIntBetween } from "./utils/random"; +import { logger } from "./utils/logger"; + +// ===== DUMMY CREDENTIALS ===== + +type EmailPasswordCredentials = { + email: string; + password: string; +}; + +const VALID_PASSWORD = '123456'; + +const NON_EXISTENT_CREDENTIALS: EmailPasswordCredentials = { + email: 'non-existent@email.com', + password: VALID_PASSWORD, +}; + +const INVALID_CREDENTIALS: EmailPasswordCredentials = { + email: 'invalid', + password: '1', +}; + +function generateDummyEmail(): string { + return `${getIntBetween(0, 100_000)}@email.com`; +} + +function getNewValidCredentials(): EmailPasswordCredentials { + return { + email: generateDummyEmail(), + password: VALID_PASSWORD, + }; +} + +function getExistingCredentials( + registeredEmail: string, +): EmailPasswordCredentials { + return { + email: registeredEmail, + password: VALID_PASSWORD, + }; +} + +// ===== AUTH OPERATIONS ===== + +const app = getAtlasApp(); +let currentUser: Realm.User | null = null; +let mostRecentAccessToken: string | null = null; + +function resetUser(): void { + currentUser?.removeListener(handleUserEventChange); + currentUser = null; + mostRecentAccessToken = null; +} + +export function getCurrentUser(): Realm.User | null { + return currentUser; +} + +/** + * An email that has been registered but the user has not yet logged in. + */ +let pendingEmail: string | undefined; + +function getRegisteredEmail(): string { + // The user will only appear in `app.allUsers` once it has logged in + // for the first time. Between registration and login, the user status + // will be "Pending User Login" which can be seen in the App Services UI. + // If the app is restarted while the user is logged out, `app.allUsers` + // will be empty on startup. + const allUsers = Object.values(app.allUsers); + return pendingEmail ?? allUsers[allUsers.length - 1]?.profile.email!; +} + +/** + * Register a user to an Atlas App Services App. + * + * For this simplified example, the app is configured via the Atlas App Services UI + * to automatically confirm users' emails. + * + * @returns A promise that resolves to `true` if the login is successful, otherwise `false`. + */ +async function register( + credentials: EmailPasswordCredentials, + { failIfEmailInUse } = { failIfEmailInUse: true }, +): Promise { + try { + logger.info("Registering..."); + await app.emailPasswordAuth.registerUser(credentials); + logger.info("Registered."); + return true; + } catch (err: any) { + if (!failIfEmailInUse && err?.message?.includes("name already in use")) { + return true; + } + logger.error(`Error registering: ${err?.message}`); + return false; + } +}; + +export async function registerSuccessfully(): Promise { + try { + return await registerWithEmailAlreadyInUse({ failIfEmailInUse: false }); + } catch(err) { + const validCredentials = getNewValidCredentials(); + pendingEmail = validCredentials.email; + return register(validCredentials, { failIfEmailInUse: false }); + } +} + +export function registerWithInvalidCredentials(): Promise { + return register(INVALID_CREDENTIALS); +} + +export async function registerWithEmailAlreadyInUse(options = { failIfEmailInUse: true }): Promise { + const registeredEmail = getRegisteredEmail(); + if (!registeredEmail) { + throw new Error("You need to register a user first."); + } + return register(getExistingCredentials(registeredEmail), options); +} + +/** + * Log in a user to an Atlas App Services App. + * + * Access tokens are created once a user logs in. These tokens are refreshed + * automatically by the SDK when needed. Manually refreshing the token is only + * required if requests are sent outside of the SDK. If that's the case, see: + * {@link https://www.mongodb.com/docs/realm/sdk/node/examples/authenticate-users/#get-a-user-access-token}. + * + * By default, refresh tokens expire 60 days after they are issued. You can configure this + * time for your App's refresh tokens to be anywhere between 30 minutes and 180 days. See: + * {@link https://www.mongodb.com/docs/atlas/app-services/users/sessions/#configure-refresh-token-expiration}. + * + * @returns A promise that resolves to `true` if the login is successful, otherwise `false`. + */ +async function logIn(credentials: EmailPasswordCredentials): Promise { + // If there is already a logged in user, there is no need to reauthenticate. + if (currentUser) { + return true; + } + + try { + logger.info("Logging in..."); + // The credentials here can be substituted using a JWT or another preferred method. + currentUser = await app.logIn(Credentials.emailPassword(credentials)); + mostRecentAccessToken = currentUser.accessToken; + logger.info("Logged in."); + + // Listen for changes to user-related events. + currentUser.addListener(handleUserEventChange); + return true; + } catch (err: any) { + logger.error(`Error logging in: ${err?.message}`); + return false; + } +} + +export function logInSuccessfully(): Promise { + const registeredEmail = getRegisteredEmail(); + if (!registeredEmail) { + throw new Error("You need to register a user first."); + } + return logIn(getExistingCredentials(registeredEmail)); +} + +export function logInWithInvalidCredentials(): Promise { + return logIn(INVALID_CREDENTIALS); +} + +export function logInWithNonExistentCredentials(): Promise { + return logIn(NON_EXISTENT_CREDENTIALS); +} + +export async function logOut(): Promise { + if (currentUser) { + logger.info("Logging out..."); + await currentUser.logOut(); + // The `currentUser` variable is being set to `null` in the user listener. + } +} + +/** + * Trigger the user event listener by refreshing the custom user data + * and thereby the access token. + */ +export async function refreshAccessToken(): Promise { + logger.info("Triggering refresh of access token..."); + await currentUser?.refreshCustomData(); +} + +/** + * The user listener - Will be invoked on various user related events including + * refresh of auth token, refresh token, custom user data, removal, and logout. + */ +export function handleUserEventChange(): void { + if (currentUser) { + // As the SDK currently does not provide any arguments to this callback, to be + // able to detect whether a token has been refreshed we store the most recent + // access token in a variable and compare it against the current one. + if (mostRecentAccessToken !== currentUser.accessToken) { + logger.info("New access token."); + mostRecentAccessToken = currentUser.accessToken; + } + + switch (currentUser.state) { + case UserState.LoggedIn: + logger.info(`User (id: ${currentUser.id}) has been authenticated.`); + break; + case UserState.LoggedOut: + logger.info(`User (id: ${currentUser.id}) has been logged out.`); + resetUser(); + break; + case UserState.Removed: + logger.info(`User (id: ${currentUser.id}) has been removed from the app.`); + resetUser(); + break; + default: + // Should not be reachable. + logger.error(`Unknown user state: ${currentUser.state}.`); + break; + } + } +} diff --git a/examples/node-connection-and-error/node/src/demo-sync-triggers.ts b/examples/node-connection-and-error/node/src/demo-sync-triggers.ts new file mode 100644 index 0000000000..312d013d18 --- /dev/null +++ b/examples/node-connection-and-error/node/src/demo-sync-triggers.ts @@ -0,0 +1,147 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import type Realm from "realm"; +import { BSON, ConnectionState, SyncError } from "realm"; + +import { Store } from "./models/Store"; +import { getCurrentUser } from "./demo-auth-triggers"; +import { getRealm } from "./store-manager"; +import { logger } from "./utils/logger"; + +let isConnected = false; + +/** + * The connection listener - Will be invoked when the the underlying sync + * session changes its connection state. + * + * @note + * Be aware of that there may be a delay from the time of actual disconnect + * until this listener is invoked. + */ +export function handleConnectionChange(newState: ConnectionState, oldState: ConnectionState): void { + const connecting = newState === ConnectionState.Connecting; + const connected = newState === ConnectionState.Connected; + const disconnected = oldState === ConnectionState.Connected && newState === ConnectionState.Disconnected; + const failedReconnecting = oldState === ConnectionState.Connecting && newState === ConnectionState.Disconnected; + + if (connecting) { + logger.info("Connecting..."); + } else if (connected) { + logger.info("Connected."); + } else if (disconnected) { + logger.info("Disconnected."); + + // At this point, the `newState` is `ConnectionState.Disconnected`. Automatic retries + // will start and the state will alternate in the following way for the period where + // there is NO network connection: + // (1) oldState: ConnectionState.Disconnected, newState: ConnectionState.Connecting + // (2) oldState: ConnectionState.Connecting, newState: ConnectionState.Disconnected + // Calling `App.Sync.reconnect()` is not needed due to automatic retries. + } else /* failedReconnecting */ { + logger.info("Failed to reconnect."); + } + + isConnected = connected; +} + +/** + * Trigger the connection listener by reconnecting to the sync session. + */ +function reconnect(): void { + getRealm()?.syncSession?.resume(); +} + +/** + * Trigger the connection listener by disconnecting from the sync session. + */ +function disconnect(): void { + getRealm()?.syncSession?.pause(); +} + +/** + * The sync error listener - Will be invoked when various synchronization errors occur. + * + * To trigger, for instance, a session level sync error, you may modify the Document + * Permissions in Atlas App Services to NOT allow `Delete`, then rerun this app since + * this example will always delete all items in the database at startup. + * For how to modify the rules and permissions, see: + * {@link https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions}. + * + * For detailed error codes, see {@link https://github.com/realm/realm-core/blob/master/doc/protocol.md#error-codes}. + * Examples: + * - 202 (Access token expired) + * - 225 (Invalid schema change) + */ +export function handleSyncError(session: Realm.App.Sync.SyncSession, error: SyncError): void { + logger.error(error); +} + +/** + * Trigger the sync error listener by trying to create a `Store` that + * is outside of the query filter subscribed to. Since we subscribed + * to the store with a given ID (see `openRealm/()`), attempting to + * create one with a different ID will generate a sync error. + * + * @note + * You can also trigger sync errors by modifying the permissions of + * fields and/or collections, and then try to perform non-permitted + * operations from the client. To read more about permissions, see: + * {@link https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions} + */ +export function triggerSyncError(): void { + logger.info("Triggering sync error..."); + const realm = getRealm(); + realm?.write(() => { + const NON_SUBSCRIBED_STORE_ID = new BSON.ObjectId(); + realm.create(Store, {_id: NON_SUBSCRIBED_STORE_ID}); + }); +} + +/** + * The pre-client reset listener - Will be invoked before sync initiates + * a client reset. + */ +export function handlePreClientReset(localRealm: Realm): void { + logger.info("Initiating client reset..."); +} + +/** + * The post-client reset listener - Will be invoked after a client reset. + */ +export function handlePostClientReset(localRealm: Realm, remoteRealm: Realm): void { + logger.info("Client has been reset."); +} + +/** + * Trigger the client reset listeners by calling a custom Atlas Function + * (see `backend/functions/triggerClientReset.js`) that deletes the client + * files for the current user. + * + * @note + * This should NOT be used in production. + */ +export async function triggerClientReset(): Promise { + logger.info("Triggering client reset ..."); + await getCurrentUser()?.functions.triggerClientReset(); + // Once the client tries to reconnect, the client reset will be triggered. + if (isConnected) { + disconnect(); + } + reconnect(); +} diff --git a/examples/node-connection-and-error/src/models/Kiosk.ts b/examples/node-connection-and-error/node/src/models/Kiosk.ts similarity index 100% rename from examples/node-connection-and-error/src/models/Kiosk.ts rename to examples/node-connection-and-error/node/src/models/Kiosk.ts diff --git a/examples/node-connection-and-error/src/models/Product.ts b/examples/node-connection-and-error/node/src/models/Product.ts similarity index 61% rename from examples/node-connection-and-error/src/models/Product.ts rename to examples/node-connection-and-error/node/src/models/Product.ts index 480c55bc7d..c3e6641948 100644 --- a/examples/node-connection-and-error/src/models/Product.ts +++ b/examples/node-connection-and-error/node/src/models/Product.ts @@ -18,10 +18,14 @@ import Realm, { BSON, ObjectSchema } from "realm"; +import { getIntBetween } from "../utils/random"; + /** * Current information and inventory about a type of product in a particular store. - * (This is simplified to refer to a complete product (e.g. a sandwich, rather than - * e.g. bread, cheese, lettuce, etc.) + * + * @note + * This is simplified to refer to a complete product (e.g. a complete sandwich, + * rather than individual pieces such as bread, cheese, lettuce, etc.). */ export class Product extends Realm.Object { _id!: BSON.ObjectId; @@ -42,3 +46,36 @@ export class Product extends Realm.Object { }, }; } + +/** + * A dummy list of product names to use when creating a product. + */ +const productNames = [ + "Fresh Salad", + "Hoagie", + "Burrito", + "Quesadilla", + "Bagel", + "Panini", + "Pizza", + "Chicken Sandwich", + "Fish Soup", + "Chicken Soup", + "Noodle Soup", + "Blueberry Muffin", + "Chocolate Chip Muffin", + "Brownie", + "Coke", + "Diet Coke", + "Strawberry Milkshake", + "Chocolate Milkshake", + "Vanilla Milkshake", + "Iced Coffee", +] as const; + +/** + * @returns one of the valid product names. + */ +export function getRandomProductName() { + return productNames[getIntBetween(0, productNames.length)]; +} \ No newline at end of file diff --git a/examples/node-connection-and-error/src/models/Store.ts b/examples/node-connection-and-error/node/src/models/Store.ts similarity index 100% rename from examples/node-connection-and-error/src/models/Store.ts rename to examples/node-connection-and-error/node/src/models/Store.ts diff --git a/examples/node-connection-and-error/node/src/store-manager.ts b/examples/node-connection-and-error/node/src/store-manager.ts new file mode 100644 index 0000000000..604b3d546a --- /dev/null +++ b/examples/node-connection-and-error/node/src/store-manager.ts @@ -0,0 +1,240 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import process from "node:process"; +import Realm, { + BSON, + ClientResetMode, + CollectionChangeCallback, + ConfigurationWithSync, + OpenRealmBehaviorType, +} from "realm"; + +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"; +import { getCurrentUser } from "./demo-auth-triggers"; +import { getFloatBetween, getIntBetween } from "./utils/random"; +import { handleConnectionChange, handlePostClientReset, handlePreClientReset, handleSyncError } from "./demo-sync-triggers"; +import { logger } from "./utils/logger"; + +let realm: Realm | null = null; + +export function getRealm(): Realm | null { + return realm; +} + +/** + * Configures and opens the synced realm. + */ +export async function openRealm(): Promise { + try { + const currentUser = getCurrentUser(); + if (!currentUser) { + throw new Error("The user needs to be logged in before the synced Realm can be opened."); + } + + const config: ConfigurationWithSync = { + schema: [Store, Kiosk, Product], + sync: { + user: currentUser, + flexible: true, + // To sync a preferred subset of data to the device, we only subscribe to + // the kiosks and products in a particular store. For this demo, we have + // defined the specific store ID in `app/atlas-app-services/config.ts`. + // When adding subscriptions, best practice is to name each subscription + // for better managing removal of them. To read more about subscriptions, see: + // https://www.mongodb.com/docs/realm/sdk/node/examples/flexible-sync/ + initialSubscriptions: { + update: (mutableSubs, realm) => { + // 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" }, + ); + }, + }, + // The `ClientResetMode.RecoverOrDiscardUnsyncedChanges` will download a fresh copy + // from the server if recovery of unsynced changes is not possible. For read-only + // clients, `ClientResetMode.DiscardUnsyncedChanges` is suitable. + clientReset: { + mode: ClientResetMode.RecoverOrDiscardUnsyncedChanges, + onBefore: handlePreClientReset, + onAfter: handlePostClientReset, + }, + onError: handleSyncError, + // We can specify the behavior when opening a Realm for the first time + // (`newRealmFileBehavior`) and for subsequent ones (`existingRealmFileBehavior`). + // If the user has logged in at least 1 time before, the Realm and its data will + // exist on disk and can be opened even when offline. We can either (a) open the + // Realm immediately (or first create a new empty Realm file if it does not + // exist before opening it) and sync the data to the device in the background + // (`OpenRealmBehaviorType.OpenImmediately`), or (b) wait for any non-synced + // data to be fully downloaded (`OpenRealmBehaviorType.DownloadBeforeOpen`). + newRealmFileBehavior: { + type: OpenRealmBehaviorType.OpenImmediately, + }, + existingRealmFileBehavior: { + type: OpenRealmBehaviorType.OpenImmediately, + }, + }, + }; + + logger.info("Opening realm..."); + realm = await Realm.open(config); + logger.info("Realm opened."); + + // Listen for changes to the connection. + realm.syncSession?.addConnectionNotification(handleConnectionChange); + + // Listen for changes to the products at the given store ID. + realm.objects(Product).filtered("storeId = $0", SYNC_STORE_ID).addListener(handleProductsChange); + return realm; + } catch (err: any) { + logger.error(`Error opening the realm: ${err?.message}`); + throw err; + } +} + +function closeRealm(): void { + if (realm && !realm.isClosed) { + // Explicitly removing the connection listener is not needed + // if you intend for it to live throughout the session. + realm.syncSession?.removeConnectionNotification(handleConnectionChange); + + logger.info("Closing the realm..."); + realm.close(); + logger.info("Realm closed."); + } + realm = null; +} + +/** + * The products collection listener - Will be invoked when the listener is added + * and whenever an object in the collection is deleted, inserted, or modified. + * + * @note + * Always handle potential deletions first. + */ +const handleProductsChange: CollectionChangeCallback< + Product, + [number, Product] +> = (collection, changes) => { + if (changes.deletions.length) { + logger.info(`Removed ${changes.deletions.length} product(s).`); + } + for (const insertedIndex of changes.insertions) { + logger.info(`Product inserted: ${collection[insertedIndex].name}`); + } + for (const modifiedIndex of changes.newModifications) { + logger.info(`Product modified: ${collection[modifiedIndex].name}`); + } +} + +export function getStore() { + return realm?.objects(Store).filtered("_id = $0", SYNC_STORE_ID)[0]; + // or: .objects(Store.schema.name, ..) +} + +function getKiosks() { + return realm ? realm.objects(Kiosk).filtered("storeId = $0", SYNC_STORE_ID) : []; +} + +function getProducts() { + return realm ? realm.objects(Product).filtered("storeId = $0", SYNC_STORE_ID) : []; +} + +function addProducts() { + realm?.write(() => { + const NUM_PRODUCTS = 5; + for (let i = 1; i <= NUM_PRODUCTS; i++) { + realm?.create(Product, { + _id: new BSON.ObjectId(), + storeId: SYNC_STORE_ID, + name: getRandomProductName(), + price: parseFloat(getFloatBetween(3, 15).toFixed(2)), + numInStock: getIntBetween(0, 100), + }); + } + }); +} + +function addKiosks() { + realm?.write(() => { + const NUM_KIOSKS = 3; + for (let i = 1; i <= NUM_KIOSKS; i++) { + realm?.create(Kiosk, { + _id: new BSON.ObjectId(), + storeId: SYNC_STORE_ID, + products: [...getProducts()], + }); + } + }); +} + +function addStore() { + realm?.write(() => { + realm?.create(Store, { + _id: SYNC_STORE_ID, + kiosks: [...getKiosks()], + }); + }); +} + +export function addDummyData() { + addProducts(); + addKiosks(); + addStore(); +} + +export function updateDummyData() { + const products = getProducts(); + // Updating products one-by-one (separate `write`s) to simulate + // updates occurring in different batches. + for (const product of products) { + realm?.write(() => { + product.numInStock = getIntBetween(0, 100); + }); + } +} + +export function deleteDummyData() { + realm?.write(() => { + logger.info("Deleting dummy data..."); + realm?.deleteAll(); + }); +} + +function handleExit(code: number): void { + closeRealm(); + logger.info(`Exiting with code ${code}.`); +} + +process.on("exit", handleExit); +process.on("SIGINT", process.exit); diff --git a/examples/node-connection-and-error/node/src/utils/logger.ts b/examples/node-connection-and-error/node/src/utils/logger.ts new file mode 100644 index 0000000000..2c1db9cdc8 --- /dev/null +++ b/examples/node-connection-and-error/node/src/utils/logger.ts @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import type { SyncError } from "realm"; + +/** + * Logger - This is meant to be replaced with a preferred logging + * implementation or service. + */ +export const logger = { + info(message: string): void { + console.log(prefixWithDate(message)); + }, + error(error: string | SyncError): void { + const message = typeof error === 'string' ? error : formatErrorMessage(error); + console.error(prefixWithDate(message)); + }, +}; + +/** + * @returns The message prefixed with the current local date and timestamp. + */ +function prefixWithDate(message: string): string { + return `${new Date().toLocaleString()} | ${message}`; +} + +/** + * @returns A formatted error message with its name, message, and reason. + * + * @note + * To print the entire message as a JSON string you may use e.g. + * `JSON.stringify(error, null, 2)` if needed. + */ +function formatErrorMessage(error: SyncError): string { + return ( + `${error.name}:` + + `\n Message: ${error.message}.` + + `\n Reason: ${error.reason}` + + `\n Code: ${error.code}` + ); +} diff --git a/examples/node-connection-and-error/src/logger.ts b/examples/node-connection-and-error/node/src/utils/random.ts similarity index 67% rename from examples/node-connection-and-error/src/logger.ts rename to examples/node-connection-and-error/node/src/utils/random.ts index 21a13458ea..e2dc6aa5ff 100644 --- a/examples/node-connection-and-error/src/logger.ts +++ b/examples/node-connection-and-error/node/src/utils/random.ts @@ -17,13 +17,15 @@ //////////////////////////////////////////////////////////////////////////// /** - * Logger - This is meant to be replaced with a preferred logging implementation. + * @returns A floating point number between `min` and `max`. */ -export const logger = { - info(message: string) { - console.info(new Date().toLocaleString(), '|', message); - }, - error(message: string) { - console.error(new Date().toLocaleString(), '|', message); - }, -}; +export function getFloatBetween(min: number, max: number): number { + return min + Math.random() * (max - min); +} + +/** + * @returns An integer-representing number between `min` and `max`. + */ +export function getIntBetween(min: number, max: number): number { + return Math.floor(getFloatBetween(min, max)); +} diff --git a/examples/node-connection-and-error/tsconfig.json b/examples/node-connection-and-error/node/tsconfig.json similarity index 96% rename from examples/node-connection-and-error/tsconfig.json rename to examples/node-connection-and-error/node/tsconfig.json index 45a64fa5b1..97968cc677 100644 --- a/examples/node-connection-and-error/tsconfig.json +++ b/examples/node-connection-and-error/node/tsconfig.json @@ -7,7 +7,7 @@ /* Modules */ "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./", /* Specify the root folder within your source files. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "resolveJsonModule": true, /* Enable importing .json files. */ diff --git a/examples/node-connection-and-error/package.json b/examples/node-connection-and-error/package.json deleted file mode 100644 index 1bff9180c1..0000000000 --- a/examples/node-connection-and-error/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@realm/node-connection-and-error", - "version": "1.0.0", - "description": "A skeleton app to be used as a reference for how to use the Realm Node.js SDK specifically around detecting various changes in e.g. connection state, user state, and sync errors", - "main": "src/index.ts", - "scripts": { - "build": "tsc", - "start": "npm run build && node dist/src/index.js", - "rm-local-db": "rm -rf mongodb-realm/", - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "dependencies": { - "realm": "^12.0.0" - }, - "devDependencies": { - "@types/node": "^20.5.7", - "typescript": "^5.1.6" - } -} diff --git a/examples/node-connection-and-error/src/index.ts b/examples/node-connection-and-error/src/index.ts deleted file mode 100644 index 620c73f42f..0000000000 --- a/examples/node-connection-and-error/src/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2023 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -import { - register, - logIn, - logOut, - openRealm, - triggerConnectionChange, - triggerUserEventChange, -} from "./realm-auth"; -import { addDummyData, updateDummyData, deleteDummyData, getStore } from "./realm-query"; - -const exampleEmail = "john@doe.com"; -const examplePassword = "123456"; - -/** - * Illustrates the flow of using a synced Realm. - */ -async function main(): Promise { - let success = await register(exampleEmail, examplePassword); - if (!success) { - return; - } - - success = await logIn(exampleEmail, examplePassword); - if (!success) { - return; - } - - await openRealm(); - - // Cleaning the DB for this example before continuing. - deleteDummyData(); - addDummyData(); - updateDummyData(); - - // Print a kiosk and its products. - const store = getStore(); - const firstKiosk = store?.kiosks[0]; - if (firstKiosk) { - console.log("Printing the first Kiosk:"); - console.log(JSON.stringify(firstKiosk, null, 2)); - } - - // Manually trigger specific listeners. - const TRIGGER_LISTENER_AFTER_MS = 4000; - triggerUserEventChange(TRIGGER_LISTENER_AFTER_MS); - triggerConnectionChange(TRIGGER_LISTENER_AFTER_MS * 2, TRIGGER_LISTENER_AFTER_MS * 4); -} - -main(); diff --git a/examples/node-connection-and-error/src/realm-auth.ts b/examples/node-connection-and-error/src/realm-auth.ts deleted file mode 100644 index 2e7cb734c0..0000000000 --- a/examples/node-connection-and-error/src/realm-auth.ts +++ /dev/null @@ -1,333 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2023 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -import process from "node:process"; -import Realm, { - ClientResetMode, - CollectionChangeCallback, - ConfigurationWithSync, - ConnectionState, - Credentials, - SyncError, - UserState, -} from "realm"; - -import { SYNC_STORE_ID } from "./atlas-app-services/config"; -import { getAtlasApp } from "./atlas-app-services/getAtlasApp"; -import { Store } from "./models/Store"; -import { Kiosk } from "./models/Kiosk"; -import { Product } from "./models/Product"; -import { logger } from "./logger"; - -const app = getAtlasApp(); -let currentUser: Realm.User | null = null; -let originalAccessToken: string | null = null; -let realm: Realm | null = null; - -// Exported for use by `./realm-query.ts` -export function getRealm(): Realm | null { - return realm; -} - -function resetUser(): void { - currentUser?.removeAllListeners(); - currentUser = null; - originalAccessToken = null; -} - -/** - * The user listener - Will be invoked on various user related events including - * refresh of auth token, refresh token, custom user data, removal, and logout. - */ -function handleUserEventChange(): void { - if (currentUser) { - // Currently we don't provide any arguments to this callback but we have opened - // a ticket for this (see https://github.com/realm/realm-core/issues/6454). To - // detect that a token has been refreshed (which can also be manually triggered - // by `await user.refreshCustomData()`), the original access token can be saved - // to a variable and compared against the current one. - if (originalAccessToken !== currentUser.accessToken) { - logger.info("Refreshed access token."); - originalAccessToken = currentUser.accessToken; - } - - switch (currentUser.state) { - case UserState.LoggedIn: - logger.info(`User (id: ${currentUser.id}) has been authenticated.`); - break; - case UserState.LoggedOut: - logger.info(`User (id: ${currentUser.id}) has been logged out.`); - resetUser(); - break; - case UserState.Removed: - logger.info(`User (id: ${currentUser.id}) has been removed from the app.`); - resetUser(); - break; - default: - // Should not be reachable. - break; - } - } -} - -/** - * Trigger the user event listener by refreshing the access token. - */ -export function triggerUserEventChange(triggerAfterMs: number) { - logger.info(`Triggering refresh of access token in ${triggerAfterMs / 1000} sec...`); - setTimeout(async () => await currentUser?.refreshCustomData(), triggerAfterMs); -} - -/** - * The connection listener - Will be invoked when the the underlying sync - * session changes its connection state. - */ -function handleConnectionChange(newState: ConnectionState, oldState: ConnectionState): void { - const connecting = newState === ConnectionState.Connecting; - const connected = newState === ConnectionState.Connected; - const disconnected = oldState === ConnectionState.Connected && newState === ConnectionState.Disconnected; - const failedReconnecting = oldState === ConnectionState.Connecting && newState === ConnectionState.Disconnected; - - if (connecting) { - logger.info("Connecting..."); - } else if (connected) { - logger.info("Connected."); - } else if (disconnected) { - logger.info("Disconnected."); - - // At this point, the `newState` is `ConnectionState.Disconnected`. Automatic retries - // will start and the state will alternate in the following way for the period where - // there is NO network connection: - // (1) oldState: ConnectionState.Disconnected, newState: ConnectionState.Connecting - // (2) oldState: ConnectionState.Connecting, newState: ConnectionState.Disconnected - - // Calling `App.Sync.reconnect()` is not needed due to automatic retries. - - // Be aware of that there may be a delay from the time of actual disconnect until this - // listener is invoked. - } else /* failedReconnecting */ { - logger.info("Failed to reconnect."); - } -} - -/** - * Trigger the connection listener by disconnecting and reconnecting. - */ -export function triggerConnectionChange(disconnectAfterMs: number, reconnectAfterMs: number) { - if (reconnectAfterMs < disconnectAfterMs) { - throw new Error("Reconnecting must be performed after disconnecting."); - } - - logger.info(`Triggering disconnection and reconnection in ${disconnectAfterMs / 1000} sec...`); - setTimeout(() => realm?.syncSession?.pause(), disconnectAfterMs); - setTimeout(() => realm?.syncSession?.resume(), reconnectAfterMs); -} - -/** - * The sync error listener - Will be invoked when various synchronization errors occur. - * - * To trigger, for instance, a session level sync error, you may modify the Document - * Permissions in Atlas App Services to NOT allow `Delete`, then rerun this app since - * this example will always delete all items in the database at startup. - * For how to modify the rules and permissions, see: - * {@link https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions}. - * - * For detailed error codes, see {@link https://github.com/realm/realm-core/blob/master/doc/protocol.md#error-codes}. - * Examples: - * - 202 (Access token expired) - * - 225 (Invalid schema change) - */ -function handleSyncError(session: Realm.App.Sync.SyncSession, error: SyncError): void { - if (error.code >= 100 && error.code < 200) { - logger.error(`Connection level and protocol error: ${error.message}. ${JSON.stringify(error)}`); - } else if (error.code >= 200 && error.code < 300) { - logger.error(`Session level error: ${error.message}. ${JSON.stringify(error)}`); - } else { - // Should not be reachable. - logger.error(`Unexpected error code: ${error.code}. ${JSON.stringify(error)}`); - } -} - -function handlePreClientReset(localRealm: Realm): void { - logger.info("Initiating client reset..."); -} - -function handlePostClientReset(localRealm: Realm, remoteRealm: Realm) { - logger.info("Client has been reset."); -} - -/** - * The products collection listener - Will be invoked when the listener is added - * and whenever an object in the collection is deleted, inserted, or modified. - * (Always handle potential deletions first.) - */ -const handleProductsChange: CollectionChangeCallback = (products, changes) => { - logger.info("Products changed."); -} - -/** - * Register a user to an Atlas App Services App. - * - * For this simplified example, the app is configured via the Atlas App Services UI - * to automatically confirm users' emails. - */ -export async function register(email: string, password: string): Promise { - try { - logger.info("Registering..."); - await app.emailPasswordAuth.registerUser({ email, password }); - logger.info("Registered."); - return true; - } catch (err: any) { - if (err?.message?.includes("name already in use")) { - logger.info("Already registered."); - return true; - } - logger.error(`Error registering: ${err?.message}`); - return false; - } -}; - -/** - * Log in a user to an Atlas App Services App. - * - * Access tokens are created once a user logs in. These tokens are refreshed - * automatically by the SDK when needed. Manually refreshing the token is only - * required if requests are sent outside of the SDK. If that's the case, see: - * {@link https://www.mongodb.com/docs/realm/sdk/node/examples/authenticate-users/#get-a-user-access-token}. - * - * By default, refresh tokens expire 60 days after they are issued. You can configure this - * time for your App's refresh tokens to be anywhere between 30 minutes and 180 days. See: - * {@link https://www.mongodb.com/docs/atlas/app-services/users/sessions/#configure-refresh-token-expiration}. - */ -export async function logIn(email: string, password: string): Promise { - // If there is already a logged in user, there is no need to reauthenticate. - if (currentUser) { - return true; - } - - try { - logger.info("Logging in..."); - // The credentials here can be substituted using a JWT or another preferred method. - currentUser = await app.logIn(Credentials.emailPassword(email, password)); - originalAccessToken = currentUser.accessToken; - logger.info("Logged in."); - - // Listen for changes to user-related events. - currentUser.addListener(handleUserEventChange); - return true; - } catch (err: any) { - logger.error(`Error logging in: ${err?.message}`); - return false; - } -} - -export async function logOut(): Promise { - if (currentUser) { - logger.info("Logging out..."); - await currentUser.logOut(); - // The `currentUser` variable is being set to `null` in the user listener. - } -} - -/** - * Configure and open the synced realm. - */ -export async function openRealm(): Promise { - try { - if (!currentUser) { - throw new Error("The user needs to be logged in before the synced Realm can be opened."); - } - - const config: ConfigurationWithSync = { - schema: [Store, Kiosk, Product], - sync: { - user: currentUser, - // To read more about flexible sync and subscriptions, see: - // https://www.mongodb.com/docs/realm/sdk/node/examples/flexible-sync/ - flexible: true, - initialSubscriptions: { - // When adding subscriptions, best practice is to name each subscription - // for better managing removal of them. - update: (mutableSubs: Realm.App.Sync.MutableSubscriptionSet, realm: Realm) => { - // 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" }, - ); - }, - }, - // The `ClientResetMode.RecoverOrDiscardUnsyncedChanges` will download a fresh copy - // from the server if recovery of unsynced changes is not possible. For read-only - // clients, `ClientResetMode.DiscardUnsyncedChanges` is suitable. - // Regarding manual client resets, the deprecated `Realm.App.Sync.initiateClientReset()` - // was meant for use only when the `clientReset` property on the sync configuration is - // set to `ClientResetMode.Manual`. To read more about manual client reset data recovery, - // see: https://www.mongodb.com/docs/realm/sdk/node/advanced/client-reset-data-recovery/ - clientReset: { - mode: ClientResetMode.RecoverOrDiscardUnsyncedChanges, - onBefore: handlePreClientReset, - onAfter: handlePostClientReset, - }, - // The old property for the error callback was called `error`, please use `onError`. - onError: handleSyncError, - }, - }; - - logger.info("Opening realm..."); - realm = await Realm.open(config); - logger.info("Realm opened."); - - // Listen for changes to the connection. (Explicitly removing the connection - // listener is not needed if you intend for it to live throughout the session.) - realm.syncSession?.addConnectionNotification(handleConnectionChange); - - // Listen for changes to the products at the given store ID. - realm.objects(Product).filtered("storeId = $0", SYNC_STORE_ID).addListener(handleProductsChange); - return realm; - } catch (err: any) { - logger.error(`Error opening the realm: ${err?.message}`); - throw err; - } -} - -function closeRealm(): void { - if (realm && !realm.isClosed) { - logger.info("Closing the realm..."); - realm.close(); - logger.info("Realm closed."); - } - realm = null; -} - -function handleExit(code: number): void { - closeRealm(); - logger.info(`Exiting with code ${code}.`); -} - -process.on("exit", handleExit); -process.on("SIGINT", process.exit); diff --git a/examples/node-connection-and-error/src/realm-query.ts b/examples/node-connection-and-error/src/realm-query.ts deleted file mode 100644 index 8b83bcabf4..0000000000 --- a/examples/node-connection-and-error/src/realm-query.ts +++ /dev/null @@ -1,112 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2023 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -import { BSON } from "realm"; - -import { SYNC_STORE_ID } from "./atlas-app-services/config"; -import { Store } from "./models/Store"; -import { Kiosk } from "./models/Kiosk"; -import { Product } from "./models/Product"; -import { getRealm } from "./realm-auth"; - -export function getStore() { - return getRealm()?.objects(Store).filtered("_id = $0", SYNC_STORE_ID)[0]; - // or: .objects(Store.schema.name, ..) -} - -function getKiosks() { - const realm = getRealm(); - return realm ? realm.objects(Kiosk).filtered("storeId = $0", SYNC_STORE_ID) : []; -} - -function getProducts() { - const realm = getRealm(); - return realm ? realm.objects(Product).filtered("storeId = $0", SYNC_STORE_ID) : []; -} - -function addProducts() { - const realm = getRealm(); - realm?.write(() => { - const NUM_PRODUCTS = 10; - for (let i = 1; i <= NUM_PRODUCTS; i++) { - const randomPrice = parseFloat((5 + Math.random() * 10).toFixed(2)); - realm.create(Product, { // Or: `realm.create(Product.schema.name, ..)` - _id: new BSON.ObjectId(), - storeId: SYNC_STORE_ID, - name: `product${i}`, - price: randomPrice, - numInStock: NUM_PRODUCTS, - }); - } - }); -} - -function addKiosks() { - const realm = getRealm(); - const products = getProducts(); - realm?.write(() => { - const NUM_KIOSKS = 10; - for (let i = 1; i <= NUM_KIOSKS; i++) { - realm.create(Kiosk.schema.name, { - _id: new BSON.ObjectId(), - storeId: SYNC_STORE_ID, - products, - }); - } - }); -} - -function addStore() { - const realm = getRealm(); - const kiosks = getKiosks(); - realm?.write(() => { - realm.create(Store.schema.name, { - _id: SYNC_STORE_ID, - kiosks, - }); - }); -} - -export function addDummyData() { - addProducts(); - addKiosks(); - addStore(); -} - -export function updateDummyData() { - const realm = getRealm(); - const products = getProducts(); - // Updating products one-by-one (separate `write`s) to simulate - // updates occurring in different batches. - for (const product of products) { - realm?.write(() => { - // Decrease the `numInStock` by 0, 1, 2, or 3 - const decrease = Math.round(Math.random() * 3); - product.numInStock = Math.max(0, product.numInStock - decrease); - }); - } -} - -export function deleteDummyData() { - const realm = getRealm(); - if (realm) { - realm.write(() => { - realm.deleteAll(); - }); - } -} diff --git a/examples/node-telemetry/.gitignore b/examples/node-telemetry/.gitignore deleted file mode 100644 index cffe1af4ed..0000000000 --- a/examples/node-telemetry/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# TS-generated output files. - dist/ - - # Dependencies. - node_modules/ - - # Local MongoDB Realm database. - mongodb-realm/ \ No newline at end of file diff --git a/examples/node-telemetry/README.md b/examples/node-telemetry/README.md index 1061b50357..c82673bc6b 100644 --- a/examples/node-telemetry/README.md +++ b/examples/node-telemetry/README.md @@ -1,26 +1,28 @@ -# Telemetry demo app using Data Ingest and Atlas Charts +# Telemetry Demo App Using Data Ingest and Atlas Charts -A [Node.js](https://nodejs.org) application to demonstrate how to use [Atlas Device Sync](https://www.mongodb.com/atlas/app-services/device-sync) and the [Realm Node.js SDK](https://www.mongodb.com/docs/realm/sdk/node/) to read sensor data, store the data in [Atlas](https://www.mongodb.com/atlas), and visualize it with [Atlas Charts](https://www.mongodb.com/products/charts). [Data Ingest](https://www.mongodb.com/docs/atlas/app-services/sync/configure/sync-settings/#data-ingest) is enabled since this app uses insert-only workloads. +A [Node.js](https://nodejs.org) application to demonstrate how to use [MongoDB's Atlas Device SDK for Node.js](https://www.mongodb.com/docs/realm/sdk/node/) (fka Realm) to read sensor data, store the data in [Atlas](https://www.mongodb.com/atlas), and visualize it with [Atlas Charts](https://www.mongodb.com/products/charts). [Data Ingest](https://www.mongodb.com/docs/atlas/app-services/sync/configure/sync-settings/#data-ingest) is enabled since this app uses insert-only workloads. ## Project Structure The following shows the project structure and the most relevant files. +> To learn more about the backend file structure, see [App Configuration](https://www.mongodb.com/docs/atlas/app-services/reference/config/). + ``` -├── src -│ ├── models -│ │ ├── machine_info.ts -│ │ └── sensor_reading.ts -│ ├── app.ts -│ └── config.ts -├── package.json -└── README.md +├── backend - App Services App +│ └── (see link above) +├── node - Node App +│ ├── src +│ │ ├── atlas-app-services +│ │ │ └── config.ts - Add App ID +│ │ ├── models - Data model +│ │ │ ├── MachineInfo.ts +│ │ │ └── SensorReading.ts +│ │ └── app.ts - Entry point +│ └── package.json - Dependencies +└── README.md - Instructions and info ``` -* `src/app.ts` - the actual application -* `src/config.ts` - contains the configuration (Atlas App ID) -* `src/models/` - the model classes - ## Use Cases This app focuses on showing how to use Data Ingest for heavy client-side insert-only workloads. It specifically addresses the following points: @@ -30,109 +32,117 @@ This app focuses on showing how to use Data Ingest for heavy client-side insert- * Inserting sensor data every few seconds and syncing it to [Atlas](https://www.mongodb.com/atlas). * (The data in Atlas can be visualized via [Atlas Charts](https://www.mongodb.com/products/charts), but since this is a Node.js app, the visualization is not shown.) -### Realm Details - -* [Realm JavaScript](https://github.com/realm/realm-js) version: ^12.1.0 -* Device Sync type: [Flexible](https://www.mongodb.com/docs/realm/sdk/node/sync/flexible-sync/) with [Data Ingest](https://www.mongodb.com/docs/atlas/app-services/sync/configure/sync-settings/#data-ingest) - ## Getting Started ### Prerequisites * [Node.js](https://nodejs.org/) -* An [Atlas App Service](https://www.mongodb.com/docs/atlas/app-services/) account - -### Set Up an Atlas App Services App - -To sync Realm data you must first: - -1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/) -2. [Enable Anonymous Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/anonymous/) -3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **[Development Mode](https://www.mongodb.com/docs/atlas/app-services/sync/configure/sync-settings/#development-mode)** on. - -After running the client and seeing the available collection in Atlas, [set write permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions) for the collection. - -The following schema will automatically be created in **Development Mode** and you can find it at `App Services / Data Access / Schema` in the Atlas App Service UI: - -```json -{ - "title": "SensorReading", - "type": "object", - "required": [ - "_id", - "freemem", - "timestamp", - "uptime" - ], - "properties": { - "_id": { - "bsonType": "objectId" - }, - "freemem": { - "bsonType": "long" - }, - "loadAvg": { - "bsonType": "array", - "items": { - "bsonType": "float" - } - }, - "machineInfo": { - "title": "MachineInfo", - "type": "object", - "required": [ - "platform", - "release" - ], - "properties": { - "platform": { - "bsonType": "string" - }, - "release": { - "bsonType": "string" - } - } - }, - "timestamp": { - "bsonType": "date" - }, - "uptime": { - "bsonType": "float" - } - } -} -``` -### Visualize Data +### Set up an Atlas Database -Data can be visualized by [Charts](https://www.mongodb.com/products/charts). An example from a [dashboard](Charts/Dashboard.charts) is shown below. +Start by [deploying a free Atlas cluster](https://www.mongodb.com/docs/atlas/getting-started/#get-started-with-atlas) and create an Atlas database. -![An example on how Charts can visualize incoming data](Charts/charts-example.png) +### Set up an Atlas App Services App -## How to build and run +You can either choose to set up your App via a CLI (this has fewer steps and is much faster since all configurations are already provided in the [backend directory](./backend/)), or via the App Services UI (steps provided below). -You need to clone Realm JavaScript's git repository: +#### Via a CLI (recommended) +To import and deploy changes from your local directory to App Services you can use the command line interface: + +1. [Set up Realm CLI](https://www.mongodb.com/docs/atlas/app-services/cli/). +2. In the provided [backend directory](./backend/) (the App Services App), update the following: + * Cluster Name + * Update the `"clusterName"` in [data_sources/mongodb-atlas/config.json](./backend/data_sources/mongodb-atlas/config.json) to the name of your cluster. + * (The default name is `Cluster0`.) + * App ID + * There is no `"app_id"` defined in [realm_config.json](./backend/realm_config.json) since we will create a brand new App. **If** you for some reason are updating an existing app, add an `"app_id"` field and its value. +3. [Push and deploy](https://www.mongodb.com/docs/atlas/app-services/cli/realm-cli-push/#std-label-realm-cli-push) the local directory to App Services: ```sh -git clone https://github.com/realm/realm-js +realm-cli push --local ``` +4. Once pushed, verify that your App shows up in the App Services UI. +5. 🥳 You can now go ahead and [install dependencies and run the Node app](#install-dependencies). + +#### Via the App Services UI -Moreover, you need to install the dependencies for this app: +To sync data used in this app you must first: + +1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/). +2. [Enable Anonymous Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/anonymous/). +3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **Development Mode** enabled. + * When Development Mode is enabled, schemas and Data Ingest will be inferred based on the client Realm data models. + * (Development Mode should be turned off in production.) +4. Don't forget to click `Review Draft and Deploy`. + +### Install Dependencies + +From the [node directory](./node/), run: ```sh -cd realm-js/examples/example-node-telemetry npm install ``` -Before building the app, you need to add your app id to `src/config.ts`. After that, you can build and run the app: +### Run the App + +1. Copy your [Atlas App ID](https://www.mongodb.com/docs/atlas/app-services/reference/find-your-project-or-app-id/#std-label-find-your-app-id) from the App Services UI. +2. Paste the copied ID as the value of the existing field `config.appId` in [src/atlas-app-services/config.ts](./node/src/atlas-app-services/config.ts): +```js +const config: Config = { + appId: "", +}; +``` +3. Start the script. + +After running the below command, the app will start reading sensor data every few seconds. To modify the interval duration, update `INSERT_DATA_INTERVAL` in [src/app.ts](./node/src/app.ts). ```sh npm start ``` -You can enable debug messages: +> DEBUG mode is enabled by default when running this app via `npm start`. +> Options available: +> ```sh +> # Only debug messages for the app. +> DEBUG=realm:telemetry node dist/app.js +> +> # Debug messages for many Realm operations. +> # - WARNING: Much output. +> DEBUG=realm:* node dist/app.js +> ``` + +### Set Data Access Permissions + +> If you set up your App Services App [via a CLI](#via-a-cli-recommended), you can **skip this step** as the permissions should already be defined for you. + +After running the client app for the first time, [check the rules](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#define-roles---permissions) for the collections in the App Services UI and make sure all collections have `readAndWriteAll` permissions (see [corresponding json](./backend/data_sources/mongodb-atlas/Telemetry/SensorReading/rules.json)). + +> To learn more and see examples of permissions depending on a certain use case, see [Device Sync Permissions Guide](https://www.mongodb.com/docs/atlas/app-services/sync/app-builder/device-sync-permissions-guide/#std-label-flexible-sync-permissions-guide) and [Data Access Role Examples](https://www.mongodb.com/docs/atlas/app-services/rules/examples/). + + +### Visualize Data + +Data can be visualized by [Charts](https://www.mongodb.com/products/charts). An example from a [dashboard](./node/Charts/Dashboard.charts) is shown below. + +![An example on how Charts can visualize incoming data](./node/Charts/charts-example.png) + +## Troubleshooting + +A great help when troubleshooting is to look at the [Application Logs](https://www.mongodb.com/docs/atlas/app-services/activity/view-logs/) in the App Services UI. + +### Permissions + +If permission is denied: + * Make sure your IP address is on the [IP Access List](https://www.mongodb.com/docs/atlas/app-services/security/network/#ip-access-list) for your App. + * Make sure you have the correct data access permissions for the collections. + * See [Set Data Access Permissions](#set-data-access-permissions) further above. + +### Removing the Local Realm Database + +Removing the local database can be useful for certain errors. When running the app, the local database will exist in the directory `mongodb-realm/`. + +From the [node directory](./node/), run: ```sh -DEBUG=realm:telemetry node dist/app.js # only debug messages for the app -DEBUG=realm:* node dist/app.js # debug messages for many Realm operations - WARNING: much output +npm run rm-local-db ``` diff --git a/examples/node-telemetry/backend/README.md b/examples/node-telemetry/backend/README.md new file mode 100644 index 0000000000..07508bebf5 --- /dev/null +++ b/examples/node-telemetry/backend/README.md @@ -0,0 +1,7 @@ +# Backend + +This contains the Atlas App Services App and its configurations for the example app. + +Please see the [main README](../README.md) for all instructions. + +> To learn more about the backend file structure, see [App Configuration](https://www.mongodb.com/docs/atlas/app-services/reference/config/). diff --git a/examples/node-telemetry/backend/auth/custom_user_data.json b/examples/node-telemetry/backend/auth/custom_user_data.json new file mode 100644 index 0000000000..a82d0fb255 --- /dev/null +++ b/examples/node-telemetry/backend/auth/custom_user_data.json @@ -0,0 +1,3 @@ +{ + "enabled": false +} diff --git a/examples/node-telemetry/backend/auth/providers.json b/examples/node-telemetry/backend/auth/providers.json new file mode 100644 index 0000000000..dff103b315 --- /dev/null +++ b/examples/node-telemetry/backend/auth/providers.json @@ -0,0 +1,12 @@ +{ + "anon-user": { + "name": "anon-user", + "type": "anon-user", + "disabled": false + }, + "api-key": { + "name": "api-key", + "type": "api-key", + "disabled": true + } +} diff --git a/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/relationships.json b/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/relationships.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/relationships.json @@ -0,0 +1 @@ +{} diff --git a/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/rules.json b/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/rules.json new file mode 100644 index 0000000000..64e3b6bbc0 --- /dev/null +++ b/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/rules.json @@ -0,0 +1,19 @@ +{ + "collection": "SensorReading", + "database": "Telemetry", + "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/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/schema.json b/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/schema.json new file mode 100644 index 0000000000..cadc336633 --- /dev/null +++ b/examples/node-telemetry/backend/data_sources/mongodb-atlas/Telemetry/SensorReading/schema.json @@ -0,0 +1,50 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "freemem": { + "bsonType": "long" + }, + "loadAvg": { + "bsonType": "array", + "items": { + "bsonType": "float" + } + }, + "machineInfo": { + "properties": { + "identifier": { + "bsonType": "string" + }, + "platform": { + "bsonType": "string" + }, + "release": { + "bsonType": "string" + } + }, + "required": [ + "identifier", + "platform", + "release" + ], + "title": "MachineInfo", + "type": "object" + }, + "timestamp": { + "bsonType": "date" + }, + "uptime": { + "bsonType": "float" + } + }, + "required": [ + "_id", + "freemem", + "timestamp", + "uptime" + ], + "title": "SensorReading", + "type": "object" +} diff --git a/examples/node-telemetry/backend/data_sources/mongodb-atlas/config.json b/examples/node-telemetry/backend/data_sources/mongodb-atlas/config.json new file mode 100644 index 0000000000..9913676dd9 --- /dev/null +++ b/examples/node-telemetry/backend/data_sources/mongodb-atlas/config.json @@ -0,0 +1,10 @@ +{ + "name": "mongodb-atlas", + "type": "mongodb-atlas", + "config": { + "clusterName": "Cluster0", + "readPreference": "primary", + "wireProtocolEnabled": false + }, + "version": 1 +} diff --git a/examples/node-telemetry/backend/data_sources/mongodb-atlas/default_rule.json b/examples/node-telemetry/backend/data_sources/mongodb-atlas/default_rule.json new file mode 100644 index 0000000000..86c55c8767 --- /dev/null +++ b/examples/node-telemetry/backend/data_sources/mongodb-atlas/default_rule.json @@ -0,0 +1,17 @@ +{ + "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/node-telemetry/backend/environments/development.json b/examples/node-telemetry/backend/environments/development.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-telemetry/backend/environments/development.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-telemetry/backend/environments/no-environment.json b/examples/node-telemetry/backend/environments/no-environment.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-telemetry/backend/environments/no-environment.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-telemetry/backend/environments/production.json b/examples/node-telemetry/backend/environments/production.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-telemetry/backend/environments/production.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-telemetry/backend/environments/qa.json b/examples/node-telemetry/backend/environments/qa.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-telemetry/backend/environments/qa.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-telemetry/backend/environments/testing.json b/examples/node-telemetry/backend/environments/testing.json new file mode 100644 index 0000000000..ad7e98e68c --- /dev/null +++ b/examples/node-telemetry/backend/environments/testing.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/examples/node-telemetry/backend/functions/config.json b/examples/node-telemetry/backend/functions/config.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/examples/node-telemetry/backend/functions/config.json @@ -0,0 +1 @@ +[] diff --git a/examples/node-telemetry/backend/graphql/config.json b/examples/node-telemetry/backend/graphql/config.json new file mode 100644 index 0000000000..406b1abcde --- /dev/null +++ b/examples/node-telemetry/backend/graphql/config.json @@ -0,0 +1,4 @@ +{ + "use_natural_pluralization": true, + "disable_schema_introspection": false +} diff --git a/examples/node-telemetry/backend/http_endpoints/config.json b/examples/node-telemetry/backend/http_endpoints/config.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/examples/node-telemetry/backend/http_endpoints/config.json @@ -0,0 +1 @@ +[] diff --git a/examples/node-telemetry/backend/realm_config.json b/examples/node-telemetry/backend/realm_config.json new file mode 100644 index 0000000000..ec653517f1 --- /dev/null +++ b/examples/node-telemetry/backend/realm_config.json @@ -0,0 +1,7 @@ +{ + "config_version": 20210101, + "name": "Telemetry", + "location": "IE", + "provider_region": "aws-eu-west-1", + "deployment_model": "GLOBAL" +} diff --git a/examples/node-telemetry/backend/sync/config.json b/examples/node-telemetry/backend/sync/config.json new file mode 100644 index 0000000000..43802310a6 --- /dev/null +++ b/examples/node-telemetry/backend/sync/config.json @@ -0,0 +1,16 @@ +{ + "type": "flexible", + "state": "enabled", + "development_mode_enabled": true, + "service_name": "mongodb-atlas", + "database_name": "Telemetry", + "client_max_offline_days": 30, + "is_recovery_mode_disabled": false, + "permissions": { + "rules": {}, + "defaultRoles": [] + }, + "asymmetric_tables": [ + "SensorReading" + ] +} diff --git a/examples/node-telemetry/.eslintignore b/examples/node-telemetry/node/.eslintignore similarity index 100% rename from examples/node-telemetry/.eslintignore rename to examples/node-telemetry/node/.eslintignore diff --git a/examples/node-telemetry/.eslintrc.json b/examples/node-telemetry/node/.eslintrc.json similarity index 100% rename from examples/node-telemetry/.eslintrc.json rename to examples/node-telemetry/node/.eslintrc.json diff --git a/examples/node-telemetry/node/.gitignore b/examples/node-telemetry/node/.gitignore new file mode 100644 index 0000000000..4dcf46063a --- /dev/null +++ b/examples/node-telemetry/node/.gitignore @@ -0,0 +1,8 @@ +# TS-generated output files. +dist/ + +# Dependencies. +node_modules/ + +# Local MongoDB Realm database. +mongodb-realm/ diff --git a/examples/node-telemetry/Charts/Dashboard.charts b/examples/node-telemetry/node/Charts/Dashboard.charts similarity index 100% rename from examples/node-telemetry/Charts/Dashboard.charts rename to examples/node-telemetry/node/Charts/Dashboard.charts diff --git a/examples/node-telemetry/Charts/charts-example.png b/examples/node-telemetry/node/Charts/charts-example.png similarity index 100% rename from examples/node-telemetry/Charts/charts-example.png rename to examples/node-telemetry/node/Charts/charts-example.png diff --git a/examples/node-telemetry/LICENSE b/examples/node-telemetry/node/LICENSE similarity index 100% rename from examples/node-telemetry/LICENSE rename to examples/node-telemetry/node/LICENSE diff --git a/examples/node-telemetry/node/README.md b/examples/node-telemetry/node/README.md new file mode 100644 index 0000000000..e5b2b7ba77 --- /dev/null +++ b/examples/node-telemetry/node/README.md @@ -0,0 +1,5 @@ +# Node + +This contains the Node code base for the example app. + +Please see the [main README](../README.md) for all instructions. diff --git a/examples/node-telemetry/package-lock.json b/examples/node-telemetry/node/package-lock.json similarity index 99% rename from examples/node-telemetry/package-lock.json rename to examples/node-telemetry/node/package-lock.json index f58fca7ee8..aecea56813 100644 --- a/examples/node-telemetry/package-lock.json +++ b/examples/node-telemetry/node/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "debug": "^4.3.4", "node-machine-id": "^1.1.12", - "realm": "^12.1.0" + "realm": "^12.2.0" }, "devDependencies": { "@types/debug": "^4.1.8", @@ -1260,9 +1260,9 @@ } }, "node_modules/realm": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.1.0.tgz", - "integrity": "sha512-uX2txyh4kWmH/rorWmsS9FZ4AM4vBh2TXDbwFn6z3b9z48kaqjCLLCcSlFN47DwHbOttCPnARg/xLHbPj5rynA==", + "version": "12.2.1", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.2.1.tgz", + "integrity": "sha512-r9lB5S3FiqS2QZmxRvwt8cRLAx/g8KGotz5neV/ky1AhG9b7FtqTwLyJwhVLjkstnV8j40GaY0xMjuAbURINDg==", "hasInstallScript": true, "dependencies": { "bson": "^4.7.2", @@ -2519,9 +2519,9 @@ } }, "realm": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.1.0.tgz", - "integrity": "sha512-uX2txyh4kWmH/rorWmsS9FZ4AM4vBh2TXDbwFn6z3b9z48kaqjCLLCcSlFN47DwHbOttCPnARg/xLHbPj5rynA==", + "version": "12.2.1", + "resolved": "https://registry.npmjs.org/realm/-/realm-12.2.1.tgz", + "integrity": "sha512-r9lB5S3FiqS2QZmxRvwt8cRLAx/g8KGotz5neV/ky1AhG9b7FtqTwLyJwhVLjkstnV8j40GaY0xMjuAbURINDg==", "requires": { "bson": "^4.7.2", "debug": "^4.3.4", diff --git a/examples/node-telemetry/package.json b/examples/node-telemetry/node/package.json similarity index 81% rename from examples/node-telemetry/package.json rename to examples/node-telemetry/node/package.json index 6799b6f07e..4355c2cda2 100644 --- a/examples/node-telemetry/package.json +++ b/examples/node-telemetry/node/package.json @@ -5,9 +5,10 @@ "description": "A demonstration of how to use data ingest to store and visualize sensor data", "main": "src/app.ts", "scripts": { - "build": "tsc -p tsconfig.json", + "build": "tsc", "lint": "eslint --ext .js,.mjs,.ts .", - "start": "npm run build && node dist/app.js", + "start": "npm run build && DEBUG=realm:telemetry node dist/app.js", + "rm-local-db": "rm -rf mongodb-realm/", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], @@ -20,7 +21,7 @@ "dependencies": { "debug": "^4.3.4", "node-machine-id": "^1.1.12", - "realm": "^12.1.0" + "realm": "^12.2.0" }, "devDependencies": { "@types/debug": "^4.1.8", diff --git a/examples/node-telemetry/src/app.ts b/examples/node-telemetry/node/src/app.ts similarity index 83% rename from examples/node-telemetry/src/app.ts rename to examples/node-telemetry/node/src/app.ts index c1a72f3b4c..11f5d5549e 100644 --- a/examples/node-telemetry/src/app.ts +++ b/examples/node-telemetry/node/src/app.ts @@ -16,22 +16,21 @@ // //////////////////////////////////////////////////////////////////////////// -import * as os from "node:os"; +import os from "node:os"; import { machineId } from "node-machine-id"; - +import Realm from "realm"; import createDebug from "debug"; export const debug = createDebug("realm:telemetry"); -import * as Realm from "realm"; import { MachineInfo } from "./models/MachineInfo"; import { SensorReading } from "./models/SensorReading"; -import { config } from "./config"; +import { config } from "./atlas-app-services/config"; const INSERT_DATA_INTERVAL = 10_000 as const; /** - * Samples system's load and memory - * @returns sensor readings + * Samples system's load and memory. + * @returns sensor readings. */ function readSensorData() { const loadAvg = os.loadavg(); @@ -42,8 +41,8 @@ function readSensorData() { /** * Computes a unique identifier for the computer and looks up - * the platform/operating system and its version/release - * @returns machine identifier, platform, and version/release + * the platform/operating system and its version/release. + * @returns machine identifier, platform, and version/release. */ async function readMachineInfo(): Promise { const identifier = await machineId(); @@ -52,6 +51,9 @@ async function readMachineInfo(): Promise { return { identifier, platform, release } as MachineInfo; } +/** + * Entry point. + */ async function main() { // Initialize the app using the App ID. To copy it, see: // https://www.mongodb.com/docs/atlas/app-services/apps/metadata/#std-label-find-your-app-id @@ -80,25 +82,30 @@ async function main() { const intervalId = setInterval(() => { const now = new Date(); const measurement = readSensorData(); - const obj = { - timestamp: now, - machineInfo, - ...measurement, - } as unknown as SensorReading; debug("Writing new sensor reading"); - realm.write(() => realm.create(SensorReading, obj)); + realm.write(() => { + realm.create(SensorReading, { + timestamp: now, + machineInfo, + ...measurement, + }); + }); }, INSERT_DATA_INTERVAL); - // Catch CTRL-C in a nice way process.stdin.resume(); + + // Catch CTRL-C in a nice way process.on("SIGINT", async () => { - debug("Shutting down."); - debug("Remove periodic sensor readings"); + debug("Shutting down"); + debug("Removing periodic sensor readings"); clearInterval(intervalId); - debug("Sync any outstanding changes"); + + debug("Syncing any outstanding changes"); await realm.syncSession?.uploadAllLocalChanges(); - debug("Closing Realm"); + + debug("Closing the Realm"); realm.close(); + debug(`Logging out user ${user.id}`); await user.logOut(); process.exit(0); diff --git a/examples/node-telemetry/src/config.ts b/examples/node-telemetry/node/src/atlas-app-services/config.ts similarity index 100% rename from examples/node-telemetry/src/config.ts rename to examples/node-telemetry/node/src/atlas-app-services/config.ts diff --git a/examples/node-telemetry/src/models/MachineInfo.ts b/examples/node-telemetry/node/src/models/MachineInfo.ts similarity index 92% rename from examples/node-telemetry/src/models/MachineInfo.ts rename to examples/node-telemetry/node/src/models/MachineInfo.ts index 449997aeff..9882c7ae60 100644 --- a/examples/node-telemetry/src/models/MachineInfo.ts +++ b/examples/node-telemetry/node/src/models/MachineInfo.ts @@ -16,14 +16,14 @@ // //////////////////////////////////////////////////////////////////////////// -import * as Realm from "realm"; +import Realm, { ObjectSchema } from "realm"; export class MachineInfo extends Realm.Object { identifier!: string; platform!: string; release!: string; - static schema: Realm.ObjectSchema = { + static schema: ObjectSchema = { name: "MachineInfo", embedded: true, properties: { diff --git a/examples/node-telemetry/src/models/SensorReading.ts b/examples/node-telemetry/node/src/models/SensorReading.ts similarity index 83% rename from examples/node-telemetry/src/models/SensorReading.ts rename to examples/node-telemetry/node/src/models/SensorReading.ts index 70a0a191fd..096658d910 100644 --- a/examples/node-telemetry/src/models/SensorReading.ts +++ b/examples/node-telemetry/node/src/models/SensorReading.ts @@ -16,23 +16,25 @@ // //////////////////////////////////////////////////////////////////////////// -import * as Realm from "realm"; +import Realm, { BSON, ObjectSchema } from "realm"; + import { MachineInfo } from "./MachineInfo"; export class SensorReading extends Realm.Object { - _id!: Realm.BSON.ObjectId; + _id!: BSON.ObjectId; timestamp!: Date; uptime!: number; freemem!: number; loadAvg!: Realm.List; machineInfo!: MachineInfo; - static schema: Realm.ObjectSchema = { + static schema: ObjectSchema = { name: "SensorReading", + // Mark the object as asymmetric to enable Data Ingest. asymmetric: true, primaryKey: "_id", properties: { - _id: { type: "objectId", default: () => new Realm.BSON.ObjectId() }, + _id: { type: "objectId", default: () => new BSON.ObjectId() }, timestamp: "date", uptime: "float", freemem: "int", diff --git a/examples/node-telemetry/tsconfig.json b/examples/node-telemetry/node/tsconfig.json similarity index 91% rename from examples/node-telemetry/tsconfig.json rename to examples/node-telemetry/node/tsconfig.json index 19061053bc..0003b4daea 100644 --- a/examples/node-telemetry/tsconfig.json +++ b/examples/node-telemetry/node/tsconfig.json @@ -3,7 +3,7 @@ "target": "es2018", "module": "CommonJS", "moduleResolution": "node", - "noEmit": false, + "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "types": [ "node", @@ -19,4 +19,4 @@ "src/**/*.ts" ], "exclude": [] -} \ No newline at end of file +} diff --git a/examples/rn-connection-and-error/README.md b/examples/rn-connection-and-error/README.md index 5108d1d261..55210981f4 100644 --- a/examples/rn-connection-and-error/README.md +++ b/examples/rn-connection-and-error/README.md @@ -103,7 +103,7 @@ When [opening a Realm](https://www.mongodb.com/docs/realm/sdk/node/sync/configur * `OpenRealmBehaviorType.DownloadBeforeOpen` * If there is data to be downloaded, this waits for the data to be fully synced before opening the Realm. -This app opens a Realm via `RealmProvider` (see [App.tsx](./frontend/app/App.tsx)) and passes the configuration as props. We use `OpenImmediately` for new and existing Realm files in order to use the app while offline. +This app opens a Realm via `RealmProvider` (see [App.tsx](./frontend/app/App.tsx)) and passes the configuration as props. We use `DownloadBeforeOpen` for new Realm files (first-time opens) in order to show a loading indicator (via `RealmProvider`'s `fallback` prop) until the data has been synced. We use `OpenImmediately` for existing Realm files in order to use the app while offline if the user has logged in at least once before. > See [OpenRealmBehaviorConfiguration](https://www.mongodb.com/docs/realm-sdks/js/latest/types/OpenRealmBehaviorConfiguration.html) for possible configurations of new and existing Realm file behaviors. diff --git a/examples/rn-connection-and-error/frontend/app/App.tsx b/examples/rn-connection-and-error/frontend/app/App.tsx index 8f8adf1bfd..4be6c98e9a 100644 --- a/examples/rn-connection-and-error/frontend/app/App.tsx +++ b/examples/rn-connection-and-error/frontend/app/App.tsx @@ -24,6 +24,7 @@ import {AppProvider, RealmProvider, UserProvider} from '@realm/react'; import {ATLAS_APP_ID, SYNC_STORE_ID} from './atlas-app-services/config'; import {AuthResultBoundary} from './components/AuthResultBoundary'; import {Kiosk} from './models/Kiosk'; +import {Loading} from './components/Loading'; import {LoginScreen} from './screens/LoginScreen'; import {Product} from './models/Product'; import {Store} from './models/Store'; @@ -107,6 +108,8 @@ function App() { Note that `user` does not need to be defined in the `sync` config since the `RealmProvider` will set it for you once authenticated. */} + Loading the store... + + ); +} + +const styles = StyleSheet.create({ + loading: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.grayLight, + }, + text: { + fontSize: 20, + fontWeight: 'bold', + color: colors.grayDark, + }, +}); diff --git a/examples/rn-multiple-realms/README.md b/examples/rn-multiple-realms/README.md index 8f60120f5b..5fb79725bf 100644 --- a/examples/rn-multiple-realms/README.md +++ b/examples/rn-multiple-realms/README.md @@ -1,6 +1,6 @@ -# A Netflix-Like App with Multiple Realms in Realm React Native SDK +# A Netflix-Like App with Multiple Realms Using Atlas Device SDK for React Native -A Netflix-like example app showcasing how to use different Realms in [MongoDB's Realm React Native SDK](https://www.mongodb.com/docs/realm/sdk/react-native/). All users can browse (not play) movies from MongoDB's [Mflix sample dataset](https://www.mongodb.com/docs/atlas/sample-data/sample-mflix/#std-label-mflix-movies), but only users who register with email and password are able to sync, read, add, and remove movies saved to "My List". +A Netflix-like example app showcasing how to use different Realms in [MongoDB's Atlas Device SDK for React Native](https://www.mongodb.com/docs/realm/sdk/react-native/). All users can browse (not play) movies from MongoDB's [Mflix sample dataset](https://www.mongodb.com/docs/atlas/sample-data/sample-mflix/#std-label-mflix-movies), but only users who register with email and password are able to sync, read, add, and remove movies saved to "My List". > This example app does not support playing any movies. @@ -22,33 +22,43 @@ The following shows the project structure and the most relevant files. ``` ├── backend - App Services App │ └── (see link above) +│ ├── frontend - React Native App │ ├── app │ │ ├── atlas-app-services │ │ │ └── config.ts - Add App ID +│ │ │ │ │ ├── components │ │ │ ├── MovieItem.tsx - Movie list item │ │ │ ├── MovieList.tsx - Horizontal movie list │ │ │ └── PublicLogin.tsx - Logs in public/anonymous users +│ │ │ │ │ ├── hooks │ │ │ └── useAccountInfo.ts - Provides account info +│ │ │ │ │ ├── models │ │ │ ├── Movie.ts - Mflix movie data model │ │ │ └── PrivateContent.ts - Data for private users +│ │ │ │ │ ├── navigation │ │ │ ├── MoviesNavigator.tsx - Navigates movie screens │ │ │ ├── RootNavigator.tsx - Navigates bottom tabs │ │ │ └── routes.ts - Available routes +│ │ │ │ │ ├── providers │ │ │ └── MovieProvider.tsx - Queries and updates data +│ │ │ │ │ ├── screens │ │ │ ├── AccountScreen.tsx - Login and account info │ │ │ ├── MovieInfoScreen.tsx - Movie info and add to My List │ │ │ └── MoviesScreen.tsx - Movies grouped by category +│ │ │ │ │ ├── App.tsx - Provides the App Services App │ │ └── AuthenticatedApp.tsx - Opens different Realms -│ ├── index.ts - Entry point +│ │ +│ ├── index.js - Entry point │ └── package.json - Dependencies +│ └── README.md - Instructions and info ``` @@ -80,11 +90,13 @@ This app uses multiple Realms, but only one Realm is kept open at any given time This app uses synced Realms as it needs to load the Mflix dataset from Atlas. Thus, for logging in, a network connection is required. -Users who have logged in at least once will have their credentials cached on the client. Data that was previously synced to the device will also exist locally in the Realm database. From this point on, users can be offline and still query and update data. Any changes made offline will be synced automatically to Atlas and any other devices once a network connection is established. +Users who have logged in at least once will have their credentials cached on the client. Thus, a logged in user who restarts the app will remain logged in. [@realm/react's](https://www.npmjs.com/package/@realm/react) `UserProvider` automatically handles this for you by checking if the `app.currentUser` already exists. + +Data that was previously synced to the device will also exist locally in the Realm database. From this point on, users can be offline and still query and update data. Any changes made offline will be synced automatically to Atlas and any other devices once a network connection is established. If multiple users modify the same data either while online or offline, those conflicts are [automatically resolved](https://www.mongodb.com/docs/atlas/app-services/sync/details/conflict-resolution/) before being synced. #### Realm Configuration -When opening a Realm, we can specify the behavior in the Realm configuration when opening it for the first time (via `newRealmFileBehavior`) and for subsequent ones (via `existingRealmFileBehavior`). We can either: +When [opening a Realm](https://www.mongodb.com/docs/realm/sdk/react-native/sync-data/configure-a-synced-realm/), we can specify the behavior in the Realm configuration when opening it for the first time (via `newRealmFileBehavior`) and for subsequent ones (via `existingRealmFileBehavior`). We can either: * `OpenRealmBehaviorType.OpenImmediately` * Opens the Realm file immediately if it exists, otherwise it first creates a new empty Realm file then opens it. * This lets users use the app with the existing data, while syncing any changes to the device in the background. @@ -99,17 +111,13 @@ This app opens a Realm via `RealmProvider` (see [AuthenticatedApp.tsx](./fronten Each movie poster is loaded from a remote source and is not cached on the client for this example app. Therefore, if the user is offline, only the placeholder image will be shown as the poster. -### Realm Details - -* RealmJS version: ^12.1.0 -* Device Sync type: [Flexible](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) - ## Getting Started ### Prerequisites * [Node.js](https://nodejs.org/) * [React Native development environment](https://reactnative.dev/docs/environment-setup?guide=native) + * Refer to the **"React Native CLI Quickstart"**. ### Set up an Atlas Database with a Sample Dataset diff --git a/examples/rn-multiple-realms/frontend/package-lock.json b/examples/rn-multiple-realms/frontend/package-lock.json index 78b2caf915..717c2674e3 100644 --- a/examples/rn-multiple-realms/frontend/package-lock.json +++ b/examples/rn-multiple-realms/frontend/package-lock.json @@ -11,7 +11,7 @@ "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", - "@realm/react": "^0.6.0", + "@realm/react": "^0.6.1", "react": "18.2.0", "react-native": "0.72.4", "react-native-safe-area-context": "^4.7.2", @@ -4022,9 +4022,9 @@ } }, "node_modules/@realm/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.0.tgz", - "integrity": "sha512-gggNChqj3J2ImgIf3Q6I++DEAo2KW+52Dh0ndv7QWhek0CLCHKIGiWYXBikDmW1bqGsj8gbLVr7mxbOshnRkKg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.1.tgz", + "integrity": "sha512-+W16jgjqXpNzLsQvOW294yqffZw36uvk3257tuk4A9a9JyO4RdX1kEYxondleV8jDAqpeyYf5ajyZuZeDiYErw==", "dependencies": { "lodash": "^4.17.21" }, @@ -4034,7 +4034,7 @@ }, "peerDependencies": { "react": ">=17.0.2", - "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0-rc || ^11.0.0" + "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0" } }, "node_modules/@sideway/address": { @@ -15751,9 +15751,9 @@ } }, "@realm/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.0.tgz", - "integrity": "sha512-gggNChqj3J2ImgIf3Q6I++DEAo2KW+52Dh0ndv7QWhek0CLCHKIGiWYXBikDmW1bqGsj8gbLVr7mxbOshnRkKg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.1.tgz", + "integrity": "sha512-+W16jgjqXpNzLsQvOW294yqffZw36uvk3257tuk4A9a9JyO4RdX1kEYxondleV8jDAqpeyYf5ajyZuZeDiYErw==", "requires": { "@babel/runtime": ">=7", "lodash": "^4.17.21", diff --git a/examples/rn-multiple-realms/frontend/package.json b/examples/rn-multiple-realms/frontend/package.json index bdc58f9f3f..d9e68af381 100644 --- a/examples/rn-multiple-realms/frontend/package.json +++ b/examples/rn-multiple-realms/frontend/package.json @@ -13,7 +13,7 @@ "@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", - "@realm/react": "^0.6.0", + "@realm/react": "^0.6.1", "react": "18.2.0", "react-native": "0.72.4", "react-native-safe-area-context": "^4.7.2", diff --git a/examples/rn-todo-list/README.md b/examples/rn-todo-list/README.md index d10d021f5d..0725e22a39 100644 --- a/examples/rn-todo-list/README.md +++ b/examples/rn-todo-list/README.md @@ -88,7 +88,7 @@ When opening a Realm, we can specify the behavior in the Realm configuration whe * `OpenRealmBehaviorType.DownloadBeforeOpen` * If there is data to be downloaded, this waits for the data to be fully synced before opening the Realm. -This app opens a Realm via `RealmProvider` (see [AppSync.tsx](./frontend/app/AppSync.tsx)) and passes the configuration as props. We use `OpenImmediately` for new and existing Realm files in order to use the app while offline. +This app opens a Realm via `RealmProvider` (see [AppSync.tsx](./frontend/app/AppSync.tsx)) and passes the configuration as props. We use `DownloadBeforeOpen` for new Realm files (first-time opens) in order to show a loading indicator (via `RealmProvider`'s `fallback` prop) until the data has been synced. We use `OpenImmediately` for existing Realm files in order to use the app while offline if the user has logged in at least once before. > See [OpenRealmBehaviorConfiguration](https://www.mongodb.com/docs/realm-sdks/js/latest/types/OpenRealmBehaviorConfiguration.html) for possible configurations of new and existing Realm file behaviors. diff --git a/examples/rn-todo-list/frontend/app/AppSync.tsx b/examples/rn-todo-list/frontend/app/AppSync.tsx index 231d442cfe..33ce45dea0 100644 --- a/examples/rn-todo-list/frontend/app/AppSync.tsx +++ b/examples/rn-todo-list/frontend/app/AppSync.tsx @@ -21,6 +21,7 @@ import {SafeAreaView, StyleSheet} from 'react-native'; import {OpenRealmBehaviorType} from 'realm'; import {AppProvider, RealmProvider, UserProvider} from '@realm/react'; +import {Loading} from './components/Loading'; import {LoginScreen} from './screens/LoginScreen'; import {Task} from './models/Task'; import {TaskScreenSync} from './screens/TaskScreenSync'; @@ -48,6 +49,8 @@ export function AppSync({appId}: AppSyncProps) { Note that `user` does not need to be defined in the `sync` config since the `RealmProvider` will set it for you once authenticated. */} + Loading the tasks... + + ); +} + +const styles = StyleSheet.create({ + loading: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: colors.grayLight, + }, + text: { + fontSize: 18, + fontWeight: 'bold', + color: colors.grayDark, + }, +}); diff --git a/examples/rn-todo-list/frontend/package-lock.json b/examples/rn-todo-list/frontend/package-lock.json index 521944ba70..82a4cb2def 100644 --- a/examples/rn-todo-list/frontend/package-lock.json +++ b/examples/rn-todo-list/frontend/package-lock.json @@ -8,7 +8,7 @@ "name": "@realm/example-rn-todo-list", "version": "0.0.1", "dependencies": { - "@realm/react": "^0.6.0", + "@realm/react": "^0.6.1", "react": "18.2.0", "react-native": "0.72.5", "react-native-get-random-values": "^1.9.0", @@ -3423,9 +3423,9 @@ "dev": true }, "node_modules/@realm/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.0.tgz", - "integrity": "sha512-gggNChqj3J2ImgIf3Q6I++DEAo2KW+52Dh0ndv7QWhek0CLCHKIGiWYXBikDmW1bqGsj8gbLVr7mxbOshnRkKg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.1.tgz", + "integrity": "sha512-+W16jgjqXpNzLsQvOW294yqffZw36uvk3257tuk4A9a9JyO4RdX1kEYxondleV8jDAqpeyYf5ajyZuZeDiYErw==", "dependencies": { "lodash": "^4.17.21" }, @@ -3435,7 +3435,7 @@ }, "peerDependencies": { "react": ">=17.0.2", - "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0-rc || ^11.0.0" + "realm": "^12.0.0-browser || ^12.0.0 || ^12.0.0-rc || ^11.0.0" } }, "node_modules/@sideway/address": { @@ -14382,9 +14382,9 @@ "dev": true }, "@realm/react": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.0.tgz", - "integrity": "sha512-gggNChqj3J2ImgIf3Q6I++DEAo2KW+52Dh0ndv7QWhek0CLCHKIGiWYXBikDmW1bqGsj8gbLVr7mxbOshnRkKg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@realm/react/-/react-0.6.1.tgz", + "integrity": "sha512-+W16jgjqXpNzLsQvOW294yqffZw36uvk3257tuk4A9a9JyO4RdX1kEYxondleV8jDAqpeyYf5ajyZuZeDiYErw==", "requires": { "@babel/runtime": ">=7", "lodash": "^4.17.21", diff --git a/examples/rn-todo-list/frontend/package.json b/examples/rn-todo-list/frontend/package.json index a5df594ea6..d5ad05e597 100644 --- a/examples/rn-todo-list/frontend/package.json +++ b/examples/rn-todo-list/frontend/package.json @@ -10,7 +10,7 @@ "test": "jest" }, "dependencies": { - "@realm/react": "^0.6.0", + "@realm/react": "^0.6.1", "react": "18.2.0", "react-native": "0.72.5", "react-native-get-random-values": "^1.9.0", diff --git a/package-lock.json b/package-lock.json index dd7bcd4bd1..65e7051f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "hasInstallScript": true, "license": "apache-2.0", "workspaces": [ - "examples/node-connection-and-error", "packages/realm/bindgen/", "packages/realm/bindgen/vendor/realm-core/", "packages/babel-plugin", @@ -60,67 +59,6 @@ "jsc-android": "250231.0.0" } }, - "examples/node-connection-and-error": { - "name": "@realm/node-connection-and-error", - "version": "1.0.0", - "dependencies": { - "realm": "^12.0.0" - }, - "devDependencies": { - "@types/node": "^20.5.7", - "typescript": "^5.1.6" - } - }, - "examples/node-connection-and-error/node_modules/@types/node": { - "version": "20.5.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", - "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", - "dev": true - }, - "examples/node-connection-and-error/node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "examples/rn-multiple-realms": { - "name": "@realm/example-rn-multiple-realms", - "version": "0.0.1", - "extraneous": true, - "dependencies": { - "@realm/react": "^0.6.0", - "react": "18.2.0", - "react-native": "0.72.4", - "realm": "^12.0.0" - }, - "devDependencies": { - "@babel/core": "^7.20.0", - "@babel/preset-env": "^7.20.0", - "@babel/runtime": "^7.20.0", - "@react-native/eslint-config": "^0.72.2", - "@react-native/metro-config": "^0.72.11", - "@tsconfig/react-native": "^3.0.0", - "@types/react": "^18.0.24", - "@types/react-test-renderer": "^18.0.0", - "babel-jest": "^29.2.1", - "eslint": "^8.19.0", - "jest": "^29.2.1", - "metro-react-native-babel-preset": "0.76.8", - "prettier": "^2.4.1", - "react-test-renderer": "18.2.0", - "typescript": "4.8.4" - }, - "engines": { - "node": ">=16" - } - }, "integration-tests/baas-test-server": { "name": "@realm/baas-test-server", "version": "0.1.0", @@ -5640,10 +5578,6 @@ "resolved": "packages/realm-network-transport", "link": true }, - "node_modules/@realm/node-connection-and-error": { - "resolved": "examples/node-connection-and-error", - "link": true - }, "node_modules/@realm/node-tests": { "resolved": "integration-tests/environments/node", "link": true @@ -30194,7 +30128,7 @@ }, "packages/realm-react": { "name": "@realm/react", - "version": "0.6.0", + "version": "0.6.1", "license": "Apache-2.0", "dependencies": { "lodash": "^4.17.21" @@ -36116,28 +36050,6 @@ } } }, - "@realm/node-connection-and-error": { - "version": "file:examples/node-connection-and-error", - "requires": { - "@types/node": "^20.5.7", - "realm": "^12.0.0", - "typescript": "^5.1.6" - }, - "dependencies": { - "@types/node": { - "version": "20.5.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", - "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==", - "dev": true - }, - "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true - } - } - }, "@realm/node-tests": { "version": "file:integration-tests/environments/node", "requires": { diff --git a/package.json b/package.json index 80599d0c60..bfd1a83b1e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "url": "https://www.mongodb.com/docs/realm/" }, "workspaces": [ - "examples/node-connection-and-error", "packages/realm/bindgen/", "packages/realm/bindgen/vendor/realm-core/", "packages/babel-plugin",