diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9dce6c49..a35ef469 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,6 +10,7 @@ This closes # ??? ## ☑️ ToDos * [ ] 📝 Changelog entry +* [ ] 📝 `Compatibility` label is updated or copied from previous entry * [ ] 🚦 Tests * [ ] 📝 Public documentation PR created or is not necessary * [ ] 💥 `Breaking` label has been applied or is not necessary diff --git a/CHANGELOG.md b/CHANGELOG.md index 79d18a4b..9b218172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,147 @@ +2.4.0 Release notes (2018-4-26) +============================================================= +### Compatibility +* Sync protocol: 24 +* Server-side history format: 4 +* File format: 7 +* Realm Object Server: 3.0.0 or later + +### Breaking changes +* None. + +### Enhancements +* Added password reset wrappers (#1699). +* Added a certificate validation using Android Keystore for RN (#1761). + +### Bug fixes +* Fixed logout error due to fetch body not being stringified (#1731). +* Added `Subscription` import to `browser/index.js` and register type converter (#1711). +* Fixed call to `logout()` when debugging React Native apps (#1744). + +### Internal +* Updated `scripts/test.sh` so it doesn't hang forever when the React tests fail to start (#1764). + + +2.3.4 Release notes (2018-4-12) +============================================================= +### Compatibility +* Sync protocol: 24 +* Server-side history format: 4 +* File format: 7 +* Realm Object Server: 3.0.0 or later + +### Breaking changes +* None. + +### Enhancements +* None. + +### Bug fixes +* Fixed named LinkingObject queries across different classes (#1734). +* Fixed a bug when refreshing admin token due to network errors (realm-js-private #433). + +### Internal +* None. + +2.3.3 Release notes (2018-3-23) +============================================================= +### Compatibility +* Sync protocol: 24 +* Server-side history format: 4 +* File format: 7 +* Realm Object Server: 3.0.0 or later + +### Breaking changes +* None. + +### Enhancements +* None. + +### Bug fixes +* Fixed a bug where leaking Realms when an error occurs within an event handler (#1725). + +### Internal +* Added trace logging to the global notifier (realm-js-private #426). + +2.3.2 Release notes (2018-3-21) +============================================================= +### Compatibility +* Sync protocol: 24 +* Server-side history format: 4 +* File format: 7 +* Realm Object Server: 3.0.0 or later + +### Breaking changes +* None. + +### Enhancements +* Added `Realm.Sync.Subscription.removeAllListeners()`. + +### Internal +* Tested with Realm Object Server 3.0.0. + +2.3.1 Release notes (2018-3-16) +============================================================= +### Compatibility +* Sync protocol: 24 +* Server-side history format: 4 +* File format: 7 +* Realm Object Server: 3.0.0-alpha.8 or later + +### Breaking changes +* None. + +### Enhancements +* Added an optional user as argument to `Realm.automaticSyncConfiguration` (#1708). + +### Bug fixes +* [Sync] Avoid hammering the ROS authentication service when large numbers of Realms are opened at once. + +### Internal +* Tested with Realm Object Server 3.0.0-rc.1. + + +2.3.0 Release notes (2018-3-13) +============================================================= +### Breaking changes +* [Sync] Sync protocol changed to version 24. +* [Sync] History schema format for server-side Realm files bumped to version 4. This means that after the server has been upgraded, it cannot be downgraded again without restoring state from backup. +* [Sync] `Realm.subscribeToObjects()` has been removed. Use `Realm.Results.subscribe()` instead. + +### Enhancements +* [Sync] Reduced initial download times in Realms with long transaction histories. +* [Sync] Wait for pending notifications to complete when removing a sync listener (#1648). +* Enabled sort and distinct in the query string. If sort or distinct are also applied outside of the query string, the conditions are stacked. + - Example syntax: `age > 20 SORT(name ASC, age DESC) DISTINCT(name)` + - The ordering for sorting can be one of the following case insensitive literals: `ASC`, `ASCENDING`, `DESC`, `DESCENDING`. + - Any number of properties can appear inside the brackets in a comma separated list. + - Any number of sort/distinct conditions can be indicated, they will be applied in the specified order. + - Sort or distinct cannot operate independently, these conditions must be attached to at least one query filter. +* Added support for queries over named backlinks (#1498/#1660). + - Example syntax: `parents.age > 25` and `parents.@count == 2`. +* [Sync] Added `Realm.Results.subscribe()` to subscribe to partial synced Realms. +* [Sync] Added class `Realm.Sync.Subscription` and enum `Realm.Sync.SubscriptionState` to support partial synced Realms. +* [Sync] Added an object-level permission subsystem. It is possible to grant fine-grained priviliges to users. +* Added object-level permissions: + - Schemas `Realm.Permissions.Realm`, `Realm.Permissions.Class`, `Realm.Permissions.Role`, `Realm.Permissions.User`, and `Realm.Permissions.Permission` to support working with permissions. These schemas can be used in user-defined Realms and schemas. + - Permissions are enforced by the object server but connectivity is not required. + - Method `Realm.privilges()` to compute privileges on a Realm, a Realm object schema, or a Realm object. The method returns either a `Realm.Permissions.Realm` or `Realm.Permissions.Class` object. + - For non-synced Realms, all privileges are always granted. + - For more details, please read the reference documentation. +* [Sync] Revoke refresh token upon logout (#1354). +* Added `Realm.automaticSyncConfiguration()` which will return the configuration for a default synced Realm (#1688). +* [Sync] Deprecated `Realm.Sync.setFeatureToken` (#1689). + +### Bug fixes +* Fixed usage of disk space preallocation which would occasionally fail on recent MacOS running with the APFS filesystem (Realm Core #3005). + +### Internal +* Updated to Realm Core 5.4.0. +* Updated to Realm Sync 3.0.0. +* Tested against Realm Object Server 3.0.0-alpha.8. +* Added `_disablePartialSyncUrlChecks` to `Realm.Configuration`. + + 2.2.20 Release notes (2018-4-13) ============================================================= ### Breaking changes @@ -97,8 +241,7 @@ * None. ### Bug fixes -* [Object Server] Fixed race condition in handling of session bootstrapping in client. - +* [Sync] Fixed race condition in handling of session bootstrapping in client. ### Internal * Updated to Realm Sync 2.2.15. @@ -113,7 +256,7 @@ * None. ### Bug fixes -* [Object Server] Fixed handling of SSL certificates for the sync client. +* [Sync] Fixed handling of SSL certificates for the sync client. ### Internal * Updated to Realm Sync 2.2.14. @@ -143,7 +286,7 @@ * None. ### Bug fixes -* [Object Server] A use-after-free bug was fixed which could cause arrays of primitives to behave unexpectedly. +* [Sync] A use-after-free bug was fixed which could cause arrays of primitives to behave unexpectedly. ### Internal * Updated to Realm Sync 2.2.12. @@ -170,12 +313,12 @@ * None. ### Enhancements -* [Object Server] For OpenSSL, the sync client includes a fixed list of certificates in its SSL certificate verification besides the default trust store in the case where the user is not specifying its own trust certificates or callback. +* [Sync] For OpenSSL, the sync client includes a fixed list of certificates in its SSL certificate verification besides the default trust store in the case where the user is not specifying its own trust certificates or callback. ### Bug fixes * None. -### Internaæ +### Internal * Updated to Realm Sync 2.2.10. @@ -185,11 +328,11 @@ * None. ### Enhancements -* [Object Server] Wait for pending notifications to complete when removing a sync listener (#1648). +* [Sync] Wait for pending notifications to complete when removing a sync listener (#1648). * Add schema name to missing primary key error message ### Bug fixes -* [Object Server] Fixed a bug causing use-after-free crashes in Global Notifier (realm-js-private #405). +* [Sync] Fixed a bug causing use-after-free crashes in Global Notifier (realm-js-private #405). ### Internal * None. @@ -204,7 +347,7 @@ * None. ### Bug fixes -* [Object Server] Fixed a bug where arguments were not transferred when debugging. +* [Sync] Fixed a bug where arguments were not transferred when debugging. ### Internal * None. @@ -218,7 +361,7 @@ * None. ### Bug fixes -* [Object Server] Fixed a typing error leading to `_getExistingUser` wasn't defined in the Chrome debugging support library (#1625). +* [Sync] Fixed a typing error leading to `_getExistingUser` wasn't defined in the Chrome debugging support library (#1625). * Fixed a bug in the TypeScript definition of `PermissionCondition` (#1574). * [Electron] Fixed a `dlopen` error related to OpenSSL that prevented using realm-js on Linux (#1636). @@ -234,8 +377,8 @@ * None. ### Bug fixes -* [Object Server] Fixed a bug where errors in `refreshAdminToken` wasn't catched (#1627). -* [Object Server] Added `_getExitingUser` to the Chrome debugging support library. +* [Sync] Fixed a bug where errors in `refreshAdminToken` wasn't catched (#1627). +* [Sync] Added `_getExitingUser` to the Chrome debugging support library. ### Internal * None. @@ -249,8 +392,8 @@ * None. ### Bug fixes -* [Object Server] Fixed a bug in upload progress reporting. -* [Object Server] Fixed a bug where any errors which occurred when trying to sync the admin Realm were ignored, which made attempting to add a listener with an invalid admin user silently do nothing. +* [Sync] Fixed a bug in upload progress reporting. +* [Sync] Fixed a bug where any errors which occurred when trying to sync the admin Realm were ignored, which made attempting to add a listener with an invalid admin user silently do nothing. ### Internal * None. @@ -264,7 +407,7 @@ * None. ### Bug fixes -* [Object Server] Added missing `Realm.Sync` listener functions. +* [Sync] Added missing `Realm.Sync` listener functions. ### Internal * None. @@ -279,7 +422,7 @@ * None. ### Bug fixes -* [Object Server] Fixed a bug preventing opening Realms with an admin token without a working ROS directory service (#1615). +* [Sync] Fixed a bug preventing opening Realms with an admin token without a working ROS directory service (#1615). ### Internal * None. @@ -292,8 +435,8 @@ ### Enhancements * Added new query features to support a subset of `NSPredicates` for example `LIKE` for string matches, `@count` and `@sum` in lists. See documentation for more details. * Potential performance enhancements in cases of many writes between queries. -* [Object Server] Added method `Realm.Sync.User.authenticate` to unify authentication of users. -* [Object Server] Added JWT authenfication (#1548). +* [Sync] Added method `Realm.Sync.User.authenticate` to unify authentication of users. +* [Sync] Added JWT authenfication (#1548). ### Bug fixes * Fix a bug where `Realm.open` could unexpectedly raise a "Realm at path ... already opened with different schema version" error. @@ -314,10 +457,10 @@ * None. ### Bug fixes -* [Object Server] Fixed a bug where long reconnection happens when a proxy in front of the sync worker returns one of those. +* [Sync] Fixed a bug where long reconnection happens when a proxy in front of the sync worker returns one of those. ### Internal -* [Object Server] Updated to Realm Object Server v2.2.0 for testing. +* [Sync] Updated to Realm Object Server v2.2.0 for testing. * Updated to Realm Sync 2.1.10 (see "Bug fixes"). @@ -345,11 +488,11 @@ * None. ### Bug fixes -* [Object Server] When authentication fails due to a misbehaving server, a proper error is thrown. +* [Sync] When authentication fails due to a misbehaving server, a proper error is thrown. ### Internal -* [Object Server] Strings can now be assigned to Date columns. When that happens the JavaScript Date constructor will be invoked to parse the string. -* [Object Server] Base64 strings can now be assigned to Data columns. +* [Sync] Strings can now be assigned to Date columns. When that happens the JavaScript Date constructor will be invoked to parse the string. +* [Sync] Base64 strings can now be assigned to Data columns. 2.0.12 Release notes (2017-12-1) ============================================================= @@ -375,8 +518,8 @@ * None ### Bug fixes -* [Object Server] Fixed a bug where deleted-then-recreated objects with identical primary keys to become empty. -* [Object Server] Fixed a bug in outward partial sync is changed to ensure convergence of partial sync in the case where the client creates a primary key object, that is already present on the server, and subscribes to it in the same transaction. +* [Sync] Fixed a bug where deleted-then-recreated objects with identical primary keys to become empty. +* [Sync] Fixed a bug in outward partial sync is changed to ensure convergence of partial sync in the case where the client creates a primary key object, that is already present on the server, and subscribes to it in the same transaction. ### Internal * Updated to Realm Sync 2.1.7 (see under "Bug fixes"). @@ -415,10 +558,10 @@ * None. ### Enhancements -* [Object Server] Improving performance of processing large changesets. +* [Sync] Improving performance of processing large changesets. ### Bug fixes -* [Object Server] Changesets over 16MB in size are now handled correctly. +* [Sync] Changesets over 16MB in size are now handled correctly. ### Internal * Updated to Realm Sync 2.1.6. diff --git a/dependencies.list b/dependencies.list index eea139fd..f85f19fc 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,5 +1,6 @@ PACKAGE_NAME=realm-js -VERSION=2.2.20 -REALM_CORE_VERSION=5.1.2 -REALM_SYNC_VERSION=2.2.17 -REALM_OBJECT_SERVER_VERSION=2.5.1 +VERSION=2.4.0 +REALM_CORE_VERSION=5.4.0 +REALM_SYNC_VERSION=3.0.0 +REALM_OBJECT_SERVER_VERSION=3.0.0 + diff --git a/docs/collection.js b/docs/collection.js index a3f524b3..fe791327 100644 --- a/docs/collection.js +++ b/docs/collection.js @@ -134,6 +134,27 @@ class Collection { */ snapshot() {} + /** + * Subscribe to a subset of objects matching the query of the collection. The Realm will only be + * partially synced. Not all queries are currently supported. Once subscribed, it is highly recommended + * to add a listener. + * + * @example + * let wines = realm.objects('Wine').filtered('vintage <= $0', maxYear); + * let subscription = wines.subscribe(); + * wines.addListener((collection, changes) => { + * if (subscription.state === Realm.Sync.SubscriptionState.Complete) { + * // update UI + * } + * }); + * + * @param {string} subscriptionName - an optional name for the subscription. + * @returns {Realm.Sync.Subscription} - the Realm.Sync.Subscription instance. + * @throws {Error} if the partial sync is not enabled in the configuration or the query is not supported by Realm Object Server. + * @since 2.3.0 + */ + subscribe(subscriptionName) {} + /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries Array.prototype.entries} * @returns {Realm.Collection~Iterator} of each `[index, object]` pair in the collection @@ -400,15 +421,21 @@ class Collection { * The callback function is called with two arguments: * - `collection`: the collection instance that changed, * - `changes`: a dictionary with keys `insertions`, `modifications` and `deletions`, - * each containing a list of indices that were inserted, updated or deleted respectively. + * each containing a list of indices that were inserted, updated or deleted respectively. If + * partial sync is enabled, an additional key `partial_sync` is added. + * - `changes.partial_sync`: `error` indicates if an error has occurred, `old_state` is the previous + * state, and `new_state` is the current state. * @throws {Error} If `callback` is not a function. * @example * wines.addListener((collection, changes) => { * // collection === wines - * console.log(`${changes.insertions.length} insertions`); - * console.log(`${changes.modifications.length} modifications`); - * console.log(`${changes.deletions.length} deletions`); - * console.log(`new size of collection: ${collection.length}`); + * if (changes.partial_sync.new_state == Realm.Sync.SubscriptionState.Initialized) { + * console.log('Our subset is ready'); + * console.log(`${changes.insertions.length} insertions`); + * console.log(`${changes.modifications.length} modifications`); + * console.log(`${changes.deletions.length} deletions`); + * console.log(`new size of collection: ${collection.length}`); + * } * }); */ addListener(callback) {} diff --git a/docs/permission.js b/docs/permission.js index 450ae7f9..dc4d03fb 100644 --- a/docs/permission.js +++ b/docs/permission.js @@ -21,7 +21,7 @@ * They are created exclusively by the client and are processed by the server * as indicated by the status fields. * PermissionChange objects allow to grant and revoke permissions by setting - * mayRead, mayWrite and mayManage accordingly. + * mayRead, mayWrite and mayManage accordingly. * If any of these flags are not set, these are merged * with either the existing or default permissions as applicable. As a * side-effect this causes that the default permissions are permanently @@ -30,7 +30,7 @@ * ErrorCode will be updated accordingly. */ class PermissionChange { - + /** * Gets the unique identifier of this object in the Management realm. * @type {string} @@ -229,3 +229,161 @@ class PermissionOfferResponse { */ get realmUrl() {} } + + + +/** + * A permission which can be applied to a Realm, Class, or specific Object. + * Permissions are applied by adding the permission to the Realm.Permission singleton + * object, the RealmClass.Permission object for the desired class, or to a user-defined + * Realm.List property on a specific Object instance. The meaning of each of + * the properties of Permission depend on what the permission is applied to, and so are + * left undocumented here. + * @since 2.3.0 +*/ +class Permission { + + /** + * The Role which this Permission applies to. All users within the Role are + * granted the permissions specified by the fields below any + * objects/classes/realms which use this Permission. + * + * This property cannot be modified once set. + * @type {Role} + */ + get role() {} + + /** + * Whether the user can read the object to which this Permission is attached. + * @type {boolean} + */ + get canRead() {} + + /** + * Whether the user can modify the object to which this Permission is attached. + * @type {boolean} + */ + get canUpdate() {} + + /** + * Whether the user can delete the object to which this Permission is attached. + * + * This property is only applicable to Permissions attached to Objects, and not + * to Realms or Classes. + * @type {boolean} + */ + get canDelete() {} + + /** + * Whether the user can add or modify Permissions for the object which this + * Permission is attached to. + * @type {boolean} + */ + get canSetPermissions() {} + + /** + * Whether the user can subscribe to queries for this object type. + * + * This property is only applicable to Permissions attached to Classes, and not + * to Realms or Objects. + * @type {boolean} + */ + get canQuery() {} + + /** + * Whether the user can create new objects of the type this Permission is attached to. + * + * This property is only applicable to Permissions attached to Classes, and not + * to Realms or Objects. + * @type {boolean} + */ + get canCreate() {} + + /** + * Whether the user can modify the schema of the Realm which this + * Permission is attached to. + * + * This property is only applicable to Permissions attached to Realms, and not + * to Realms or Objects. + * @type {boolean} + */ + get canModifySchema() {} +} + +/** + * A representation of a sync user within the permissions system. + * + * User objects are created automatically for each sync user which connects to + * a Realm, and can also be created manually if you wish to grant permissions to a user + * which has not yet connected to this Realm. + * @since 2.3.0 + */ +class User { + /** + * The unique Realm Object Server user ID string identifying this user. This will have + * the same value as Realm.Sync.User.identity. + * @type {string} + */ + get id() {} +} + +/** + * A Role within the permissions system. + * + * A Role consists of a name for the role and a list of users which are members of the role. + * Roles are granted privileges on Realms, Classes and Objects, and in turn grant those + * privileges to all users which are members of the role. + * A role named "everyone" is automatically created in new Realms, and all new users which + * connect to the Realm are automatically added to it. Any other roles you wish to use are + * managed as normal Realm objects. + * @since 2.3.0 + */ +class Role { + /** + * The name of the Role. + * @type {string} + */ + get name() {} + + /** + * The users which belong to the role. + * @type {Array} + */ + get members() {} +} + +/** + * An object which describes class-wide permissions. + * + * An instance of this object is automatically created in the Realm for class in your schema, + * and should not be created manually. + * @since 2.3.0 + */ +class Class { + /** + * The name of the class which these permissions apply to. + * @type {string} + */ + get class_name() {} + + /** + * The permissions for this class. + * @type {Array} + */ + get permissions() {} +} + +/** + * A singleton object which describes Realm-wide permissions. + * + * An object of this type is automatically created in the Realm for you, and more objects + * cannot be created manually. + * @since 2.3.0 + */ +class Realm { + /** + * The permissions for the Realm. + * @type {Array} + */ + get permissions() {} +} diff --git a/docs/realm.js b/docs/realm.js index d4785253..e0a0ddde 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -119,12 +119,43 @@ class Realm { */ static openAsync(config, callback, progressCallback) {} + /** + * Return a configuration for a default synced Realm. The server URL for the user will be used as base for + * the URL for the synced Realm. If no user is supplied, the current user will be used. + * @param {Realm.Sync.User} - an optional sync user + * @throws {Error} if zero or multiple users are logged in + * @returns {Realm~Configuration} - a configuration matching a default synced Realm. + * @since 2.3.0 + */ + static automaticSyncConfiguration(user) {} + /** * Closes this Realm so it may be re-opened with a newer schema version. * All objects and collections from this Realm are no longer valid after calling this method. */ close() {} + /** + * Returns the granted privilges. + * + * This combines all privileges granted on the Realm/Class/Object by all Roles which + * the current User is a member of into the final privileges which will + * be enforced by the server. + * + * The privilege calculation is done locally using cached data, and inherently may + * be stale. It is possible that this method may indicate that an operation is + * permitted but the server will still reject it if permission is revoked before + * the changes have been integrated on the server. + * + * Non-synchronized Realms always have permission to perform all operations. + * + * @param {(Realm~ObjectType|Realm.Object)} arg - the object type or the object to compute priviliges from + * @returns {Object} as the computed priviliges as properties + * @since 2.3.0 + * @see {Realm.Permissions} for details of priviliges and roles. + */ + privileges(arg) {} + /** * Create a new Realm object of the given type and with the specified properties. * @param {Realm~ObjectType} type - The type of Realm object to create. @@ -243,15 +274,16 @@ class Realm { compact() {} /** - * If the Realm is a partially synchronized Realm, fetch and synchronize the objects - * of a given object type that match the given query (in string format). + * Writes a compacted copy of the Realm to the given path. * - * **Partial synchronization is a tech preview. Its APIs are subject to change.** - * @param {Realm~ObjectType} type - The type of Realm objects to retrieve. - * @param {string} query - Query used to filter objects. - * @return {Promise} - a promise that will be resolved with the Realm.Results instance when it's available. + * The destination file cannot already exist. + * + * Note that if this method is called from within a write transaction, the current data is written, + * not the data from the point when the previous write transaction was committed. + * @param {string} path path to save the Realm to + * @param {ArrayBuffer|ArrayBufferView} [encryptionKey] - Optional 64-byte encryption key to encrypt the new file with. */ - subscribeToObjects(className, query, callback) {} + writeCopyTo(path, encryptionKey) {} } /** diff --git a/docs/sync.js b/docs/sync.js index 65d6520f..8afcfddd 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -270,6 +270,55 @@ class User { */ static register(server, username, password, callback) {} + /** + * Request a password reset email to be sent to a user's email. + * This will not throw an exception, even if the email doesn't belong to a Realm Object Server user. + * + * This can only be used for users who authenticated with the 'password' provider, and passed a valid email address as a username. + * + * @param {string} server - authentication server + * @param {string} email - The email that corresponds to the user's username. + * @return {Promise} A promise which is resolved when the request has been sent. + */ + static requestPasswordReset(server, email) {} + + /** + * Complete the password reset flow by using the reset token sent to the user's email as a one-time authorization token to change the password. + * + * By default, Realm Object Server will send a link to the user's email that will redirect to a webpage where they can enter their new password. + * If you wish to provide a native UX, you may wish to modify the password authentication provider to use a custom URL with deep linking, so you can + * open the app, extract the token, and navigate to a view that allows to change the password within the app. + * + * @param {string} server - authentication server + * @param {string} reset_token - The token that was sent to the user's email address. + * @param {string} new_password - The user's new password. + * @return {Promise} A promise which is resolved when the request has been sent. + */ + static completePasswordReset(server, reset_token, new_password) {} + + /** + * Request an email confirmation email to be sent to a user's email. + * This will not throw an exception, even if the email doesn't belong to a Realm Object Server user. + * + * @param {string} server - authentication server + * @param {string} email - The email that corresponds to the user's username. + * @return {Promise} A promise which is resolved when the request has been sent. + */ + static requestEmailConfirmation(server, email) {} + + /** + * Complete the email confirmation flow by using the confirmation token sent to the user's email as a one-time authorization token to confirm their email. + * + * By default, Realm Object Server will send a link to the user's email that will redirect to a webpage where they can enter their new password. + * If you wish to provide a native UX, you may wish to modify the password authentication provider to use a custom URL with deep linking, so you can + * open the app, extract the token, and navigate to a view that allows to confirm the email within the app. + * + * @param {string} server - authentication server + * @param {string} confirmation_token - The token that was sent to the user's email address. + * @return {Promise} A promise which is resolved when the request has been sent. + */ + static confirmEmail(server, confirmation_token) {} + /** * Create an admin user for the given authentication server with an existing token * @param {string} adminToken - existing admin token @@ -468,6 +517,69 @@ class Session { removeProgressNotification(progressCallback) {} } +/** + * An object encapsulating partial sync subscriptions. + * @memberof Realm.Sync + */ +class Subscription { + /** + * Gets the current state of the subscription. + * Can be either: + * - Realm.Sync.SubscriptionState.Error: An error occurred while creating or processing the partial sync subscription. + * - Realm.Sync.SubscriptionState.Creating: The subscription is being created. + * - Realm.Sync.SubscriptionState.Pending: The subscription was created, but has not yet been processed by the sync server. + * - Realm.Sync.SubscriptionState.Complete: The subscription has been processed by the sync server and data is being synced to the device. + * - Realm.Sync.SubscriptionState.Invalidated: The subscription has been removed. + * @type {number} + */ + get state() {} + + /** + * Gets the error message. `undefined` if no error. + * @type {string} + */ + get error() {} + + /** + * Unsubscribe a partial synced `Realm.Results`. The state will change to `Realm.Sync.SubscriptionState.Invalidated`. + * The `Realm.Results` will not produce any meaningful values. Moreover, any objects matching the query will be + * removed if they are not matched by any other query. The object removal is done asynchronously. + */ + unsubscribe() {} + + /** + * Adds a listener `callback` which will be called when the state of the subscription changes. + * @param {function(subscription, state)} callback - A function to be called when changes to the subscription occur. + * @throws {Error} If `callback` is not a function. + * @example + * let subscription = results.subscribe(); + * subscription.addListener((subscription, state) => { + * switch (state) { + * case Realm.Sync.SubscriptionState.Complete: + * // results is ready to be consumed + * break; + * case Realm.Sync.SubscriptionState.Error: + * console.log('An error occurred: ', subscription.error); + * break; + * } + * } + */ + addListener(callback) {} + + /** + * Remove the listener `callback` from the subscription instance. + * @param {function(subscription, state)} callback - Callback function that was previously + * added as a listener through the {@link Subscription#addListener addListener} method. + * @throws {Error} If `callback` is not a function. + */ + removeListener(callback) {} + + /** + * Remove all listeners from the subscription instance. + */ + removeAllListeners() {} +} + /** * A Realm Worker can be used to process Sync events in multiple automatically-managed child processes. * @@ -528,7 +640,7 @@ class Worker { */ class Adapter { /** - * Create a new Adapter to moitor and process changes made across multiple Realms + * Create a new Adapter to monitor and process changes made across multiple Realms * @param {string} localPath - the local path where realm files are stored * @param {string} serverUrl - the sync server to listen to * @param {SyncUser} adminUser - an admin user obtained by calling `new Realm.Sync.User.adminUser` diff --git a/examples/ReactExample/android/app/src/main/java/io/realm/react/example/MainApplication.java b/examples/ReactExample/android/app/src/main/java/io/realm/react/example/MainApplication.java index b220a0d8..2a428589 100644 --- a/examples/ReactExample/android/app/src/main/java/io/realm/react/example/MainApplication.java +++ b/examples/ReactExample/android/app/src/main/java/io/realm/react/example/MainApplication.java @@ -20,7 +20,7 @@ public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override - protected boolean getUseDeveloperSupport() { + public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } diff --git a/lib/browser/constants.js b/lib/browser/constants.js index fe7360fe..3cb55ab6 100644 --- a/lib/browser/constants.js +++ b/lib/browser/constants.js @@ -41,6 +41,7 @@ export const propTypes = {}; 'RESULTS', 'USER', 'SESSION', + 'SUBSCRIPTION', 'UNDEFINED', ].forEach(function(type) { Object.defineProperty(objectTypes, type, { diff --git a/lib/browser/index.js b/lib/browser/index.js index a2e449d2..6ad8643a 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -26,6 +26,7 @@ import Results, { createResults } from './results'; import RealmObject, * as objects from './objects'; import User, { createUser } from './user'; import Session, { createSession } from './session'; +import Subscription, { createSubscription } from './subscription'; import * as rpc from './rpc'; import * as util from './util'; import { static as staticUserMethods } from '../user-methods'; @@ -38,6 +39,7 @@ rpc.registerTypeConverter(objectTypes.OBJECT, objects.createObject); rpc.registerTypeConverter(objectTypes.REALM, createRealm); rpc.registerTypeConverter(objectTypes.USER, createUser); rpc.registerTypeConverter(objectTypes.SESSION, createSession); +rpc.registerTypeConverter(objectTypes.SUBSCRIPTION, createSubscription); function createRealm(_, info) { let realm = Object.create(Realm.prototype); @@ -130,7 +132,6 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ 'close', '_waitForDownload', '_objectForObjectId', - '_subscribeToObjects', ]); // Mutating methods: @@ -147,7 +148,8 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ const Sync = { User, - Session + Session, + Subscription, }; Object.defineProperties(Realm, { diff --git a/lib/browser/results.js b/lib/browser/results.js index a35f9fda..d309d7e8 100644 --- a/lib/browser/results.js +++ b/lib/browser/results.js @@ -30,6 +30,7 @@ createMethods(Results.prototype, objectTypes.RESULTS, [ 'filtered', 'sorted', 'snapshot', + 'subscribe', 'isValid', 'indexOf', 'min', diff --git a/lib/browser/subscription.js b/lib/browser/subscription.js new file mode 100644 index 00000000..3dddeac2 --- /dev/null +++ b/lib/browser/subscription.js @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2018 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. +// +//////////////////////////////////////////////////////////////////////////// + +'use strict'; + +import { objectTypes } from './constants'; +import { getterForProperty, createMethods } from './util'; + +export default class Subscription { + +} + +Object.defineProperties(Subscription.prototype, { + error: { get: getterForProperty('error') }, + state: { get: getterForProperty('state') } +}); + +// // Non-mutating methods: +createMethods(Subscription.prototype, objectTypes.SUBSCRIPTION, [ + 'unsubscribe', + 'addListener', + 'removeListener' +]); diff --git a/lib/browser/user.js b/lib/browser/user.js index 85f34ba1..25627353 100644 --- a/lib/browser/user.js +++ b/lib/browser/user.js @@ -42,7 +42,7 @@ export default class User { } createMethods(User.prototype, objectTypes.USER, [ - 'logout', + '_logout', '_sessionForOnDiskPath' ]); diff --git a/lib/extensions.js b/lib/extensions.js index f1c04695..505bdf51 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -18,6 +18,8 @@ 'use strict'; +const URL = require('url-parse'); + let getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) { return Object.getOwnPropertyNames(obj).reduce(function (descriptors, name) { descriptors[name] = Object.getOwnPropertyDescriptor(obj, name); @@ -149,29 +151,118 @@ module.exports = function(realmConstructor) { setConstructorOnPrototype(realmConstructor.Sync.User); setConstructorOnPrototype(realmConstructor.Sync.Session); + // A configuration for a default Realm + realmConstructor.automaticSyncConfiguration = function() { + let user; + + if (arguments.length === 0) { + let users = this.Sync.User.all; + let identities = Object.keys(users); + if (identities.length === 1) { + user = users[identities[0]]; + } else { + new Error(`One and only one user should be logged in but found ${users.length} users.`); + } + } else if (arguments.length === 1) { + user = arguments[0]; + } else { + new Error(`Zero or one argument expected.`); + } + + let url = new URL(user.server); + let secure = (url.protocol === 'https:')?'s':''; + let port = (url.port === undefined)?'9080':url.port + let realmUrl = `realm${secure}://${url.hostname}:${port}/default`; + + let config = { + sync: { + user: user, + url: realmUrl, + partial: true + } + }; + return config; + } + if (realmConstructor.Sync._setFeatureToken) { realmConstructor.Sync.setFeatureToken = function(featureToken) { - if (typeof featureToken !== 'string' && !(featureToken instanceof String)) { - throw new Error("featureToken should be a string"); - } - - realmConstructor.Sync._setFeatureToken(featureToken.trim()); + console.log('Realm.Sync.setFeatureToken() is deprecated and you can remove any calls to it.'); } } - realmConstructor.prototype.subscribeToObjects = function(objectType, query) { - const realm = this; - let promise = new Promise((resolve, reject) => { - realm._subscribeToObjects(objectType, query, function(err, results) { - if (err) { - reject(err); - } else { - resolve(results); - } - }); - }); - return promise; + // Keep these value in sync with subscription_state.hpp + realmConstructor.Sync.SubscriptionState = { + Error: -1, // An error occurred while creating or processing the partial sync subscription. + Creating: 2, // The subscription is being created. + Pending: 0, // The subscription was created, but has not yet been processed by the sync server. + Complete: 1, // The subscription has been processed by the sync server and data is being synced to the device. + Invalidated: 3, // The subscription has been removed. }; + + // Define the permission schemas as constructors so that they can be + // passed into directly to functions which want object type names + const permissionsSchema = Object.freeze({ + Class: function() {}, + Permission: function() {}, + Realm: function() {}, + Role: function() {}, + User: function() {}, + }); + permissionsSchema.Permission.schema = Object.freeze({ + name: '__Permission', + properties: { + role: '__Role', + canRead: {type: 'bool', default: false}, + canUpdate: {type: 'bool', default: false}, + canDelete: {type: 'bool', default: false}, + canSetPermissions: {type: 'bool', default: false}, + canQuery: {type: 'bool', default: false}, + canCreate: {type: 'bool', default: false}, + canModifySchema: {type: 'bool', default: false}, + } + }); + + permissionsSchema.User.schema = Object.freeze({ + name: '__User', + primaryKey: 'id', + properties: { + id: 'string', + role: '__Role' + } + }); + + permissionsSchema.Role.schema = Object.freeze({ + name: '__Role', + primaryKey: 'name', + properties: { + name: 'string', + members: '__User[]' + } + }); + + permissionsSchema.Class.schema = Object.freeze({ + name: '__Class', + primaryKey: 'class_name', + properties: { + class_name: 'string', + permissions: '__Permission[]' + } + }); + + permissionsSchema.Realm.schema = Object.freeze({ + name: '__Realm', + primaryKey: 'id', + properties: { + id: 'int', + permissions: '__Permission[]' + } + }); + if (!realmConstructor.Permissions) { + Object.defineProperty(realmConstructor, 'Permissions', { + value: permissionsSchema, + configurable: false + }); + } } // TODO: Remove this now useless object. diff --git a/lib/index.d.ts b/lib/index.d.ts index 9e59bd73..7f80ad01 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -81,7 +81,7 @@ declare namespace Realm { path?: string; readOnly?: boolean; inMemory?: boolean; - schema?: ObjectClass[] | ObjectSchema[]; + schema?: (ObjectClass | ObjectSchema)[]; schemaVersion?: number; sync?: Realm.Sync.SyncConfiguration; deleteRealmIfMigrationNeeded?: boolean; @@ -161,6 +161,11 @@ declare namespace Realm { sorted(descriptor: SortDescriptor[]): Results; sorted(descriptor: string, reverse?: boolean): Results; + /** + * @returns Results + */ + subscribe(subscriptionName?: string): Realm.Sync.Subscription; + /** * @returns Results */ @@ -291,6 +296,14 @@ declare namespace Realm.Sync { static registerWithProvider(server: string, options: { provider: string, providerToken: string, userInfo: any }, callback: (error: Error | null, user: User | null) => void): void; static registerWithProvider(server: string, options: { provider: string, providerToken: string, userInfo: any }): Promise; + static requestPasswordReset(server: string, email: string): Promise; + + static completePasswordReset(server:string, reset_token:string, new_password:string): Promise; + + static requestEmailConfirmation(server:string, email:string): Promise; + + static confirmEmail(server:string, confirmation_token:string): Promise; + authenticate(server: string, provider: string, options: any): Promise; logout(): void; openManagementRealm(): Realm; @@ -374,6 +387,7 @@ declare namespace Realm.Sync { open_ssl_verify_callback?: SSLVerifyCallback; error?: ErrorCallback; partial?: boolean; + _disablePartialSyncUrlChecks?:boolean; } type ProgressNotificationCallback = (transferred: number, transferable: number) => void; @@ -394,6 +408,30 @@ declare namespace Realm.Sync { removeProgressNotification(progressCallback: ProgressNotificationCallback): void; } + type SubscriptionNotificationCallback = (subscription: Subscription, state: number) => void; + + /** + * Subscription + * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Subscription.html } + */ + class Subscription { + readonly state: SubscriptionState; + readonly error: string; + + unsubscribe(): void; + addListener(subscruptionCallback: SubscriptionNotificationCallback): void; + removeListener(subscruptionCallback: SubscriptionNotificationCallback): void; + removeAllListeners(): void; + } + + enum SubscriptionState { + Error, + Creating, + Pending, + Complete, + Invalidated, + } + /** * AuthError * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.AuthError.html } @@ -420,6 +458,10 @@ declare namespace Realm.Sync { function removeListener(regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): Promise; function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void; function initiateClientReset(path: string): void; + + /** + * @deprecated, to be removed in future versions + */ function setFeatureToken(token: string): void; type Instruction = { @@ -455,6 +497,42 @@ declare namespace Realm.Sync { } } +declare namespace Realm.Permissions { + class Permission { + static schema: ObjectSchema; + + identity: string; + canRead: boolean; + canUpdate: boolean; + canDelete: boolean; + canSetPermissions: boolean; + canQuery: boolean; + canCreate: boolean; + canModifySchema: boolean; + } + + class User { + static schema: ObjectSchema; + identity: string; + } + + class Role { + static schema: ObjectSchema; + name: string; + members: User[]; + } + + class Class { + static schema: ObjectSchema; + class_name: string; + permissions: Permission[]; + } + + class Realm { + static schema: ObjectSchema; + permissions: Permission[]; + } +} interface ProgressPromise extends Promise { progress(callback: Realm.Sync.ProgressNotificationCallback): Promise @@ -497,11 +575,17 @@ declare class Realm { */ static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void, progressCallback?: Realm.Sync.ProgressNotificationCallback): void + /** + * Return a configuration for a default Realm. + * @param {Realm.Sync.User} optional user. + */ + static automaticSyncConfiguration(user?: Realm.Sync.User): string; + /** * Delete the Realm file for the given configuration. * @param {Configuration} config */ - static deleteFile(config: Realm.Configuration): void + static deleteFile(config: Realm.Configuration): void; /** * @param {Realm.Configuration} config? @@ -524,7 +608,7 @@ declare class Realm { * @param {boolean} update? * @returns T */ - create(type: string | Realm.ObjectClass | Function, properties: T & Realm.ObjectPropsType, update?: boolean): T; + create(type: string | Realm.ObjectClass | Function, properties: T | Realm.ObjectPropsType, update?: boolean): T; /** * @param {Realm.Object|Realm.Object[]|Realm.List|Realm.Results|any} object @@ -602,9 +686,16 @@ declare class Realm { compact(): boolean; /** - * @returns Promise> + * Write a copy to destination path + * @param path destination path + * @param encryptionKey encryption key to use + * @returns void */ - subscribeToObjects(objectType: string, query: string): Promise>; + writeCopyTo(path: string, encryptionKey?: ArrayBuffer | ArrayBufferView): void; + + privileges() : Realm.Permissions.Realm; + privileges(objectType: string | Realm.ObjectSchema | Function) : Realm.Permissions.Class; + privileges(obj: Realm.Object) : Realm.Permissions.Class; } declare module 'realm' { diff --git a/lib/notification-worker.js b/lib/notification-worker.js index 84191b3b..5aa6b4ae 100644 --- a/lib/notification-worker.js +++ b/lib/notification-worker.js @@ -25,7 +25,7 @@ function nodeRequire(module) { return require_method(module); } -const Realm = require('.'); +const Realm = nodeRequire('.'); let impl; process.on('message', (m) => { diff --git a/lib/notifier.js b/lib/notifier.js index a6bc6dd4..0add6bf6 100644 --- a/lib/notifier.js +++ b/lib/notifier.js @@ -85,7 +85,7 @@ class FunctionListener { throw e; }); } -}; +} class OutOfProcListener { constructor(regex, regexStr, worker) { @@ -120,7 +120,7 @@ class OutOfProcListener { } this.worker.onchange(changes); } -}; +} class Listener { constructor(Sync, server, user) { @@ -231,7 +231,7 @@ class Listener { } this.initPromises = []; } -}; +} let listener; function addListener(server, user, regex, event, callback) { diff --git a/lib/user-methods.js b/lib/user-methods.js index 2367837d..f83b2a86 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -98,7 +98,7 @@ function print_error() { function validateRefresh(user, localRealmPath, response, json) { let session = user._sessionForOnDiskPath(localRealmPath); if (!session) { - print_error(`Unhandled session token refresh error: could not look up session at path ${localRealmPath}`); + print_error(`Unhandled session token refresh error: could not look up session for user ${user.identity} at path ${localRealmPath}`); return; } @@ -108,7 +108,7 @@ function validateRefresh(user, localRealmPath, response, json) { if (errorHandler) { errorHandler(session, error); } else { - print_error('Unhandled session token refresh error', error); + print_error(`Unhandled session token refresh error for user ${user.identity} at path ${localRealmPath}`, error); } return; } @@ -155,6 +155,7 @@ function refreshAdminToken(user, localRealmPath, realmUrl) { }) .catch((e) => { print_error(e); + setTimeout(() => refreshAccessToken(user, localRealmPath, realmUrl), 10 * 1000); }); } @@ -274,6 +275,32 @@ function _authenticate(userConstructor, server, json, callback) { } } +function _updateAccount(userConstructor, server, json) { + const url = append_url(server, 'auth/password/updateAccount'); + const options = { + method: 'POST', + body: JSON.stringify(json), + headers: postHeaders, + open_timeout: 5000 + }; + + return performFetch(url, options) + .then((response) => { + const contentType = response.headers.get('Content-Type'); + if (contentType.indexOf('application/json') === -1) { + return response.text().then((body) => { + throw new AuthError({ + title: `Could not update user account: Realm Object Server didn't respond with valid JSON`, + body, + }); + }); + } + if (!response.ok) { + return response.json().then((body) => Promise.reject(new AuthError(body))); + } + }); +} + const staticMethods = { get current() { const allUsers = this.all; @@ -378,11 +405,77 @@ const staticMethods = { return _authenticate(this, server, json) }, + requestPasswordReset(server, email) { + checkTypes(arguments, ['string', 'string']); + const json = { + provider_id: email, + data: { action: 'reset_password' } + }; + + return _updateAccount(this, server, json); + }, + + completePasswordReset(server, reset_token, new_password) { + checkTypes(arguments, ['string', 'string']); + const json = { + data: { + action: 'complete_reset', + token: reset_token, + new_password: new_password + } + }; + + return _updateAccount(this, server, json); + }, + + requestEmailConfirmation(server, email) { + checkTypes(arguments, ['string', 'string']); + const json = { + provider_id: email, + data: { action: 'request_email_confirmation' } + }; + + return _updateAccount(this, server, json); + }, + + confirmEmail(server, confirmation_token) { + checkTypes(arguments, ['string', 'string']); + const json = { + data: { + action: 'confirm_email', + token: confirmation_token + } + }; + + return _updateAccount(this, server, json); + }, + _refreshAccessToken: refreshAccessToken, }; const instanceMethods = { + logout() { + this._logout(); + const url = url_parse(this.server); + url.set('pathname', '/auth/revoke'); + const headers = { + Authorization: this.token + }; + const body = JSON.stringify({ + token: this.token + }); + const options = { + method: 'POST', + headers, + body: body, + open_timeout: 5000 + }; + + performFetch(url.href, options) + .then(() => console.log('User is logged out')) + .catch((e) => print_error(e)); + }, openManagementRealm() { let url = url_parse(this.server); if (url.protocol === 'http:') { diff --git a/lib/worker.js b/lib/worker.js index 77a8f8bc..eec34c50 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -123,6 +123,6 @@ class Worker { const message = this._workQueue.shift(); worker.send(message); } -}; +} module.exports = Worker; diff --git a/package.json b/package.json index eb0b1d00..fa0b1379 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "realm", "description": "Realm is a mobile database: an alternative to SQLite and key-value stores", - "version": "2.2.20", + "version": "2.4.0", "license": "Apache-2.0", "homepage": "https://realm.io", "keywords": [ @@ -90,7 +90,7 @@ "request": "^2.78.0", "stream-counter": "^1.0.0", "sync-request": "^3.0.1", - "url-parse": "^1.1.7" + "url-parse": "^1.2.0" }, "devDependencies": { "@types/node": "^4.0.35", diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java index f09f65ec..9b84da3f 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 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. + */ + package io.realm.react; import android.content.res.AssetManager; diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java b/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java index 9fb66f9d..a722b059 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactPackage.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018 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. + */ + package io.realm.react; import com.facebook.react.ReactPackage; diff --git a/react-native/android/src/main/java/io/realm/react/util/SSLHelper.java b/react-native/android/src/main/java/io/realm/react/util/SSLHelper.java new file mode 100644 index 00000000..df49dd72 --- /dev/null +++ b/react-native/android/src/main/java/io/realm/react/util/SSLHelper.java @@ -0,0 +1,152 @@ +/* + * Copyright 2018 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. + */ + +package io.realm.react.util; + +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.internal.tls.OkHostnameVerifier; + +public class SSLHelper { + private final static String TAG = "REALM SSLHelper"; + // Holds the certificate chain (per hostname). We need to keep the order of each certificate + // according to it's depth in the chain. The depth of the last + // certificate is 0. The depth of the first certificate is chain + // length - 1. + private static HashMap> ROS_CERTIFICATES_CHAIN; + + // The default Android Trust Manager which uses the default KeyStore to + // validate the certificate chain. + private static X509TrustManager TRUST_MANAGER; + + // Help transform a String PEM representation of the certificate, into + // X509Certificate format. + private static CertificateFactory CERTIFICATE_FACTORY; + + // From Sync implementation: + // A recommended way of using the callback function is to return true + // if preverify_ok = 1 and depth > 0, + // always check the host name if depth = 0, + // and use an independent verification step if preverify_ok = 0. + // + // Another possible way of using the callback is to collect all the + // ROS_CERTIFICATES_CHAIN until depth = 0, and present the entire chain for + // independent verification. + // + // In this implementation we use the second method, since it's more suitable for + // the underlying Java API we need to call to validate the certificate chain. + + public synchronized static boolean certificateVerifier(String serverAddress, String pemData, int depth) { + try { + if (ROS_CERTIFICATES_CHAIN == null) { + ROS_CERTIFICATES_CHAIN = new HashMap<>(); + TRUST_MANAGER = systemDefaultTrustManager(); + CERTIFICATE_FACTORY = CertificateFactory.getInstance("X.509"); + } + + if (!ROS_CERTIFICATES_CHAIN.containsKey(serverAddress)) { + ROS_CERTIFICATES_CHAIN.put(serverAddress, new ArrayList()); + } + + ROS_CERTIFICATES_CHAIN.get(serverAddress).add(pemData); + + if (depth == 0) { + // transform all PEM ROS_CERTIFICATES_CHAIN into Java X509 + // with respecting the order/depth provided from Sync. + List pemChain = ROS_CERTIFICATES_CHAIN.get(serverAddress); + int n = pemChain.size(); + X509Certificate[] chain = new X509Certificate[n]; + for (String pem : pemChain) { + // The depth of the last certificate is 0. + // The depth of the first certificate is chain length - 1. + chain[--n] = buildCertificateFromPEM(pem); + } + + // verify the entire chain + try { + TRUST_MANAGER.checkServerTrusted(chain, "RSA"); + // verify the hostname + boolean isValid = OkHostnameVerifier.INSTANCE.verify(serverAddress, chain[0]); + if (isValid) { + return true; + } else { + Log.e(TAG, "Can not verify the hostname for the host: " + serverAddress); + return false; + } + } catch (CertificateException e) { + Log.e(TAG, "Can not validate SSL chain certificate for the host: " + serverAddress, e); + return false; + } finally { + // don't keep the certificate chain in memory + ROS_CERTIFICATES_CHAIN.remove(serverAddress); + } + } else { + // return true, since the verification will happen for the entire chain + // when receiving the depth == 0 (host certificate) + return true; + } + } catch (Exception e) { + Log.e(TAG, "Error during certificate validation for host: " + serverAddress, e); + return false; + } + } + + // Credit OkHttp https://github.com/square/okhttp/blob/e5c84e1aef9572adb493197c1b6c4e882aca085b/okhttp/src/main/java/okhttp3/OkHttpClient.java#L270 + private static X509TrustManager systemDefaultTrustManager() { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + return (X509TrustManager) trustManagers[0]; + } catch (GeneralSecurityException e) { + throw new AssertionError(); // The system has no TLS. Just give up. + } + } + + private static X509Certificate buildCertificateFromPEM(String pem) throws IOException, CertificateException { + InputStream stream = null; + try { + stream = new ByteArrayInputStream(pem.getBytes("UTF-8")); + return (X509Certificate) CERTIFICATE_FACTORY.generateCertificate(stream); + } finally { + if (stream != null) { + stream.close(); + } + } + } +} diff --git a/react-native/android/src/main/jni/Android.mk b/react-native/android/src/main/jni/Android.mk index 87ced46c..a3d727c3 100644 --- a/react-native/android/src/main/jni/Android.mk +++ b/react-native/android/src/main/jni/Android.mk @@ -45,6 +45,7 @@ LOCAL_SRC_FILES += src/rpc.cpp LOCAL_SRC_FILES += src/jsc/jsc_init.cpp LOCAL_SRC_FILES += src/jsc/jsc_value.cpp LOCAL_SRC_FILES += src/android/io_realm_react_RealmReactModule.cpp +LOCAL_SRC_FILES += src/android/jni_utils.cpp LOCAL_SRC_FILES += src/android/jsc_override.cpp LOCAL_SRC_FILES += src/android/platform.cpp LOCAL_SRC_FILES += src/object-store/src/impl/collection_change_builder.cpp @@ -78,6 +79,7 @@ LOCAL_SRC_FILES += src/object-store/src/sync/sync_user.cpp LOCAL_SRC_FILES += src/object-store/src/sync/sync_permission.cpp LOCAL_SRC_FILES += src/object-store/src/sync/impl/sync_file.cpp LOCAL_SRC_FILES += src/object-store/src/sync/impl/sync_metadata.cpp +LOCAL_SRC_FILES += src/object-store/src/sync/impl/work_queue.cpp endif LOCAL_C_INCLUDES := src diff --git a/realm.gypi b/realm.gypi index 94b8c297..45aa4dfb 100644 --- a/realm.gypi +++ b/realm.gypi @@ -99,6 +99,7 @@ "src/object-store/src/sync/impl/sync_client.hpp", "src/object-store/src/sync/impl/sync_file.hpp", "src/object-store/src/sync/impl/sync_metadata.hpp", + "src/object-store/src/sync/impl/work_queue.hpp", "src/object-store/src/sync/partial_sync.hpp", "src/object-store/src/sync/sync_config.hpp", "src/object-store/src/sync/sync_manager.hpp", @@ -147,7 +148,8 @@ "src/object-store/src/sync/sync_session.cpp", "src/object-store/src/sync/sync_config.cpp", "src/object-store/src/sync/impl/sync_file.cpp", - "src/object-store/src/sync/impl/sync_metadata.cpp" + "src/object-store/src/sync/impl/sync_metadata.cpp", + "src/object-store/src/sync/impl/work_queue.cpp" ], }] ], diff --git a/scripts/test.sh b/scripts/test.sh index f4bd0784..4cfc3dfc 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -147,7 +147,17 @@ xctest() { echo "Launching application. (output is in $(pwd)/build/out.txt)" - xcrun simctl launch --console ${SIM_DEVICE_NAME} io.realm.$1 | tee $(pwd)/build/out.txt + testpid=$(xcrun simctl launch --stdout=$(pwd)/build/out.txt --stderr=$(pwd)/build/err.txt ${SIM_DEVICE_NAME} io.realm.$1 | grep -m1 -o '\d\+$') + tail -n +0 -f $(pwd)/build/out.txt & + stdoutpid=$! + tail -n +0 -f $(pwd)/build/err.txt & + stderrpid=$! + + # `kill -0` checks if a signal can be sent to the pid without actually doing so + while kill -0 $testpid 2> /dev/null; do sleep 1; done + + kill $stdoutpid + kill $stderrpid echo "Shuttting down ${SIM_DEVICE_NAME} simulator. (device is not deleted. you can use it to debug the app)" shutdown_ios_simulator @@ -171,7 +181,7 @@ setup_ios_simulator() { delete_ios_simulator >/dev/null 2>&1 #parse devices - IOS_RUNTIME=$(xcrun simctl list runtimes | grep -m1 -o 'com.apple.CoreSimulator.SimRuntime.iOS.*' | sed 's/[()]//g') + IOS_RUNTIME=$(xcrun simctl list runtimes | grep -v unavailable | grep -m1 -o 'com.apple.CoreSimulator.SimRuntime.iOS.*' | sed 's/[()]//g') echo using iOS Runtime ${IOS_RUNTIME} to create new simulator ${SIM_DEVICE_NAME} #create new test simulator diff --git a/src/RealmJS.xcodeproj/project.pbxproj b/src/RealmJS.xcodeproj/project.pbxproj index d77654f6..7ca13e1e 100644 --- a/src/RealmJS.xcodeproj/project.pbxproj +++ b/src/RealmJS.xcodeproj/project.pbxproj @@ -41,6 +41,8 @@ 3FCE2A931F58BE0300D4855B /* uuid.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A911F58BDFF00D4855B /* uuid.cpp */; }; 3FCE2A971F58BE2200D4855B /* sync_permission.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A951F58BE1D00D4855B /* sync_permission.cpp */; }; 420FB79F1F7FBFE900D43D0F /* partial_sync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 423737AF1F7E333400FAEDFF /* partial_sync.cpp */; }; + 425A121120235A1400C2F932 /* work_queue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 425A120F20235A1400C2F932 /* work_queue.cpp */; }; + 4261AF8E203C42000052450D /* work_queue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 425A120F20235A1400C2F932 /* work_queue.cpp */; }; 502B07E41E2CD201007A84ED /* object.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 502B07E31E2CD1FA007A84ED /* object.cpp */; }; 504CF85E1EBCAE3600A9A4B6 /* network_reachability_observer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 504CF8541EBCAE3600A9A4B6 /* network_reachability_observer.cpp */; }; 504CF85F1EBCAE3600A9A4B6 /* system_configuration.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 504CF8561EBCAE3600A9A4B6 /* system_configuration.cpp */; }; @@ -185,6 +187,8 @@ 3FCE2A991F58BE3600D4855B /* feature_checks.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = feature_checks.hpp; path = "object-store/src/feature_checks.hpp"; sourceTree = SOURCE_ROOT; }; 423737AF1F7E333400FAEDFF /* partial_sync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = partial_sync.cpp; path = src/sync/partial_sync.cpp; sourceTree = ""; }; 423737B01F7E333400FAEDFF /* partial_sync.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = partial_sync.hpp; path = src/sync/partial_sync.hpp; sourceTree = ""; }; + 425A120F20235A1400C2F932 /* work_queue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = work_queue.cpp; sourceTree = ""; }; + 425A121020235A1400C2F932 /* work_queue.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = work_queue.hpp; sourceTree = ""; }; 426FCDFF1F7DA2F9005565DC /* sync_config.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sync_config.cpp; path = src/sync/sync_config.cpp; sourceTree = ""; }; 502B07E31E2CD1FA007A84ED /* object.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = object.cpp; path = src/object.cpp; sourceTree = ""; }; 502B07E51E2CD20D007A84ED /* object.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = object.hpp; path = src/object.hpp; sourceTree = ""; }; @@ -474,6 +478,8 @@ 504CF8521EBCAE3600A9A4B6 /* impl */ = { isa = PBXGroup; children = ( + 425A120F20235A1400C2F932 /* work_queue.cpp */, + 425A121020235A1400C2F932 /* work_queue.hpp */, 504CF8531EBCAE3600A9A4B6 /* apple */, 504CF8581EBCAE3600A9A4B6 /* network_reachability.hpp */, 504CF8591EBCAE3600A9A4B6 /* sync_client.hpp */, @@ -873,6 +879,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4261AF8E203C42000052450D /* work_queue.cpp in Sources */, F63FF2E21C15921A00B3B8E0 /* base64.cpp in Sources */, 022BF1021E7266DF00F382F1 /* binding_callback_thread_observer.cpp in Sources */, 02414BA51CE6ABCF00A8669F /* collection_change_builder.cpp in Sources */, @@ -934,6 +941,7 @@ F63FF31E1C1642BB00B3B8E0 /* GCDWebServerRequest.m in Sources */, F63FF31F1C1642BB00B3B8E0 /* GCDWebServerResponse.m in Sources */, F63FF3271C1642BB00B3B8E0 /* GCDWebServerStreamedResponse.m in Sources */, + 425A121120235A1400C2F932 /* work_queue.cpp in Sources */, F63FF3231C1642BB00B3B8E0 /* GCDWebServerURLEncodedFormRequest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -961,7 +969,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2.2.20; + CURRENT_PROJECT_VERSION = 2.4.0; CXX = "$(SRCROOT)/../scripts/ccache-clang++.sh"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1025,7 +1033,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 2.2.20; + CURRENT_PROJECT_VERSION = 2.4.0; CXX = "$(SRCROOT)/../scripts/ccache-clang++.sh"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -1115,10 +1123,7 @@ "-isystem", "$(SRCROOT)/../vendor/sync/include", ); - OTHER_LIBTOOLFLAGS = ( - "$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios-dbg.a", - "$(SRCROOT)/../vendor/realm-ios/librealm-ios-dbg.a", - ); + OTHER_LIBTOOLFLAGS = "$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios-dbg.a $(SRCROOT)/../vendor/realm-ios/librealm-ios-dbg.a"; PRODUCT_NAME = RealmJS; SKIP_INSTALL = YES; }; @@ -1140,10 +1145,7 @@ "-isystem", "$(SRCROOT)/../vendor/sync/include", ); - OTHER_LIBTOOLFLAGS = ( - "$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios.a", - "$(SRCROOT)/../vendor/realm-ios/librealm-ios.a", - ); + OTHER_LIBTOOLFLAGS = "$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios.a $(SRCROOT)/../vendor/realm-ios/librealm-ios.a"; PRODUCT_NAME = RealmJS; SKIP_INSTALL = YES; }; diff --git a/src/android/io_realm_react_RealmReactModule.cpp b/src/android/io_realm_react_RealmReactModule.cpp index e776bca0..78c4e730 100644 --- a/src/android/io_realm_react_RealmReactModule.cpp +++ b/src/android/io_realm_react_RealmReactModule.cpp @@ -23,17 +23,49 @@ #include "io_realm_react_RealmReactModule.h" #include "rpc.hpp" #include "platform.hpp" +#include "jni_utils.hpp" using namespace realm::rpc; +using namespace realm::jni_util; static RPCServer *s_rpc_server; extern bool realmContextInjected; +jclass ssl_helper_class; namespace realm { // set the AssetManager used to access bundled files within the APK void set_asset_manager(AAssetManager* assetManager); } +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) +{ + JNIEnv* env; + if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; + } + else { + JniUtils::initialize(vm, JNI_VERSION_1_6); + } + + // We do lookup the class in this Thread, since FindClass sometimes fails + // when issued from the sync client thread + ssl_helper_class = reinterpret_cast(env->NewGlobalRef(env->FindClass("io/realm/react/util/SSLHelper"))); + + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*) +{ + JNIEnv* env; + if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return; + } + else { + env->DeleteLocalRef(ssl_helper_class); + JniUtils::release(); + } +} + JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_setDefaultRealmFileDirectory (JNIEnv *env, jclass, jstring fileDir, jobject javaAssetManager) { diff --git a/src/android/jni_utils.cpp b/src/android/jni_utils.cpp new file mode 100644 index 00000000..a9ea88da --- /dev/null +++ b/src/android/jni_utils.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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. + */ + +#include "jni_utils.hpp" + +#include + +using namespace realm::jni_util; + +static std::unique_ptr s_instance; + +void JniUtils::initialize(JavaVM* vm, jint vm_version) noexcept +{ + s_instance = std::unique_ptr(new JniUtils(vm, vm_version)); +} + +void JniUtils::release() +{ + s_instance.release(); +} + +JNIEnv* JniUtils::get_env(bool attach_if_needed) +{ + JNIEnv* env; + if (s_instance->m_vm->GetEnv(reinterpret_cast(&env), s_instance->m_vm_version) != JNI_OK) { + if (attach_if_needed) { + jint ret = s_instance->m_vm->AttachCurrentThread(reinterpret_cast(&env), nullptr); + } + } + + return env; +} + +void JniUtils::detach_current_thread() +{ + s_instance->m_vm->DetachCurrentThread(); +} diff --git a/src/android/jni_utils.hpp b/src/android/jni_utils.hpp new file mode 100644 index 00000000..9c8869fc --- /dev/null +++ b/src/android/jni_utils.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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. + */ + +#ifndef REALM_JNI_UTIL_JNI_UTILS_HPP +#define REALM_JNI_UTIL_JNI_UTILS_HPP + +#include + +#include + +namespace realm { +namespace jni_util { + +// Util functions for JNI. +class JniUtils { +public: + ~JniUtils() + { + } + + // Call this only once in JNI_OnLoad. + static void initialize(JavaVM* vm, jint vm_version) noexcept; + // Call this in JNI_OnUnload. + static void release(); + // When attach_if_needed is false, returns the JNIEnv if there is one attached to this thread. Assert if there is + // none. When attach_if_needed is true, try to attach and return a JNIEnv if necessary. + static JNIEnv* get_env(bool attach_if_needed = false); + // Detach the current thread from the JVM. Only required for C++ threads that where attached in the first place. + // Failing to do so is a resource leak. + static void detach_current_thread(); + +private: + JniUtils(JavaVM* vm, jint vm_version) noexcept + : m_vm(vm) + , m_vm_version(vm_version) + { + } + + JavaVM* m_vm; + jint m_vm_version; +}; + +} // namespace realm +} // namespace jni_util + +#endif // REALM_JNI_UTIL_JNI_UTILS_HPP diff --git a/src/js_collection.hpp b/src/js_collection.hpp index 4e537791..a83db75f 100644 --- a/src/js_collection.hpp +++ b/src/js_collection.hpp @@ -23,6 +23,9 @@ #include "js_observable.hpp" #include "collection_notifications.hpp" +#if REALM_ENABLE_SYNC +#include "sync/subscription_state.hpp" +#endif namespace realm { namespace js { @@ -39,7 +42,7 @@ struct CollectionClass : ClassDefinition> { using Value = js::Value; std::string const name = "Collection"; - + static inline ValueType create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set); }; @@ -48,7 +51,7 @@ typename T::Value CollectionClass::create_collection_change_set(ContextType c { ObjectType object = Object::create_empty(ctx); std::vector deletions, insertions, modifications; - + if (change_set.deletions.count() == std::numeric_limits::max()) { deletions.push_back(Value::from_null(ctx)); } @@ -58,12 +61,12 @@ typename T::Value CollectionClass::create_collection_change_set(ContextType c } } Object::set_property(ctx, object, "deletions", Object::create_array(ctx, deletions)); - + for (auto index : change_set.insertions.as_indexes()) { insertions.push_back(Value::from_number(ctx, index)); } Object::set_property(ctx, object, "insertions", Object::create_array(ctx, insertions)); - + for (auto index : change_set.modifications.as_indexes()) { modifications.push_back(Value::from_number(ctx, index)); } diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 5a434e64..d38f783c 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -35,7 +35,6 @@ #include "js_sync.hpp" #include "sync/sync_config.hpp" #include "sync/sync_manager.hpp" -#include "sync/partial_sync.hpp" #endif #include "shared_realm.hpp" @@ -184,11 +183,10 @@ public: static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); static void close(ContextType, ObjectType, Arguments, ReturnValue &); static void compact(ContextType, ObjectType, Arguments, ReturnValue &); + static void writeCopyTo(ContextType, ObjectType, Arguments, ReturnValue &); static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&); -#if REALM_ENABLE_SYNC - static void subscribe_to_objects(ContextType, ObjectType, Arguments, ReturnValue &); -#endif + static void privileges(ContextType, ObjectType, Arguments, ReturnValue&); // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -244,11 +242,12 @@ public: {"removeAllListeners", wrap}, {"close", wrap}, {"compact", wrap}, + {"writeCopyTo", wrap}, {"deleteModel", wrap}, + {"privileges", wrap}, {"_objectForObjectId", wrap}, #if REALM_ENABLE_SYNC {"_waitForDownload", wrap}, - {"_subscribeToObjects", wrap}, #endif }; @@ -295,7 +294,8 @@ public: return name; } - static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value, std::string& object_type) { + static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value) { + std::string object_type; if (Value::is_constructor(ctx, value)) { FunctionType constructor = Value::to_constructor(ctx, value); @@ -545,7 +545,7 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t template SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Config config, bool schema_updated, - ObjectDefaultsMap && defaults, ConstructorMap && constructors) { + ObjectDefaultsMap&& defaults, ConstructorMap&& constructors) { config.execution_context = Context::get_execution_context_id(ctx); SharedRealm realm; @@ -555,9 +555,6 @@ SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Co catch (const RealmFileException& ex) { handleRealmFileException(ctx, config, ex); } - catch (...) { - throw; - } GlobalContextType global_context = Context::get_global_context(ctx); if (!realm->m_binding_context) { @@ -794,10 +791,8 @@ void RealmClass::objects(ContextType ctx, ObjectType this_object, Arguments a args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); - std::string object_type; - validated_object_schema_for_value(ctx, realm, args[0], object_type); - - return_value.set(ResultsClass::create_instance(ctx, realm, object_type)); + auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]); + return_value.set(ResultsClass::create_instance(ctx, realm, object_schema.name)); } template @@ -806,7 +801,7 @@ void RealmClass::object_for_primary_key(ContextType ctx, ObjectType this_obje SharedRealm realm = *get_internal>(this_object); std::string object_type; - auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type); + auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0]); NativeAccessor accessor(ctx, realm, object_schema); auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]); @@ -824,8 +819,7 @@ void RealmClass::create(ContextType ctx, ObjectType this_object, Arguments ar SharedRealm realm = *get_internal>(this_object); realm->verify_open(); - std::string object_type; - auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type); + auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0]); ObjectType object = Value::validated_to_object(ctx, args[1], "properties"); if (Value::is_array(ctx, args[1])) { @@ -902,7 +896,13 @@ void RealmClass::delete_all(ContextType ctx, ObjectType this_object, Argument } for (auto objectSchema : realm->schema()) { - ObjectStore::table_for_object_type(realm->read_group(), objectSchema.name)->clear(); + auto table = ObjectStore::table_for_object_type(realm->read_group(), objectSchema.name); + if (realm->is_partial()) { + realm::Results(realm, *table).clear(); + } + else { + table->clear(); + } } } @@ -1006,35 +1006,35 @@ void RealmClass::compact(ContextType ctx, ObjectType this_object, Arguments a return_value.set(realm->compact()); } -#if REALM_ENABLE_SYNC -namespace { +template +void RealmClass::writeCopyTo(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(2); -// FIXME: Sync should provide this: https://github.com/realm/realm-sync/issues/1796 -inline sync::ObjectID object_id_from_string(std::string const& string) -{ - if (string.front() != '{' || string.back() != '}') - throw std::invalid_argument("Invalid object ID."); - - size_t dash_index = string.find('-'); - if (dash_index == std::string::npos) - throw std::invalid_argument("Invalid object ID."); - - std::string high_string = string.substr(1, dash_index - 1); - std::string low_string = string.substr(dash_index + 1, string.size() - dash_index - 2); - - if (high_string.size() == 0 || high_string.size() > 16 || low_string.size() == 0 || low_string.size() > 16) - throw std::invalid_argument("Invalid object ID."); - - auto isxdigit = static_cast(std::isxdigit); - if (!std::all_of(high_string.begin(), high_string.end(), isxdigit) || - !std::all_of(low_string.begin(), low_string.end(), isxdigit)) { - throw std::invalid_argument("Invalid object ID."); + if (args.count == 0) { + throw std::runtime_error("At least path has to be provided for 'writeCopyTo'"); } - return sync::ObjectID(strtoull(high_string.c_str(), nullptr, 16), strtoull(low_string.c_str(), nullptr, 16)); -} -} // unnamed namespace -#endif // REALM_ENABLE_SYNC + SharedRealm realm = *get_internal>(this_object); + + ValueType pathValue = args[0]; + if (!Value::is_string(ctx, pathValue)) { + throw std::runtime_error("Argument to 'writeCopyTo' must be a String."); + } + + std::string path = Value::validated_to_string(ctx, pathValue); + BinaryData key; + if (args.count == 2) { + ValueType key_value = args[1]; + if (!Value::is_binary(ctx, key_value)) { + throw std::runtime_error("Encryption key for 'writeCopyTo' must be a Binary."); + } + + auto key_data = Value::validated_to_binary(ctx, key_value); + key = { static_cast(key_data.data()), key_data.size() }; + } + + realm->write_copy(path, key); +} template void RealmClass::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) { @@ -1045,67 +1045,67 @@ void RealmClass::object_for_object_id(ContextType ctx, ObjectType this_object if (!sync::has_object_ids(realm->read_group())) throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); - std::string object_type = Value::validated_to_string(ctx, args[0]); - validated_object_schema_for_value(ctx, realm, args[0], object_type); - + auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]); std::string object_id_string = Value::validated_to_string(ctx, args[1]); - auto object_id = object_id_from_string(object_id_string); + auto object_id = sync::ObjectID::from_string(object_id_string); const Group& group = realm->read_group(); - size_t ndx = sync::row_for_object_id(group, *ObjectStore::table_for_object_type(group, object_type), object_id); + size_t ndx = sync::row_for_object_id(group, *ObjectStore::table_for_object_type(group, object_schema.name), object_id); if (ndx != realm::npos) { - return_value.set(RealmObjectClass::create_instance(ctx, realm::Object(realm, object_type, ndx))); + return_value.set(RealmObjectClass::create_instance(ctx, realm::Object(realm, object_schema.name, ndx))); } #else throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); #endif // REALM_ENABLE_SYNC } -#if REALM_ENABLE_SYNC template -void RealmClass::subscribe_to_objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { - args.validate_count(3); +void RealmClass::privileges(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); - SharedRealm realm = *get_internal>(this_object); - std::string object_type = Value::validated_to_string(ctx, args[0]); - std::string query = Value::validated_to_string(ctx, args[1]); - auto callback = Value::validated_to_function(ctx, args[2]); - - auto &schema = realm->schema(); - auto object_schema = schema.find(object_type); - - if (object_schema == schema.end()) { - throw std::runtime_error("Object type '" + object_type + "' not found in schema."); - } - - Protected protected_this(ctx, this_object); - Protected protected_ctx(Context::get_global_context(ctx)); - Protected protected_callback(ctx, callback); - auto cb = [=](realm::Results results, std::exception_ptr err) { - HANDLESCOPE - - if (err) { - try { - std::rethrow_exception(err); - } - catch (const std::exception& e) { - ValueType callback_arguments[2]; - callback_arguments[0] = Value::from_string(protected_ctx, e.what()); - callback_arguments[1] = Value::from_null(protected_ctx); - Function::callback(ctx, protected_callback, protected_this, 2, callback_arguments); - } - return; - } - - ValueType callback_arguments[2]; - callback_arguments[0] = Value::from_null(protected_ctx); - callback_arguments[1] = ResultsClass::create_instance(protected_ctx, results); - Function::callback(protected_ctx, protected_callback, protected_this, 2, callback_arguments); + using Privilege = realm::ComputedPrivileges; + auto has_privilege = [](Privilege actual, Privilege expected) { + return (static_cast(actual) & static_cast(expected)) == static_cast(expected); }; - partial_sync::register_query(realm, object_type, query, std::move(cb)); + SharedRealm realm = *get_internal>(this_object); + if (args.count == 0) { + auto p = realm->get_privileges(); + ObjectType object = Object::create_empty(ctx); + Object::set_property(ctx, object, "read", Value::from_boolean(ctx, has_privilege(p, Privilege::Read))); + Object::set_property(ctx, object, "update", Value::from_boolean(ctx,has_privilege(p, Privilege::Update))); + Object::set_property(ctx, object, "modifySchema", Value::from_boolean(ctx, has_privilege(p, Privilege::ModifySchema))); + Object::set_property(ctx, object, "setPermissions", Value::from_boolean(ctx, has_privilege(p, Privilege::SetPermissions))); + return_value.set(object); + return; + } + + if (Value::is_object(ctx, args[0])) { + auto arg = Value::to_object(ctx, args[0]); + if (Object::template is_instance>(ctx, arg)) { + auto obj = get_internal>(arg); + auto p = realm->get_privileges(obj->row()); + + ObjectType object = Object::create_empty(ctx); + Object::set_property(ctx, object, "read", Value::from_boolean(ctx, has_privilege(p, Privilege::Read))); + Object::set_property(ctx, object, "update", Value::from_boolean(ctx,has_privilege(p, Privilege::Update))); + Object::set_property(ctx, object, "delete", Value::from_boolean(ctx,has_privilege(p, Privilege::Delete))); + Object::set_property(ctx, object, "setPermissions", Value::from_boolean(ctx, has_privilege(p, Privilege::SetPermissions))); + return_value.set(object); + return; + } + } + + auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]); + auto p = realm->get_privileges(object_schema.name); + ObjectType object = Object::create_empty(ctx); + Object::set_property(ctx, object, "read", Value::from_boolean(ctx, has_privilege(p, Privilege::Read))); + Object::set_property(ctx, object, "update", Value::from_boolean(ctx,has_privilege(p, Privilege::Update))); + Object::set_property(ctx, object, "create", Value::from_boolean(ctx, has_privilege(p, Privilege::Create))); + Object::set_property(ctx, object, "subscribe", Value::from_boolean(ctx, has_privilege(p, Privilege::Query))); + Object::set_property(ctx, object, "setPermissions", Value::from_boolean(ctx, has_privilege(p, Privilege::SetPermissions))); + return_value.set(object); } -#endif } // js } // realm diff --git a/src/js_results.hpp b/src/js_results.hpp index f179be3f..e9f73294 100644 --- a/src/js_results.hpp +++ b/src/js_results.hpp @@ -28,6 +28,11 @@ #include #include +#include +#ifdef REALM_ENABLE_SYNC +#include "js_sync.hpp" +#include "sync/partial_sync.hpp" +#endif namespace realm { namespace js { @@ -82,6 +87,9 @@ struct ResultsClass : ClassDefinition, CollectionClass< static void filtered(ContextType, ObjectType, Arguments, ReturnValue &); static void sorted(ContextType, ObjectType, Arguments, ReturnValue &); static void is_valid(ContextType, ObjectType, Arguments, ReturnValue &); +#if REALM_ENABLE_SYNC + static void subscribe(ContextType, ObjectType, Arguments, ReturnValue &); +#endif static void index_of(ContextType, ObjectType, Arguments, ReturnValue &); @@ -107,6 +115,9 @@ struct ResultsClass : ClassDefinition, CollectionClass< {"filtered", wrap}, {"sorted", wrap}, {"isValid", wrap}, +#if REALM_ENABLE_SYNC + {"subscribe", wrap}, +#endif {"min", wrap, AggregateFunc::Min>>}, {"max", wrap, AggregateFunc::Max>>}, {"sum", wrap, AggregateFunc::Sum>>}, @@ -141,6 +152,22 @@ typename T::Object ResultsClass::create_instance(ContextType ctx, SharedRealm return create_object>(ctx, new realm::js::Results(realm, *table)); } +inline void alias_backlinks(parser::KeyPathMapping &mapping, const realm::SharedRealm &realm) +{ + const realm::Schema &schema = realm->schema(); + for (auto it = schema.begin(); it != schema.end(); ++it) { + for (const Property &property : it->computed_properties) { + if (property.type == realm::PropertyType::LinkingObjects) { + auto target_object_schema = schema.find(property.object_type); + const TableRef table = ObjectStore::table_for_object_type(realm->read_group(), it->name); + const TableRef target_table = ObjectStore::table_for_object_type(realm->read_group(), target_object_schema->name); + std::string native_name = "@links." + std::string(target_table->get_name()) + "." + property.link_origin_property_name; + mapping.add_mapping(table, property.name, native_name); + } + } + } +} + template template typename T::Object ResultsClass::create_filtered(ContextType ctx, const U &collection, Arguments args) { @@ -152,13 +179,17 @@ typename T::Object ResultsClass::create_filtered(ContextType ctx, const U &co auto query = collection.get_query(); auto const &realm = collection.get_realm(); auto const &object_schema = collection.get_object_schema(); + DescriptorOrdering ordering; + parser::KeyPathMapping mapping; + alias_backlinks(mapping, realm); - parser::Predicate predicate = parser::parse(query_string); + parser::ParserResult result = parser::parse(query_string); NativeAccessor accessor(ctx, realm, object_schema); query_builder::ArgumentConverter> converter(accessor, &args.value[1], args.count - 1); - query_builder::apply_predicate(query, predicate, converter); + query_builder::apply_predicate(query, result.predicate, converter, mapping); + query_builder::apply_ordering(ordering, query.get_table(), result.ordering); - return create_instance(ctx, collection.filter(std::move(query))); + return create_instance(ctx, collection.filter(std::move(query)).apply_ordering(std::move(ordering))); } template @@ -252,6 +283,28 @@ void ResultsClass::is_valid(ContextType ctx, ObjectType this_object, Argument return_value.set(get_internal>(this_object)->is_valid()); } +#if REALM_ENABLE_SYNC +template +void ResultsClass::subscribe(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); + + auto results = get_internal>(this_object); + auto realm = results->get_realm(); + auto sync_config = realm->config().sync_config; + + util::Optional subscription_name; + if (args.count == 1) { + subscription_name = util::Optional(Value::validated_to_string(ctx, args[0])); + } + else { + subscription_name = util::none; + } + + auto subscription = partial_sync::subscribe(*results, subscription_name); + return_value.set(SubscriptionClass::create_instance(ctx, std::move(subscription))); +} +#endif + template template void ResultsClass::index_of(ContextType ctx, Fn& fn, Arguments args, ReturnValue &return_value) { @@ -324,13 +377,13 @@ void ResultsClass::add_listener(ContextType ctx, U& collection, ObjectType th Protected protected_ctx(Context::get_global_context(ctx)); auto token = collection.add_notification_callback([=](CollectionChangeSet const& change_set, std::exception_ptr exception) { - HANDLESCOPE - ValueType arguments[] { - static_cast(protected_this), - CollectionClass::create_collection_change_set(protected_ctx, change_set) - }; - Function::callback(protected_ctx, protected_callback, protected_this, 2, arguments); - }); + HANDLESCOPE + ValueType arguments[] { + static_cast(protected_this), + CollectionClass::create_collection_change_set(protected_ctx, change_set) + }; + Function::callback(protected_ctx, protected_callback, protected_this, 2, arguments); + }); collection.m_notification_tokens.emplace_back(protected_callback, std::move(token)); } diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 238ab94c..501fc1a3 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -29,9 +29,18 @@ #include "sync/sync_config.hpp" #include "sync/sync_session.hpp" #include "sync/sync_user.hpp" +#include "sync/partial_sync.hpp" #include "realm/util/logger.hpp" #include "realm/util/uri.hpp" +#if REALM_ANDROID +#include +#include "./android/io_realm_react_RealmReactModule.h" +#include "./android/jni_utils.hpp" + +extern jclass ssl_helper_class; +#endif + namespace realm { namespace js { @@ -102,7 +111,7 @@ public: static void session_for_on_disk_path(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); MethodMap const methods = { - {"logout", wrap}, + {"_logout", wrap}, {"_sessionForOnDiskPath", wrap} }; }; @@ -302,7 +311,6 @@ public: bool operator ()(const std::string& server_address, sync::Session::port_type server_port, const char* pem_data, size_t pem_size, int preverify_ok, int depth) { const std::string pem_certificate {pem_data, pem_size}; - { std::lock_guard lock {*m_mutex}; m_ssl_certificate_callback_done = false; @@ -555,6 +563,135 @@ void SessionClass::override_server(ContextType ctx, ObjectType this_object, A } } +template +class Subscription : public partial_sync::Subscription { +public: + Subscription(partial_sync::Subscription s) : partial_sync::Subscription(std::move(s)) {} + Subscription(Subscription &&) = default; + + std::vector, partial_sync::SubscriptionNotificationToken>> m_notification_tokens; +}; + +template +class SubscriptionClass : public ClassDefinition> { + using GlobalContextType = typename T::GlobalContext; + using ContextType = typename T::Context; + using FunctionType = typename T::Function; + using ObjectType = typename T::Object; + using ValueType = typename T::Value; + using String = js::String; + using Object = js::Object; + using Value = js::Value; + using Function = js::Function; + using ReturnValue = js::ReturnValue; + using Arguments = js::Arguments; + +public: + std::string const name = "Subscription"; + + static FunctionType create_constructor(ContextType); + static ObjectType create_instance(ContextType, partial_sync::Subscription); + + static void get_state(ContextType, ObjectType, ReturnValue &); + static void get_error(ContextType, ObjectType, ReturnValue &); + + static void unsubscribe(ContextType, ObjectType, Arguments, ReturnValue &); + static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); + + PropertyMap const properties = { + {"state", {wrap, nullptr}}, + {"error", {wrap, nullptr}} + }; + + MethodMap const methods = { + {"unsubscribe", wrap}, + {"addListener", wrap}, + {"removeListener", wrap}, + {"removeAllListeners", wrap}, + }; +}; + +template +typename T::Object SubscriptionClass::create_instance(ContextType ctx, partial_sync::Subscription subscription) { + return create_object>(ctx, new Subscription(std::move(subscription))); +} + +template +void SubscriptionClass::get_state(ContextType ctx, ObjectType object, ReturnValue &return_value) { + auto subscription = get_internal>(object); + return_value.set(static_cast(subscription->state())); +} + +template +void SubscriptionClass::get_error(ContextType ctx, ObjectType object, ReturnValue &return_value) { + auto subscription = get_internal>(object); + if (auto error = subscription->error()) { + try { + std::rethrow_exception(error); + } + catch (const std::exception& e) { + return_value.set(e.what()); + } + } + else { + return_value.set_undefined(); + } +} + +template +void SubscriptionClass::unsubscribe(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); + auto subscription = get_internal>(this_object); + partial_sync::unsubscribe(*subscription); + return_value.set_undefined(); +} + +template +void SubscriptionClass::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); + auto subscription = get_internal>(this_object); + + auto callback = Value::validated_to_function(ctx, args[0]); + Protected protected_callback(ctx, callback); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + + auto token = subscription->add_notification_callback([=]() { + HANDLESCOPE + + ValueType arguments[2]; + arguments[0] = static_cast(protected_this), + arguments[1] = Value::from_number(ctx, static_cast(subscription->state())); + Function::callback(protected_ctx, protected_callback, protected_this, 2, arguments); + }); + + subscription->m_notification_tokens.emplace_back(protected_callback, std::move(token)); +} + +template +void SubscriptionClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); + auto subscription = get_internal>(this_object); + + auto callback = Value::validated_to_function(ctx, args[0]); + auto protected_function = Protected(ctx, callback); + + auto& tokens = subscription->m_notification_tokens; + auto compare = [&](auto&& token) { + return typename Protected::Comparator()(token.first, protected_function); + }; + tokens.erase(std::remove_if(tokens.begin(), tokens.end(), compare), tokens.end()); +} + +template +void SubscriptionClass::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); + auto subscription = get_internal>(this_object); + subscription->m_notification_tokens.clear(); +} + template class SyncClass : public ClassDefinition { using GlobalContextType = typename T::GlobalContext; @@ -694,14 +831,52 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr SSLVerifyCallbackSyncThreadFunctor ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)}; ssl_verify_callback = std::move(ssl_verify_functor); } - +#if REALM_ANDROID + // For React Native Android, if the user didn't define the ssl_verify_callback, we provide a default + // implementation for him, otherwise all SSL validation will fail, since the Sync client doesn't have + // access to the Android Keystore. + // This default implementation will perform a JNI call to invoke a Java method defined at the `SSLHelper` + // to perform the certificate verification. + else { + auto ssl_verify_functor = + [](const std::string server_address, realm::sync::Session::port_type server_port, + const char* pem_data, size_t pem_size, int preverify_ok, int depth) { + JNIEnv* env = realm::jni_util::JniUtils::get_env(true); + static jmethodID java_certificate_verifier = env->GetStaticMethodID(ssl_helper_class, "certificateVerifier", "(Ljava/lang/String;Ljava/lang/String;I)Z"); + jstring jserver_address = env->NewStringUTF(server_address.c_str()); + // deep copy the pem_data into a string so DeleteLocalRef delete the local reference not the original const char + std::string pem(pem_data, pem_size); + jstring jpem = env->NewStringUTF(pem.c_str()); + + bool isValid = env->CallStaticBooleanMethod(ssl_helper_class, java_certificate_verifier, + jserver_address, + jpem, depth) == JNI_TRUE; + env->DeleteLocalRef(jserver_address); + env->DeleteLocalRef(jpem); + return isValid; + }; + ssl_verify_callback = std::move(ssl_verify_functor); + } +#endif bool is_partial = false; ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial"); if (!Value::is_undefined(ctx, partial_value)) { is_partial = Value::validated_to_boolean(ctx, partial_value); } - config.sync_config = std::make_shared(shared_user, std::move(raw_realm_url)); + bool disable_partial_sync_url_checks = false; + ValueType disable_partial_sync_url_checks_value = Object::get_property(ctx, sync_config_object, "_disablePartialSyncUrlChecks"); + if (!Value::is_undefined(ctx, disable_partial_sync_url_checks_value)) { + disable_partial_sync_url_checks = Value::validated_to_boolean(ctx, disable_partial_sync_url_checks_value); + } + + if (disable_partial_sync_url_checks) { + config.sync_config = std::make_shared(shared_user, std::move("")); + config.sync_config->reference_realm_url = std::move(raw_realm_url); + } + else { + config.sync_config = std::make_shared(shared_user, std::move(raw_realm_url)); + } config.sync_config->bind_session_handler = std::move(bind); config.sync_config->error_handler = std::move(error_handler); config.sync_config->client_validate_ssl = client_validate_ssl; diff --git a/src/object-store b/src/object-store index d5fe9a62..f2a536d2 160000 --- a/src/object-store +++ b/src/object-store @@ -1 +1 @@ -Subproject commit d5fe9a626c0adc0345d128201e0df8bb4004725d +Subproject commit f2a536d29de48e34e60799a5bf3f36e13806387e diff --git a/tests/js/asserts.js b/tests/js/asserts.js index ff01df46..154c4821 100644 --- a/tests/js/asserts.js +++ b/tests/js/asserts.js @@ -37,7 +37,8 @@ module.exports = { } else if (type === 'object') { for (const key of Object.keys(val1)) { - this.assertEqual(val1[key], val2[key], errorMessage, depth + 1); + const message = errorMessage ? `${errorMessage}: ${key}` : key; + this.assertEqual(val1[key], val2[key], message, depth + 1); } } else if (type === 'list') { diff --git a/tests/js/download-api-helper.js b/tests/js/download-api-helper.js index 629c80f6..46076544 100644 --- a/tests/js/download-api-helper.js +++ b/tests/js/download-api-helper.js @@ -11,7 +11,8 @@ const Realm = require(realmModule); function createObjects(user) { const config = { - sync: { user, + sync: { + user: user, url: `realm://localhost:9080/~/${realmName}`, error: err => console.log(err) }, @@ -19,7 +20,6 @@ function createObjects(user) { }; const realm = new Realm(config); - realm.write(() => { for (let i = 1; i <= 3; i++) { realm.create('Dog', { name: `Lassy ${i}` }); diff --git a/tests/js/index.js b/tests/js/index.js index f7f52436..bda91112 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -55,7 +55,7 @@ if (global.enableSyncTests) { // FIXME: Permission tests currently fail in chrome debugging mode. if (typeof navigator === 'undefined' || !/Chrome/.test(navigator.userAgent)) { // eslint-disable-line no-undef - TESTS.PermissionTests = require('./permission-tests'); + TESTS.PermissionTests = require('./permission-tests'); } } diff --git a/tests/js/linkingobjects-tests.js b/tests/js/linkingobjects-tests.js index fb07a0ff..520513a4 100644 --- a/tests/js/linkingobjects-tests.js +++ b/tests/js/linkingobjects-tests.js @@ -83,6 +83,52 @@ module.exports = { TestCase.assertArraysEqual(names(resultsC), ['JP']); }, + testFilteredLinkingObjectsByName: function() { + var realm = new Realm({schema: [schemas.PersonObject]}); + + var christine, olivier; + realm.write(function() { + olivier = realm.create('PersonObject', {name: 'Olivier', age: 0}); + christine = realm.create('PersonObject', {name: 'Christine', age: 25, children: [olivier]}); + realm.create('PersonObject', {name: 'JP', age: 28, children: [olivier]}); + }); + + let people = realm.objects('PersonObject') + + TestCase.assertEqual(people.filtered('parents.age > 25').length, 1); + TestCase.assertEqual(people.filtered('parents.age > 25')[0].name, 'Olivier'); + TestCase.assertEqual(people.filtered('parents.@count == 2').length, 1); + TestCase.assertEqual(people.filtered('parents.name CONTAINS[c] "chris"').length, 1); + TestCase.assertEqual(people.filtered('parents.name.@size == 2').length, 1); + TestCase.assertEqual(people.filtered('25 IN parents.age').length, 1); + }, + + testNamedLinkingObjectsAcrossClasses: function() { + let realm = new Realm({schema: [schemas.Language, schemas.Country]}); + realm.write(() => { + let english = realm.create('Language', {name: 'English'}); + let french = realm.create('Language', {name: 'French'}); + let danish = realm.create('Language', {name: 'Danish'}); + let canada = realm.create('Country', {name: 'Canada', languages: [english, french]}); + let denmark = realm.create('Country', {name: 'Denmark', languages: [danish, english]}); + let france = realm.create('Country', {name: 'France', languages: [french, english]}); + }); + let languages = realm.objects('Language'); + let spokenInThreeCountries = languages.filtered('spokenIn.@count == 3'); + TestCase.assertEqual(spokenInThreeCountries.length, 1); + TestCase.assertEqual(spokenInThreeCountries[0].name, 'English'); + let spokenInTwoCountries = languages.filtered('spokenIn.@count == 2'); + TestCase.assertEqual(spokenInTwoCountries.length, 1); + TestCase.assertEqual(spokenInTwoCountries[0].name, 'French') + let spokenInOneCountry = languages.filtered('spokenIn.@count == 1'); + TestCase.assertEqual(spokenInOneCountry.length, 1); + TestCase.assertEqual(spokenInOneCountry[0].name, 'Danish') + let languagesSpokenInCanada = languages.filtered('spokenIn.name ==[c] "canada"'); + TestCase.assertEqual(languagesSpokenInCanada.length, 2); + TestCase.assertEqual(languagesSpokenInCanada[0].name, 'English'); + TestCase.assertEqual(languagesSpokenInCanada[1].name, 'French'); + }, + testMethod: function() { var realm = new Realm({schema: [schemas.PersonObject]}); diff --git a/tests/js/nested-list-helper.js b/tests/js/nested-list-helper.js index e4b0803c..7de46e5e 100644 --- a/tests/js/nested-list-helper.js +++ b/tests/js/nested-list-helper.js @@ -9,6 +9,8 @@ const realmName = process.argv[4]; const realmModule = process.argv[5]; const Realm = require(realmModule); +// Ensure that schemas.js gets the correct module with `require('realm')` +require.cache[require.resolve('realm')] = require.cache[require.resolve(realmModule)]; let schemas = require(process.argv[2]); function createObjects(user) { diff --git a/tests/js/partial-sync-api-helper.js b/tests/js/partial-sync-api-helper.js new file mode 100644 index 00000000..c0b0bcb4 --- /dev/null +++ b/tests/js/partial-sync-api-helper.js @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2016 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. +// +//////////////////////////////////////////////////////////////////////////// + +/* +* This script creates 3 new objects into a new realm. These are objects are validated to exists by the download api tests. +*/ + +'use strict'; +console.log("partial-sync-api-helper started"); +const username = process.argv[2]; +const realmModule = process.argv[3]; + +const Realm = require(realmModule); + +function createObjects(user) { + const config = { + sync: { + user, + url: `realm://localhost:9080/default`, + partial: true, + error: err => console.log('partial-sync-api-helper', err) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; + + const realm = new Realm(config); + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); + + let session = realm.syncSession; + return new Promise((resolve, reject) => { + let callback = (transferred, total) => { + if (transferred === total) { + session.removeProgressNotification(callback); + resolve(realm); + } + } + session.addProgressNotification('upload', 'forCurrentlyOutstandingWork', callback); + }); +} + +let registrationError; +Realm.Sync.User.register('http://localhost:9080', username, 'password') + .catch((error) => { + registrationError = JSON.stringify(error); + return Realm.Sync.User.login('http://localhost:9080', username, 'password') + }) + .catch((error) => { + const loginError = JSON.stringify(error); + console.error(`partial-sync-api-helper failed:\n User.register() error:\n${registrationError}\n User.login() error:\n${registrationError}`); + process.exit(-2); + }) + .then((user) => createObjects(user)) + .then(() => process.exit(0)); diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index 4379e494..d396f725 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -16,33 +16,33 @@ // //////////////////////////////////////////////////////////////////////////// - 'use strict'; var Realm = require('realm'); var TestCase = require('./asserts'); function uuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); } function createUsersWithTestRealms(count) { - const createUserWithTestRealm = () => { - return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password') - .then(user => { - new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close(); - return user; - }); - }; + const createUserWithTestRealm = () => { + return Realm.Sync.User + .register('http://localhost:9080', uuid(), 'password') + .then(user => { + new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close(); + return user; + }); + }; - return Promise.all(Array.from({length: count}, createUserWithTestRealm)); + return Promise.all(Array.from({length: count}, createUserWithTestRealm)); } function wait(t) { - return new Promise(resolve => setTimeout(resolve, t)); + return new Promise(resolve => setTimeout(resolve, t)); } function repeatUntil(fn, predicate) { @@ -57,66 +57,179 @@ function repeatUntil(fn, predicate) { return check; } +function subscribe(results) { + const subscription = results.subscribe(); + return new Promise((resolve, reject) => { + subscription.addListener((subscription, state) => { + if (state === Realm.Sync.SubscriptionState.Complete) { + resolve(); + } + else if (state === Realm.Sync.SubscriptionState.Error) { + reject(); + } + }); + setTimeout(() => reject("listener never called"), 5000); + }); +} + +function waitForUpload(realm) { + let session = realm.syncSession; + return new Promise(resolve => { + let callback = (transferred, total) => { + if (transferred === total) { + session.removeProgressNotification(callback); + resolve(realm); + } + }; + session.addProgressNotification('upload', 'forCurrentlyOutstandingWork', callback); + }); +} + +function permissionForPath(permissions, path) { + for (const permission of permissions) { + if (permission.path == path) { + return permission; + } + } +} + module.exports = { testApplyAndGetGrantedPermissions() { - return createUsersWithTestRealms(1) - .then(([user]) => { - return user.applyPermissions({ userId: `${user.identity}` }, `/${user.identity}/test`, 'read') - .then(repeatUntil(() => user.getGrantedPermissions('any'), - permissions => permissions.length > 1)) - .then(permissions => { - TestCase.assertEqual(permissions[0].path, `/${user.identity}/test`); - TestCase.assertEqual(permissions[0].mayRead, true); - TestCase.assertEqual(permissions[0].mayWrite, false); - TestCase.assertEqual(permissions[0].mayManage, false); - }); + return createUsersWithTestRealms(1).then(([user]) => { + const path = `/${user.identity}/test`; + return user + .applyPermissions({userId: `${user.identity}`}, + `/${user.identity}/test`, 'read') + .then(repeatUntil(() => user.getGrantedPermissions('any'), + permissions => { + let permission = permissionForPath(permissions, path); + return permission && !permission.mayWrite; + })) + .then(permissions => { + let permission = permissionForPath(permissions, path); + TestCase.assertDefined(permission); + TestCase.assertEqual(permission.mayRead, true); + TestCase.assertEqual(permission.mayWrite, false); + TestCase.assertEqual(permission.mayManage, false); + }); }); }, testOfferPermissions() { - return createUsersWithTestRealms(2) - .then(([user1, user2]) => { - return user1.offerPermissions(`/${user1.identity}/test`, 'read') - .then(token => user2.acceptPermissionOffer(token)) - .then(realmUrl => { - TestCase.assertEqual(realmUrl, `/${user1.identity}/test`); - return realmUrl; - }) - .then(repeatUntil(() => user2.getGrantedPermissions('any'), - permissions => permissions.length > 1)) - .then(permissions => { - TestCase.assertEqual(permissions[2].path, `/${user1.identity}/test`); - TestCase.assertEqual(permissions[2].mayRead, true); - TestCase.assertEqual(permissions[2].mayWrite, false); - TestCase.assertEqual(permissions[2].mayManage, false); - }); + return createUsersWithTestRealms(2).then(([user1, user2]) => { + const path = `/${user1.identity}/test`; + return user1.offerPermissions(`/${user1.identity}/test`, 'read') + .then(token => user2.acceptPermissionOffer(token)) + .then(realmUrl => { + TestCase.assertEqual(realmUrl, path); + return realmUrl; + }) + .then(repeatUntil(() => user2.getGrantedPermissions('any'), + permissions => permissions.length > 2 + && permissionForPath(permissions, path))) + .then(permissions => { + let permission = permissionForPath(permissions, path) + TestCase.assertDefined(permission); + TestCase.assertEqual(permission.mayRead, true); + TestCase.assertEqual(permission.mayWrite, false); + TestCase.assertEqual(permission.mayManage, false); + }); }); }, testInvalidatePermissionOffer() { - let user1, user2, token; - return createUsersWithTestRealms(2) - .then(users => { - user1 = users[0]; - user2 = users[1]; - return user1.offerPermissions(`/${user1.identity}/test`, 'read'); - }) - .then(t => { token = t; return user1.invalidatePermissionOffer(token); }) - // Since we don't yet support notification when the invalidation has gone through, - // wait for a bit and hope the server is done processing. - .then(wait(100)) - .then(() => user2.acceptPermissionOffer(token)) - // We want the call to fail, i.e. the catch() below should be called. - .then(() => { throw new Error("User was able to accept an invalid permission offer token"); }) - .catch(error => { - try { - TestCase.assertEqual(error.message, 'The permission offer is expired.'); - TestCase.assertEqual(error.statusCode, 701); - } - catch (e) { - throw new Error(e); - } - }); + let user1, user2, token; + return createUsersWithTestRealms(2) + .then(users => { + user1 = users[0]; + user2 = users[1]; + return user1.offerPermissions(`/${user1.identity}/test`, 'read'); + }) + .then(t => { + token = t; + return user1.invalidatePermissionOffer(token); + }) + // Since we don't yet support notification when the invalidation has + // gone through, wait for a bit and hope the server is done + // processing. + .then(() => wait(100)) + .then(() => user2.acceptPermissionOffer(token)) + // We want the call to fail, i.e. the catch() below should be + // called. + .then(() => { + throw new Error("User was able to accept an invalid permission offer token"); + }) + .catch(error => { + try { + TestCase.assertEqual(error.message, 'The permission offer is expired.'); + TestCase.assertEqual(error.statusCode, 701); + } + catch (e) { + throw new Error(e); + } + }); }, -} + testObjectPermissions() { + let config = (user, url) => { + return { + schema: [ + Realm.Permissions.Permission, + Realm.Permissions.User, + Realm.Permissions.Role, + { + name: 'Object', + properties: { + value: 'int', + permissions: '__Permission[]' + } + } + ], + sync: {user: user, url: url, partial: true} + }; + }; + let owner, otherUser + return Realm.Sync.User + .register('http://localhost:9080', uuid(), 'password') + .then(user => { + owner = user; + new Realm({sync: {user, url: 'realm://localhost:9080/default', partial: true}}).close(); + return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password') + }) + .then((user) => { + otherUser = user; + let realm = new Realm(config(owner, 'realm://localhost:9080/default')); + realm.write(() => { + let user = realm.create(Realm.Permissions.User, {id: otherUser.identity}) + let role = realm.create(Realm.Permissions.Role, {name: 'reader'}) + role.members.push(user) + + let obj1 = realm.create('Object', {value: 1}); + let obj2 = realm.create('Object', {value: 2}); + obj2.permissions.push(realm.create(Realm.Permissions.Permission, + {role: role, canRead: true, canUpdate: false})) + }); + return waitForUpload(realm).then(() => realm.close()); + }) + .then(() => Realm.open(config(otherUser, `realm://localhost:9080/default`))) + .then((realm) => subscribe(realm.objects('Object')).then(() => realm)) + .then((realm) => { + // Should have full access to the Realm as a whole + TestCase.assertSimilar('object', realm.privileges(), + {read: true, update: true, modifySchema: true, setPermissions: true}); + TestCase.assertSimilar('object', realm.privileges('Object'), + {read: true, update: true, create: true, subscribe: true, setPermissions: true}); + // Verify that checking via constructor works too + TestCase.assertSimilar('object', realm.privileges(Realm.Permissions.User), + {read: true, update: true, create: true, subscribe: true, setPermissions: true}); + + // Should only be able to see the second object + let results = realm.objects('Object') + TestCase.assertEqual(results.length, 1); + TestCase.assertEqual(results[0].value, 2); + TestCase.assertSimilar('object', realm.privileges(results[0]), + {read: true, update: false, delete: false, setPermissions: false}); + realm.close(); + }); + } +} diff --git a/tests/js/query-tests.js b/tests/js/query-tests.js index dcc1a1ef..9ccd43fc 100644 --- a/tests/js/query-tests.js +++ b/tests/js/query-tests.js @@ -154,5 +154,8 @@ module.exports = { }, testOptionalQueries: function() { runQuerySuite(testCases.optionalTests); + }, + testOrderingQueries: function() { + runQuerySuite(testCases.orderingTests); } }; diff --git a/tests/js/query-tests.json b/tests/js/query-tests.json index b55bb297..678e0f5b 100644 --- a/tests/js/query-tests.json +++ b/tests/js/query-tests.json @@ -1,7 +1,7 @@ { "dateTests" : { - "schema" : [{ + "schema" : [{ "name": "DateObject", "properties": [{ "name": "date", "type": "date" }] }], @@ -25,7 +25,7 @@ }, "boolTests" : { - "schema" : [{ + "schema" : [{ "name": "BoolObject", "properties": [{ "name": "boolCol", "type": "bool" }] }], @@ -57,11 +57,11 @@ ["QueryThrows", "BoolObject", "boolCol BEGINSWITH true"], ["QueryThrows", "BoolObject", "boolCol CONTAINS true"], ["QueryThrows", "BoolObject", "boolCol ENDSWITH true"] - ] + ] }, "intTests" : { - "schema" : [{ + "schema" : [{ "name": "IntObject", "properties": [{ "name": "intCol", "type": "int" }] }], @@ -92,7 +92,7 @@ }, "floatTests" : { - "schema" : [{ + "schema" : [{ "name": "FloatObject", "properties": [{ "name": "floatCol", "type": "float" }] }], @@ -126,7 +126,7 @@ }, "doubleTests" : { - "schema" : [{ + "schema" : [{ "name": "DoubleObject", "properties": [{ "name": "doubleCol", "type": "double" }] }], @@ -157,7 +157,7 @@ }, "stringTests" : { - "schema" : [{ + "schema" : [{ "name": "StringObject", "properties": [{ "name": "stringCol", "type": "string" }] }], @@ -205,7 +205,7 @@ }, "binaryTests" : { - "schema" : [{ + "schema" : [{ "name": "BinaryObject", "properties": [{ "name": "binaryCol", "type": "data" }] }], @@ -256,14 +256,14 @@ "compoundTests" : { "schema" : [ - { "name": "IntObject", + { "name": "IntObject", "properties": [{ "name": "intCol", "type": "int" }], "primaryKey" : "intCol" } ], "objects": [ { "type": "IntObject", "value": [0] }, { "type": "IntObject", "value": [1] }, - { "type": "IntObject", "value": [2] }, + { "type": "IntObject", "value": [2] }, { "type": "IntObject", "value": [3] } ], "tests": [ @@ -281,14 +281,14 @@ ["ObjectSet", [0, 1, 2],"IntObject", "intCol == 0 || intCol == 1 || intCol <= 2"], ["ObjectSet", [0, 1], "IntObject", "intCol == 1 && intCol >= 1 || intCol == 0"], ["ObjectSet", [0, 1], "IntObject", "intCol == 1 || intCol == 0 && intCol <= 0 && intCol >= 0"], - ["ObjectSet", [0, 1], "IntObject", "intCol == 0 || NOT (intCol == 3 && intCol >= 0) && intCol == 1"] + ["ObjectSet", [0, 1], "IntObject", "intCol == 0 || NOT (intCol == 3 && intCol >= 0) && intCol == 1"] ] }, "keyPathTests" : { "schema" : [ - { - "name": "BasicTypesObject", + { + "name": "BasicTypesObject", "properties": [ { "name": "intCol", "type": "int" }, { "name": "floatCol", "type": "float" }, @@ -296,7 +296,7 @@ { "name": "stringCol", "type": "string" }, { "name": "dateCol", "type": "date?" } ] - }, + }, { "name": "LinkTypesObject", "primaryKey": "primaryKey", @@ -326,8 +326,8 @@ "optionalTests" : { "schema" : [ - { - "name": "OptionalTypesObject", + { + "name": "OptionalTypesObject", "primaryKey": "primaryKey", "properties": [ { "name": "primaryKey", "type": "int" }, @@ -338,7 +338,7 @@ { "name": "dateCol", "type": "date", "optional": true }, { "name": "dataCol", "type": "data", "optional": true } ] - }, + }, { "name": "LinkTypesObject", "primaryKey": "primaryKey", @@ -375,9 +375,35 @@ ["ObjectSet", [1], "LinkTypesObject", "basicLink.stringCol == null"], ["ObjectSet", [0], "LinkTypesObject", "basicLink.stringCol != null"], ["ObjectSet", [1], "LinkTypesObject", "basicLink.dateCol == null"], - ["ObjectSet", [0], "LinkTypesObject", "basicLink.dateCol != null"], - ["QueryThrows", "LinkTypesObject", "basicLink.dataCol == null"] + ["ObjectSet", [0], "LinkTypesObject", "basicLink.dateCol != null"] + ] +}, + +"orderingTests" : { + "schema" : [ + { "name": "Person", + "properties": [ + { "name": "id", "type": "int"}, + { "name": "name", "type": "string" }, + { "name": "age", "type": "int"} + ], + "primaryKey" : "id" } + ], + "objects": [ + { "type": "Person", "value": [0, "John", 28] }, + { "type": "Person", "value": [1, "John", 37] }, + { "type": "Person", "value": [2, "Jake", 27] }, + { "type": "Person", "value": [3, "Jake", 32] }, + { "type": "Person", "value": [4, "Jake", 32] }, + { "type": "Person", "value": [5, "Johnny", 19] } + ], + "tests": [ + ["ObjectSet", [1, 3], "Person", "age > 20 SORT(age DESC) DISTINCT(name)"], + ["ObjectSet", [2, 0], "Person", "age > 20 SORT(age ASC) DISTINCT(name)"], + ["ObjectSet", [2, 0], "Person", "age > 20 SORT(age ASC, name DESC) DISTINCT(name)"], + ["ObjectSet", [2, 0], "Person", "age > 20 SORT(name DESC) SORT(age ASC) DISTINCT(name)"], + ["ObjectSet", [2, 0, 3, 1], "Person", "age > 20 SORT(age ASC, name DESC) DISTINCT(name, age)"], + ["ObjectSet", [0, 2], "Person", "age > 20 SORT(age ASC) DISTINCT(age) SORT(name DESC) DISTINCT(name)"] ] } - } diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 61a07b66..e51ddeff 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -1200,8 +1200,54 @@ module.exports = { testDisableFileFormatUpgrade: function() { Realm.copyBundledRealmFiles(); - TestCase.assertThrowsContaining(() => { + TestCase.assertThrowsContaining(() => { new Realm({ path: 'dates-v3.realm', disableFormatUpgrade: true } ); }, 'The Realm file format must be allowed to be upgraded in order to proceed.'); + }, + + testWriteCopyTo: function() { + const realm = new Realm({schema: [schemas.IntPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); + + realm.write(() => { + realm.create('TestObject', {doubleCol: 1}); + }); + TestCase.assertEqual(1, realm.objects('TestObject').length); + + TestCase.assertThrowsContaining(() => { + realm.writeCopyTo(); + }, "At least path has to be provided for 'writeCopyTo'"); + + TestCase.assertThrowsContaining(() => { + realm.writeCopyTo(34); + }, "Argument to 'writeCopyTo' must be a String."); + + const copyName = "testWriteCopy.realm"; + realm.writeCopyTo(copyName); + + const copyConfig = { path: copyName }; + const realmCopy = new Realm(copyConfig); + TestCase.assertEqual(1, realmCopy.objects('TestObject').length); + realmCopy.close(); + + TestCase.assertThrowsContaining(() => { + realm.writeCopyTo("testWriteCopyWithInvalidKey.realm", "hello"); + }, "Encryption key for 'writeCopyTo' must be a Binary."); + + // Failing on Linux only! + /* + const encryptedCopyName = "testWriteEncryptedCopy.realm"; + var encryptionKey = new Int8Array(64); + for(let i=0; i < 64; i++) { + encryptionKey[i] = 1; + } + realm.writeCopyTo(encryptedCopyName, encryptionKey); + + const encryptedCopyConfig = { path: encryptedCopyName, encryptionKey: encryptionKey }; + const encryptedRealmCopy = new Realm(encryptedCopyConfig); + TestCase.assertEqual(1, encryptedRealmCopy.objects('TestObject').length); + encryptedRealmCopy.close(); + */ + + realm.close(); } }; diff --git a/tests/js/schemas.js b/tests/js/schemas.js index 8c153603..44b69e5b 100644 --- a/tests/js/schemas.js +++ b/tests/js/schemas.js @@ -307,3 +307,20 @@ exports.MultiListObject = { 'list2': 'string[]' } }; + +exports.Language = { + name: 'Language', + properties: { + name: 'string', + spokenIn: {type: 'linkingObjects', objectType: 'Country', property: 'languages'} + } +}; + +exports.Country = { + name: 'Country', + properties: { + name: 'string', + languages: 'Language[]', + } +}; + diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index cd0fc323..e6f3970a 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -61,27 +61,23 @@ function copyFileToTempDir(filename) { return tmpFile.name; } -function runOutOfProcess(nodeJsFilePath) { - var nodeArgs = Array.prototype.slice.call(arguments); +function runOutOfProcess() { + const args = Array.prototype.slice.call(arguments); let tmpDir = tmp.dirSync(); - let content = fs.readFileSync(nodeJsFilePath, 'utf8'); - let tmpFile = tmp.fileSync({ dir: tmpDir.name }); - fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); - nodeArgs[0] = tmpFile.name; + console.log(`runOutOfProcess : ${args.join(' ')}`); return new Promise((resolve, reject) => { try { - console.log('runOutOfProcess command\n node ' + nodeArgs.join(" ")); - const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => { + execFile(process.execPath, args, {cwd: tmpDir.name}, (error, stdout, stderr) => { if (error) { - console.error("runOutOfProcess failed\n" + error); - reject(new Error(`Running ${nodeJsFilePath} failed. error: ${error}`)); + console.error("runOutOfProcess failed\n", error, stdout, stderr); + reject(new Error(`Running ${args[0]} failed. error: ${error}`)); return; } console.log('runOutOfProcess success\n' + stdout); resolve(); }); - } + } catch (e) { reject(e); } @@ -275,43 +271,40 @@ module.exports = { const expectedObjectsCount = 3; return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - const accessTokenRefreshed = this; - let successCounter = 0; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + const accessTokenRefreshed = this; + let successCounter = 0; - let config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}` } - }; + let config = { + sync: { user, url: `realm://localhost:9080/~/${realmName}` } + }; + return new Promise((resolve, reject) => { + Realm.openAsync(config, (error, realm) => { + if (error) { + reject(error); + return; + } + try { + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); - Realm.openAsync(config, (error, realm) => { - try { - if (error) { - reject(error); - } + let firstDog = realm.objects('Dog')[0]; + TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema"); + TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value"); + TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value"); - let actualObjectsCount = realm.objects('Dog').length; - TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); - - let firstDog = realm.objects('Dog')[0]; - TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema"); - TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value"); - TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value"); - - - const session = realm.syncSession; - TestCase.assertInstanceOf(session, Realm.Sync.Session); - TestCase.assertEqual(session.user.identity, user.identity); - TestCase.assertEqual(session.config.url, config.sync.url); - TestCase.assertEqual(session.config.user.identity, config.sync.user.identity); - TestCase.assertEqual(session.state, 'active'); - resolve(); - } - catch (e) { - reject(e); - } - }); + const session = realm.syncSession; + TestCase.assertInstanceOf(session, Realm.Sync.Session); + TestCase.assertEqual(session.user.identity, user.identity); + TestCase.assertEqual(session.config.url, config.sync.url); + TestCase.assertEqual(session.config.user.identity, config.sync.user.identity); + TestCase.assertEqual(session.state, 'active'); + resolve(); + } + catch (e) { + reject(e); + } }); }); }); @@ -321,26 +314,22 @@ module.exports = { const username = uuid(); const expectedObjectsCount = 3; + const accessTokenRefreshed = this; + let successCounter = 0; - return new Promise((resolve, reject) => { - const accessTokenRefreshed = this; - let successCounter = 0; + let config = { + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let config = { - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + return Realm.open(config).then(realm => { + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); - Realm.open(config).then(realm => { - realm.write(() => { - for (let i = 1; i <= 3; i++) { - realm.create('Dog', { name: `Lassy ${i}` }); - } - }); - - let actualObjectsCount = realm.objects('Dog').length; - TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Local realm does not contain the expected objects count"); - resolve(); - }).catch(error => reject(error)); + let actualObjectsCount = realm.objects('Dog').length; + TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Local realm does not contain the expected objects count"); }); }, @@ -412,37 +401,33 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/nested-list-helper.js', __dirname + '/schemas.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - schema: [schemas.ParentObject, schemas.NameObject], - sync: { user, url: `realm://localhost:9080/~/${realmName}` } - }; - Realm.open(config).then(realm => { - let objects = realm.objects('ParentObject'); + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + schema: [schemas.ParentObject, schemas.NameObject], + sync: { user, url: `realm://localhost:9080/~/${realmName}` } + }; + return Realm.open(config) + }).then(realm => { + let objects = realm.objects('ParentObject'); - let json = JSON.stringify(objects); - TestCase.assertEqual(json, '{"0":{"id":1,"name":{"0":{"family":"Larsen","given":{"0":"Hans","1":"Jørgen"},"prefix":{}},"1":{"family":"Hansen","given":{"0":"Ib"},"prefix":{}}}},"1":{"id":2,"name":{"0":{"family":"Petersen","given":{"0":"Gurli","1":"Margrete"},"prefix":{}}}}}'); - TestCase.assertEqual(objects.length, 2); - TestCase.assertEqual(objects[0].name.length, 2); - TestCase.assertEqual(objects[0].name[0].given.length, 2); - TestCase.assertEqual(objects[0].name[0].prefix.length, 0); - TestCase.assertEqual(objects[0].name[0].given[0], 'Hans'); - TestCase.assertEqual(objects[0].name[0].given[1], 'Jørgen') - TestCase.assertEqual(objects[0].name[1].given.length, 1); - TestCase.assertEqual(objects[0].name[1].given[0], 'Ib'); - TestCase.assertEqual(objects[0].name[1].prefix.length, 0); + let json = JSON.stringify(objects); + TestCase.assertEqual(json, '{"0":{"id":1,"name":{"0":{"family":"Larsen","given":{"0":"Hans","1":"Jørgen"},"prefix":{}},"1":{"family":"Hansen","given":{"0":"Ib"},"prefix":{}}}},"1":{"id":2,"name":{"0":{"family":"Petersen","given":{"0":"Gurli","1":"Margrete"},"prefix":{}}}}}'); + TestCase.assertEqual(objects.length, 2); + TestCase.assertEqual(objects[0].name.length, 2); + TestCase.assertEqual(objects[0].name[0].given.length, 2); + TestCase.assertEqual(objects[0].name[0].prefix.length, 0); + TestCase.assertEqual(objects[0].name[0].given[0], 'Hans'); + TestCase.assertEqual(objects[0].name[0].given[1], 'Jørgen') + TestCase.assertEqual(objects[0].name[1].given.length, 1); + TestCase.assertEqual(objects[0].name[1].given[0], 'Ib'); + TestCase.assertEqual(objects[0].name[1].prefix.length, 0); - TestCase.assertEqual(objects[1].name.length, 1); - TestCase.assertEqual(objects[1].name[0].given.length, 2); - TestCase.assertEqual(objects[1].name[0].prefix.length, 0); - TestCase.assertEqual(objects[1].name[0].given[0], 'Gurli'); - TestCase.assertEqual(objects[1].name[0].given[1], 'Margrete'); - resolve(); - }).catch(() => reject()); - }); - }); + TestCase.assertEqual(objects[1].name.length, 1); + TestCase.assertEqual(objects[1].name[0].given.length, 2); + TestCase.assertEqual(objects[1].name[0].prefix.length, 0); + TestCase.assertEqual(objects[1].name[0].given[0], 'Gurli'); + TestCase.assertEqual(objects[1].name[0].given[1], 'Margrete'); }); }, @@ -567,7 +552,7 @@ module.exports = { }); }, - testProgressNotificationsForRealmConstructor() { +/* testProgressNotificationsForRealmConstructor() { if (!isNodeProccess) { return; } @@ -599,7 +584,7 @@ module.exports = { }); }); }); - }, + },*/ testProgressNotificationsUnregisterForRealmConstructor() { if (!isNodeProccess) { @@ -610,60 +595,59 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - sync: { - user, - url: `realm://localhost:9080/~/${realmName}` - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let realm = new Realm(config); - let unregisterFunc; + let realm = new Realm(config); + let unregisterFunc; - let writeDataFunc = () => { - realm.write(() => { - for (let i = 1; i <= 3; i++) { - realm.create('Dog', { name: `Lassy ${i}` }); - } - }); + let writeDataFunc = () => { + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); + } + + return new Promise((resolve, reject) => { + let syncFinished = false; + let failOnCall = false; + const progressCallback = (transferred, total) => { + if (failOnCall) { + reject(new Error("Progress callback should not be called after removeProgressNotification")); } - let syncFinished = false; - let failOnCall = false; - const progressCallback = (transferred, total) => { - if (failOnCall) { - reject(new Error("Progress callback should not be called after removeProgressNotification")); - } + syncFinished = transferred === total; - syncFinished = transferred === total; + //unregister and write some new data. + if (syncFinished) { + failOnCall = true; + unregisterFunc(); - //unregister and write some new data. - if (syncFinished) { - failOnCall = true; - unregisterFunc(); + //use second callback to wait for sync finished + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => { + if (transferred === transferable) { + resolve(); + } + }); + writeDataFunc(); + } + }; - //use second callback to wait for sync finished - realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => { - if (transferred === transferable) { - resolve(); - } - }); - writeDataFunc(); - } - }; + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); - realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); + unregisterFunc = () => { + realm.syncSession.removeProgressNotification(progressCallback); + }; - unregisterFunc = () => { - realm.syncSession.removeProgressNotification(progressCallback); - }; - - writeDataFunc(); - }); + writeDataFunc(); }); }); }, @@ -675,36 +659,24 @@ module.exports = { const username = uuid(); const realmName = uuid(); + let progressCalled = false; return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - return new Promise((resolve, reject) => { - let config = { - sync: { - user, - url: `realm://localhost:9080/~/${realmName}` - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; - let progressCalled = false; - Realm.open(config) - .progress((transferred, total) => { - progressCalled = true; - }) - .then(() => { - TestCase.assertTrue(progressCalled); - resolve(); - }) - .catch((e) => reject(e)); - - setTimeout(function() { - reject("Progress Notifications API failed to call progress callback for Realm constructor"); - }, 5000); - }); - }); - }); + return Promise.race([ + Realm.open(config).progress((transferred, total) => { progressCalled = true; }), + new Promise((_, reject) => setTimeout(() => reject("Progress Notifications API failed to call progress callback for Realm constructor"), 5000)) + ]); + }).then(() => TestCase.assertTrue(progressCalled)); }, testProgressNotificationsForRealmOpenAsync() { @@ -716,73 +688,182 @@ module.exports = { const realmName = uuid(); return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password')) + .then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let progressCalled = false; + + Realm.openAsync(config, + (error, realm) => { + if (error) { + reject(error); + return; + } + + TestCase.assertTrue(progressCalled); + resolve(); + }, + (transferred, total) => { + progressCalled = true; + }); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }, + + + // All tests releated to partial sync is assemble in one big test. + // Since it is the same instance of ROS running, it is virtually impossible + // to reset the state between the tests. + // In the future we should away from this style of testing. + testPartialSync() { + if (!isNodeProccess) { + return; + } + + var user; + var realm; + + const username = uuid(); + const expectedObjectsCount = 3; + + function __partialIsAllowed() { + // test: __partial is allowed + let config1 = { + sync: { + user: user, + url: `realm://localhost:9080/default/__partial/`, + partial: true, + _disablePartialSyncUrlChecks: true + } + }; + const realm = new Realm(config1); + TestCase.assertFalse(realm.isClosed); + realm.close(); + } + + function __partialIsNotAllowed() { + let config2 = { + sync: { + user: user, + url: `realm://localhost:9080/default/__partial/`, // <--- not allowed URL + partial: true, + } + }; + TestCase.assertThrows(() => new Realm(config2)); + } + + function shouldFail() { + let config = { + sync: { + user: user, + url: 'realm://localhost:9080/~/default', + partial: false, // <---- calling subscribe should fail + error: (session, error) => console.log(error) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; + + Realm.deleteFile(config); + const realm = new Realm(config); + TestCase.assertEqual(realm.objects('Dog').length, 0); + TestCase.assertThrows(function () { var subscription = realm.objects('Dog').filtered("name == 'Lassy 1'").subscribe(); } ); + realm.close(); + } + + function defaultRealmInvalidArguments() { + TestCase.assertThrows(() => Realm.automaticSyncConfiguration('foo', 'bar')); // too many arguments + } + + + return runOutOfProcess(__dirname + '/partial-sync-api-helper.js', username, REALM_MODULE_PATH) .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { + return Realm.Sync.User.login('http://localhost:9080', username, 'password').then((u) => { + user = u; + + __partialIsAllowed(); + __partialIsNotAllowed(); + shouldFail(); + defaultRealmInvalidArguments(); + return new Promise((resolve, reject) => { - let config = { - sync: { - user, - url: `realm://localhost:9080/~/${realmName}` - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }], - }; + let config = Realm.automaticSyncConfiguration(); + config.schema = [{ name: 'Dog', properties: { name: 'string' } }]; + Realm.deleteFile(config); - let progressCalled = false; + realm = new Realm(config); + const session = realm.syncSession; + TestCase.assertInstanceOf(session, Realm.Sync.Session); + TestCase.assertEqual(session.user.identity, user.identity); + TestCase.assertEqual(session.state, 'active'); - Realm.openAsync(config, - (error, realm) => { - if (error) { - reject(error); - return; + var results1 = realm.objects('Dog').filtered("name == 'Lassy 1'"); + var results2 = realm.objects('Dog').filtered("name == 'Lassy 2'"); + + var subscription1 = results1.subscribe(); + TestCase.assertEqual(subscription1.state, Realm.Sync.SubscriptionState.Creating); + + var subscription2 = results2.subscribe(); + TestCase.assertEqual(subscription2.state, Realm.Sync.SubscriptionState.Creating); + + let called1 = false; + let called2 = false; + + subscription1.addListener((subscription, state) => { + if (state === Realm.Sync.SubscriptionState.Complete) { + results1.addListener((collection, changeset) => { + TestCase.assertEqual(collection.length, 1); + TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly"); + results1.removeAllListeners(); + subscription1.unsubscribe(); + called1 = true; + }); + } else if (state === Realm.Sync.SubscriptionState.Invalidated) { + subscription1.removeAllListeners(); + if (called1 && called2) { + realm.close(); + resolve('Done'); } + } + }); - TestCase.assertTrue(progressCalled); - resolve(); - }, - (transferred, total) => { - progressCalled = true; - }); + subscription2.addListener((subscription, state) => { + if (state === Realm.Sync.SubscriptionState.Complete) { + results2.addListener((collection, changeset) => { + TestCase.assertEqual(collection.length, 1); + TestCase.assertTrue(collection[0].name === 'Lassy 2', "The object is not synced correctly"); + results2.removeAllListeners(); + subscription2.unsubscribe(); + called2 = true; + }); + } else if (state === Realm.Sync.SubscriptionState.Invalidated) { + subscription2.removeAllListeners(); + if (called1 && called2) { + realm.close(); + resolve('Done'); + } + } + }); - setTimeout(function() { - reject("Progress Notifications API failed to call progress callback for Realm constructor"); - }, 5000); + setTimeout(() => { + reject("listeners never called"); + }, 15000); }); }); }); }, - testPartialSync() { - // FIXME: try to enable for React Native - if (!isNodeProccess) { - return; - } - - const username = uuid(); - const realmName = uuid(); - - return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) - .then(() => { - return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { - let config = { - sync: { - user: user, - url: `realm://localhost:9080/~/${realmName}`, - partial: true, - error: (session, error) => console.log(error) - }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; - - Realm.deleteFile(config); - const realm = new Realm(config); - TestCase.assertEqual(realm.objects('Dog').length, 0); - return realm.subscribeToObjects("Dog", "name == 'Lassy 1'").then(results => { - TestCase.assertEqual(results.length, 1); - TestCase.assertTrue(results[0].name === 'Lassy 1', "The object is not synced correctly"); - }); - }) - }) - }, testClientReset() { // FIXME: try to enable for React Native