Merge branch 'master' of github.com:realm/realm-js into kneth/merge-2.2-into-master

This commit is contained in:
Kenneth Geisshirt 2018-05-03 11:24:09 +02:00
commit c3c93a8947
47 changed files with 2283 additions and 513 deletions

View File

@ -10,6 +10,7 @@ This closes # ???
## ☑️ ToDos ## ☑️ ToDos
<!-- Add your own todos here --> <!-- Add your own todos here -->
* [ ] 📝 Changelog entry * [ ] 📝 Changelog entry
* [ ] 📝 `Compatibility` label is updated or copied from previous entry
* [ ] 🚦 Tests * [ ] 🚦 Tests
* [ ] 📝 Public documentation PR created or is not necessary * [ ] 📝 Public documentation PR created or is not necessary
* [ ] 💥 `Breaking` label has been applied or is not necessary * [ ] 💥 `Breaking` label has been applied or is not necessary

View File

@ -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) 2.2.20 Release notes (2018-4-13)
============================================================= =============================================================
### Breaking changes ### Breaking changes
@ -97,8 +241,7 @@
* None. * None.
### Bug fixes ### 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 ### Internal
* Updated to Realm Sync 2.2.15. * Updated to Realm Sync 2.2.15.
@ -113,7 +256,7 @@
* None. * None.
### Bug fixes ### Bug fixes
* [Object Server] Fixed handling of SSL certificates for the sync client. * [Sync] Fixed handling of SSL certificates for the sync client.
### Internal ### Internal
* Updated to Realm Sync 2.2.14. * Updated to Realm Sync 2.2.14.
@ -143,7 +286,7 @@
* None. * None.
### Bug fixes ### 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 ### Internal
* Updated to Realm Sync 2.2.12. * Updated to Realm Sync 2.2.12.
@ -170,12 +313,12 @@
* None. * None.
### Enhancements ### 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 ### Bug fixes
* None. * None.
### Internaæ ### Internal
* Updated to Realm Sync 2.2.10. * Updated to Realm Sync 2.2.10.
@ -185,11 +328,11 @@
* None. * None.
### Enhancements ### 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 * Add schema name to missing primary key error message
### Bug fixes ### 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 ### Internal
* None. * None.
@ -204,7 +347,7 @@
* None. * None.
### Bug fixes ### 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 ### Internal
* None. * None.
@ -218,7 +361,7 @@
* None. * None.
### Bug fixes ### 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). * 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). * [Electron] Fixed a `dlopen` error related to OpenSSL that prevented using realm-js on Linux (#1636).
@ -234,8 +377,8 @@
* None. * None.
### Bug fixes ### Bug fixes
* [Object Server] Fixed a bug where errors in `refreshAdminToken` wasn't catched (#1627). * [Sync] Fixed a bug where errors in `refreshAdminToken` wasn't catched (#1627).
* [Object Server] Added `_getExitingUser` to the Chrome debugging support library. * [Sync] Added `_getExitingUser` to the Chrome debugging support library.
### Internal ### Internal
* None. * None.
@ -249,8 +392,8 @@
* None. * None.
### Bug fixes ### Bug fixes
* [Object Server] Fixed a bug in upload progress reporting. * [Sync] 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 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 ### Internal
* None. * None.
@ -264,7 +407,7 @@
* None. * None.
### Bug fixes ### Bug fixes
* [Object Server] Added missing `Realm.Sync` listener functions. * [Sync] Added missing `Realm.Sync` listener functions.
### Internal ### Internal
* None. * None.
@ -279,7 +422,7 @@
* None. * None.
### Bug fixes ### 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 ### Internal
* None. * None.
@ -292,8 +435,8 @@
### Enhancements ### 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. * 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. * Potential performance enhancements in cases of many writes between queries.
* [Object Server] Added method `Realm.Sync.User.authenticate` to unify authentication of users. * [Sync] Added method `Realm.Sync.User.authenticate` to unify authentication of users.
* [Object Server] Added JWT authenfication (#1548). * [Sync] Added JWT authenfication (#1548).
### Bug fixes ### Bug fixes
* Fix a bug where `Realm.open` could unexpectedly raise a "Realm at path ... already opened with different schema version" error. * 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. * None.
### Bug fixes ### 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 ### 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"). * Updated to Realm Sync 2.1.10 (see "Bug fixes").
@ -345,11 +488,11 @@
* None. * None.
### Bug fixes ### 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 ### 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. * [Sync] 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] Base64 strings can now be assigned to Data columns.
2.0.12 Release notes (2017-12-1) 2.0.12 Release notes (2017-12-1)
============================================================= =============================================================
@ -375,8 +518,8 @@
* None * None
### Bug fixes ### Bug fixes
* [Object Server] Fixed a bug where deleted-then-recreated objects with identical primary keys to become empty. * [Sync] 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 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 ### Internal
* Updated to Realm Sync 2.1.7 (see under "Bug fixes"). * Updated to Realm Sync 2.1.7 (see under "Bug fixes").
@ -415,10 +558,10 @@
* None. * None.
### Enhancements ### Enhancements
* [Object Server] Improving performance of processing large changesets. * [Sync] Improving performance of processing large changesets.
### Bug fixes ### Bug fixes
* [Object Server] Changesets over 16MB in size are now handled correctly. * [Sync] Changesets over 16MB in size are now handled correctly.
### Internal ### Internal
* Updated to Realm Sync 2.1.6. * Updated to Realm Sync 2.1.6.

View File

@ -1,5 +1,6 @@
PACKAGE_NAME=realm-js PACKAGE_NAME=realm-js
VERSION=2.2.20 VERSION=2.4.0
REALM_CORE_VERSION=5.1.2 REALM_CORE_VERSION=5.4.0
REALM_SYNC_VERSION=2.2.17 REALM_SYNC_VERSION=3.0.0
REALM_OBJECT_SERVER_VERSION=2.5.1 REALM_OBJECT_SERVER_VERSION=3.0.0

View File

@ -134,6 +134,27 @@ class Collection {
*/ */
snapshot() {} 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} * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries Array.prototype.entries}
* @returns {Realm.Collection~Iterator<T>} of each `[index, object]` pair in the collection * @returns {Realm.Collection~Iterator<T>} of each `[index, object]` pair in the collection
@ -400,15 +421,21 @@ class Collection {
* The callback function is called with two arguments: * The callback function is called with two arguments:
* - `collection`: the collection instance that changed, * - `collection`: the collection instance that changed,
* - `changes`: a dictionary with keys `insertions`, `modifications` and `deletions`, * - `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. * @throws {Error} If `callback` is not a function.
* @example * @example
* wines.addListener((collection, changes) => { * wines.addListener((collection, changes) => {
* // collection === wines * // collection === wines
* console.log(`${changes.insertions.length} insertions`); * if (changes.partial_sync.new_state == Realm.Sync.SubscriptionState.Initialized) {
* console.log(`${changes.modifications.length} modifications`); * console.log('Our subset is ready');
* console.log(`${changes.deletions.length} deletions`); * console.log(`${changes.insertions.length} insertions`);
* console.log(`new size of collection: ${collection.length}`); * console.log(`${changes.modifications.length} modifications`);
* console.log(`${changes.deletions.length} deletions`);
* console.log(`new size of collection: ${collection.length}`);
* }
* }); * });
*/ */
addListener(callback) {} addListener(callback) {}

View File

@ -21,7 +21,7 @@
* They are created exclusively by the client and are processed by the server * They are created exclusively by the client and are processed by the server
* as indicated by the status fields. * as indicated by the status fields.
* PermissionChange objects allow to grant and revoke permissions by setting * 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 * If any of these flags are not set, these are merged
* with either the existing or default permissions as applicable. As a * with either the existing or default permissions as applicable. As a
* side-effect this causes that the default permissions are permanently * side-effect this causes that the default permissions are permanently
@ -30,7 +30,7 @@
* ErrorCode will be updated accordingly. * ErrorCode will be updated accordingly.
*/ */
class PermissionChange { class PermissionChange {
/** /**
* Gets the unique identifier of this object in the Management realm. * Gets the unique identifier of this object in the Management realm.
* @type {string} * @type {string}
@ -229,3 +229,161 @@ class PermissionOfferResponse {
*/ */
get realmUrl() {} 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<Realm.Permission> 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<Realm.Sync.Permissions.User>}
*/
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<Realm.Sync.Permissions.Permission>}
*/
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<Realm.Sync.Permission>}
*/
get permissions() {}
}

View File

@ -119,12 +119,43 @@ class Realm {
*/ */
static openAsync(config, callback, progressCallback) {} 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. * 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. * All objects and collections from this Realm are no longer valid after calling this method.
*/ */
close() {} 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. * 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. * @param {Realm~ObjectType} type - The type of Realm object to create.
@ -243,15 +274,16 @@ class Realm {
compact() {} compact() {}
/** /**
* If the Realm is a partially synchronized Realm, fetch and synchronize the objects * Writes a compacted copy of the Realm to the given path.
* of a given object type that match the given query (in string format).
* *
* **Partial synchronization is a tech preview. Its APIs are subject to change.** * The destination file cannot already exist.
* @param {Realm~ObjectType} type - The type of Realm objects to retrieve. *
* @param {string} query - Query used to filter objects. * Note that if this method is called from within a write transaction, the current data is written,
* @return {Promise} - a promise that will be resolved with the Realm.Results instance when it's available. * 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) {}
} }
/** /**

View File

@ -270,6 +270,55 @@ class User {
*/ */
static register(server, username, password, callback) {} 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<void>} 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<void>} 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<void>} 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<void>} 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 * Create an admin user for the given authentication server with an existing token
* @param {string} adminToken - existing admin token * @param {string} adminToken - existing admin token
@ -468,6 +517,69 @@ class Session {
removeProgressNotification(progressCallback) {} 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. * A Realm Worker can be used to process Sync events in multiple automatically-managed child processes.
* *
@ -528,7 +640,7 @@ class Worker {
*/ */
class Adapter { 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} localPath - the local path where realm files are stored
* @param {string} serverUrl - the sync server to listen to * @param {string} serverUrl - the sync server to listen to
* @param {SyncUser} adminUser - an admin user obtained by calling `new Realm.Sync.User.adminUser` * @param {SyncUser} adminUser - an admin user obtained by calling `new Realm.Sync.User.adminUser`

View File

@ -20,7 +20,7 @@ public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override @Override
protected boolean getUseDeveloperSupport() { public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG; return BuildConfig.DEBUG;
} }

View File

@ -41,6 +41,7 @@ export const propTypes = {};
'RESULTS', 'RESULTS',
'USER', 'USER',
'SESSION', 'SESSION',
'SUBSCRIPTION',
'UNDEFINED', 'UNDEFINED',
].forEach(function(type) { ].forEach(function(type) {
Object.defineProperty(objectTypes, type, { Object.defineProperty(objectTypes, type, {

View File

@ -26,6 +26,7 @@ import Results, { createResults } from './results';
import RealmObject, * as objects from './objects'; import RealmObject, * as objects from './objects';
import User, { createUser } from './user'; import User, { createUser } from './user';
import Session, { createSession } from './session'; import Session, { createSession } from './session';
import Subscription, { createSubscription } from './subscription';
import * as rpc from './rpc'; import * as rpc from './rpc';
import * as util from './util'; import * as util from './util';
import { static as staticUserMethods } from '../user-methods'; 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.REALM, createRealm);
rpc.registerTypeConverter(objectTypes.USER, createUser); rpc.registerTypeConverter(objectTypes.USER, createUser);
rpc.registerTypeConverter(objectTypes.SESSION, createSession); rpc.registerTypeConverter(objectTypes.SESSION, createSession);
rpc.registerTypeConverter(objectTypes.SUBSCRIPTION, createSubscription);
function createRealm(_, info) { function createRealm(_, info) {
let realm = Object.create(Realm.prototype); let realm = Object.create(Realm.prototype);
@ -130,7 +132,6 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
'close', 'close',
'_waitForDownload', '_waitForDownload',
'_objectForObjectId', '_objectForObjectId',
'_subscribeToObjects',
]); ]);
// Mutating methods: // Mutating methods:
@ -147,7 +148,8 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
const Sync = { const Sync = {
User, User,
Session Session,
Subscription,
}; };
Object.defineProperties(Realm, { Object.defineProperties(Realm, {

View File

@ -30,6 +30,7 @@ createMethods(Results.prototype, objectTypes.RESULTS, [
'filtered', 'filtered',
'sorted', 'sorted',
'snapshot', 'snapshot',
'subscribe',
'isValid', 'isValid',
'indexOf', 'indexOf',
'min', 'min',

View File

@ -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'
]);

View File

@ -42,7 +42,7 @@ export default class User {
} }
createMethods(User.prototype, objectTypes.USER, [ createMethods(User.prototype, objectTypes.USER, [
'logout', '_logout',
'_sessionForOnDiskPath' '_sessionForOnDiskPath'
]); ]);

View File

@ -18,6 +18,8 @@
'use strict'; 'use strict';
const URL = require('url-parse');
let getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) { let getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) {
return Object.getOwnPropertyNames(obj).reduce(function (descriptors, name) { return Object.getOwnPropertyNames(obj).reduce(function (descriptors, name) {
descriptors[name] = Object.getOwnPropertyDescriptor(obj, name); descriptors[name] = Object.getOwnPropertyDescriptor(obj, name);
@ -149,29 +151,118 @@ module.exports = function(realmConstructor) {
setConstructorOnPrototype(realmConstructor.Sync.User); setConstructorOnPrototype(realmConstructor.Sync.User);
setConstructorOnPrototype(realmConstructor.Sync.Session); 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) { if (realmConstructor.Sync._setFeatureToken) {
realmConstructor.Sync.setFeatureToken = function(featureToken) { realmConstructor.Sync.setFeatureToken = function(featureToken) {
if (typeof featureToken !== 'string' && !(featureToken instanceof String)) { console.log('Realm.Sync.setFeatureToken() is deprecated and you can remove any calls to it.');
throw new Error("featureToken should be a string");
}
realmConstructor.Sync._setFeatureToken(featureToken.trim());
} }
} }
realmConstructor.prototype.subscribeToObjects = function(objectType, query) { // Keep these value in sync with subscription_state.hpp
const realm = this; realmConstructor.Sync.SubscriptionState = {
let promise = new Promise((resolve, reject) => { Error: -1, // An error occurred while creating or processing the partial sync subscription.
realm._subscribeToObjects(objectType, query, function(err, results) { Creating: 2, // The subscription is being created.
if (err) { Pending: 0, // The subscription was created, but has not yet been processed by the sync server.
reject(err); Complete: 1, // The subscription has been processed by the sync server and data is being synced to the device.
} else { Invalidated: 3, // The subscription has been removed.
resolve(results);
}
});
});
return promise;
}; };
// 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. // TODO: Remove this now useless object.

101
lib/index.d.ts vendored
View File

@ -81,7 +81,7 @@ declare namespace Realm {
path?: string; path?: string;
readOnly?: boolean; readOnly?: boolean;
inMemory?: boolean; inMemory?: boolean;
schema?: ObjectClass[] | ObjectSchema[]; schema?: (ObjectClass | ObjectSchema)[];
schemaVersion?: number; schemaVersion?: number;
sync?: Realm.Sync.SyncConfiguration; sync?: Realm.Sync.SyncConfiguration;
deleteRealmIfMigrationNeeded?: boolean; deleteRealmIfMigrationNeeded?: boolean;
@ -161,6 +161,11 @@ declare namespace Realm {
sorted(descriptor: SortDescriptor[]): Results<T>; sorted(descriptor: SortDescriptor[]): Results<T>;
sorted(descriptor: string, reverse?: boolean): Results<T>; sorted(descriptor: string, reverse?: boolean): Results<T>;
/**
* @returns Results<T>
*/
subscribe(subscriptionName?: string): Realm.Sync.Subscription;
/** /**
* @returns Results * @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 }, callback: (error: Error | null, user: User | null) => void): void;
static registerWithProvider(server: string, options: { provider: string, providerToken: string, userInfo: any }): Promise<Realm.Sync.User>; static registerWithProvider(server: string, options: { provider: string, providerToken: string, userInfo: any }): Promise<Realm.Sync.User>;
static requestPasswordReset(server: string, email: string): Promise<void>;
static completePasswordReset(server:string, reset_token:string, new_password:string): Promise<void>;
static requestEmailConfirmation(server:string, email:string): Promise<void>;
static confirmEmail(server:string, confirmation_token:string): Promise<void>;
authenticate(server: string, provider: string, options: any): Promise<Realm.Sync.User>; authenticate(server: string, provider: string, options: any): Promise<Realm.Sync.User>;
logout(): void; logout(): void;
openManagementRealm(): Realm; openManagementRealm(): Realm;
@ -374,6 +387,7 @@ declare namespace Realm.Sync {
open_ssl_verify_callback?: SSLVerifyCallback; open_ssl_verify_callback?: SSLVerifyCallback;
error?: ErrorCallback; error?: ErrorCallback;
partial?: boolean; partial?: boolean;
_disablePartialSyncUrlChecks?:boolean;
} }
type ProgressNotificationCallback = (transferred: number, transferable: number) => void; type ProgressNotificationCallback = (transferred: number, transferable: number) => void;
@ -394,6 +408,30 @@ declare namespace Realm.Sync {
removeProgressNotification(progressCallback: ProgressNotificationCallback): void; 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 * AuthError
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.AuthError.html } * @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<void>; function removeListener(regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): Promise<void>;
function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void; function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void;
function initiateClientReset(path: string): void; function initiateClientReset(path: string): void;
/**
* @deprecated, to be removed in future versions
*/
function setFeatureToken(token: string): void; function setFeatureToken(token: string): void;
type Instruction = { 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<Realm> { interface ProgressPromise extends Promise<Realm> {
progress(callback: Realm.Sync.ProgressNotificationCallback): Promise<Realm> progress(callback: Realm.Sync.ProgressNotificationCallback): Promise<Realm>
@ -497,11 +575,17 @@ declare class Realm {
*/ */
static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void, progressCallback?: Realm.Sync.ProgressNotificationCallback): void 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. * Delete the Realm file for the given configuration.
* @param {Configuration} config * @param {Configuration} config
*/ */
static deleteFile(config: Realm.Configuration): void static deleteFile(config: Realm.Configuration): void;
/** /**
* @param {Realm.Configuration} config? * @param {Realm.Configuration} config?
@ -524,7 +608,7 @@ declare class Realm {
* @param {boolean} update? * @param {boolean} update?
* @returns T * @returns T
*/ */
create<T>(type: string | Realm.ObjectClass | Function, properties: T & Realm.ObjectPropsType, update?: boolean): T; create<T>(type: string | Realm.ObjectClass | Function, properties: T | Realm.ObjectPropsType, update?: boolean): T;
/** /**
* @param {Realm.Object|Realm.Object[]|Realm.List<any>|Realm.Results<any>|any} object * @param {Realm.Object|Realm.Object[]|Realm.List<any>|Realm.Results<any>|any} object
@ -602,9 +686,16 @@ declare class Realm {
compact(): boolean; compact(): boolean;
/** /**
* @returns Promise<Results<T>> * Write a copy to destination path
* @param path destination path
* @param encryptionKey encryption key to use
* @returns void
*/ */
subscribeToObjects<T>(objectType: string, query: string): Promise<Realm.Results<T>>; 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' { declare module 'realm' {

View File

@ -25,7 +25,7 @@ function nodeRequire(module) {
return require_method(module); return require_method(module);
} }
const Realm = require('.'); const Realm = nodeRequire('.');
let impl; let impl;
process.on('message', (m) => { process.on('message', (m) => {

View File

@ -85,7 +85,7 @@ class FunctionListener {
throw e; throw e;
}); });
} }
}; }
class OutOfProcListener { class OutOfProcListener {
constructor(regex, regexStr, worker) { constructor(regex, regexStr, worker) {
@ -120,7 +120,7 @@ class OutOfProcListener {
} }
this.worker.onchange(changes); this.worker.onchange(changes);
} }
}; }
class Listener { class Listener {
constructor(Sync, server, user) { constructor(Sync, server, user) {
@ -231,7 +231,7 @@ class Listener {
} }
this.initPromises = []; this.initPromises = [];
} }
}; }
let listener; let listener;
function addListener(server, user, regex, event, callback) { function addListener(server, user, regex, event, callback) {

View File

@ -98,7 +98,7 @@ function print_error() {
function validateRefresh(user, localRealmPath, response, json) { function validateRefresh(user, localRealmPath, response, json) {
let session = user._sessionForOnDiskPath(localRealmPath); let session = user._sessionForOnDiskPath(localRealmPath);
if (!session) { 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; return;
} }
@ -108,7 +108,7 @@ function validateRefresh(user, localRealmPath, response, json) {
if (errorHandler) { if (errorHandler) {
errorHandler(session, error); errorHandler(session, error);
} else { } 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; return;
} }
@ -155,6 +155,7 @@ function refreshAdminToken(user, localRealmPath, realmUrl) {
}) })
.catch((e) => { .catch((e) => {
print_error(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 = { const staticMethods = {
get current() { get current() {
const allUsers = this.all; const allUsers = this.all;
@ -378,11 +405,77 @@ const staticMethods = {
return _authenticate(this, server, json) 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, _refreshAccessToken: refreshAccessToken,
}; };
const instanceMethods = { 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() { openManagementRealm() {
let url = url_parse(this.server); let url = url_parse(this.server);
if (url.protocol === 'http:') { if (url.protocol === 'http:') {

View File

@ -123,6 +123,6 @@ class Worker {
const message = this._workQueue.shift(); const message = this._workQueue.shift();
worker.send(message); worker.send(message);
} }
}; }
module.exports = Worker; module.exports = Worker;

View File

@ -1,7 +1,7 @@
{ {
"name": "realm", "name": "realm",
"description": "Realm is a mobile database: an alternative to SQLite and key-value stores", "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", "license": "Apache-2.0",
"homepage": "https://realm.io", "homepage": "https://realm.io",
"keywords": [ "keywords": [
@ -90,7 +90,7 @@
"request": "^2.78.0", "request": "^2.78.0",
"stream-counter": "^1.0.0", "stream-counter": "^1.0.0",
"sync-request": "^3.0.1", "sync-request": "^3.0.1",
"url-parse": "^1.1.7" "url-parse": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^4.0.35", "@types/node": "^4.0.35",

View File

@ -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; package io.realm.react;
import android.content.res.AssetManager; import android.content.res.AssetManager;

View File

@ -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; package io.realm.react;
import com.facebook.react.ReactPackage; import com.facebook.react.ReactPackage;

View File

@ -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<String, List<String>> 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<String>());
}
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<String> 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();
}
}
}
}

View File

@ -45,6 +45,7 @@ LOCAL_SRC_FILES += src/rpc.cpp
LOCAL_SRC_FILES += src/jsc/jsc_init.cpp LOCAL_SRC_FILES += src/jsc/jsc_init.cpp
LOCAL_SRC_FILES += src/jsc/jsc_value.cpp LOCAL_SRC_FILES += src/jsc/jsc_value.cpp
LOCAL_SRC_FILES += src/android/io_realm_react_RealmReactModule.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/jsc_override.cpp
LOCAL_SRC_FILES += src/android/platform.cpp LOCAL_SRC_FILES += src/android/platform.cpp
LOCAL_SRC_FILES += src/object-store/src/impl/collection_change_builder.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/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_file.cpp
LOCAL_SRC_FILES += src/object-store/src/sync/impl/sync_metadata.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 endif
LOCAL_C_INCLUDES := src LOCAL_C_INCLUDES := src

View File

@ -99,6 +99,7 @@
"src/object-store/src/sync/impl/sync_client.hpp", "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_file.hpp",
"src/object-store/src/sync/impl/sync_metadata.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/partial_sync.hpp",
"src/object-store/src/sync/sync_config.hpp", "src/object-store/src/sync/sync_config.hpp",
"src/object-store/src/sync/sync_manager.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_session.cpp",
"src/object-store/src/sync/sync_config.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_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"
], ],
}] }]
], ],

View File

@ -147,7 +147,17 @@ xctest() {
echo "Launching application. (output is in $(pwd)/build/out.txt)" 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)" echo "Shuttting down ${SIM_DEVICE_NAME} simulator. (device is not deleted. you can use it to debug the app)"
shutdown_ios_simulator shutdown_ios_simulator
@ -171,7 +181,7 @@ setup_ios_simulator() {
delete_ios_simulator >/dev/null 2>&1 delete_ios_simulator >/dev/null 2>&1
#parse devices #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} echo using iOS Runtime ${IOS_RUNTIME} to create new simulator ${SIM_DEVICE_NAME}
#create new test simulator #create new test simulator

View File

@ -41,6 +41,8 @@
3FCE2A931F58BE0300D4855B /* uuid.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A911F58BDFF00D4855B /* uuid.cpp */; }; 3FCE2A931F58BE0300D4855B /* uuid.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A911F58BDFF00D4855B /* uuid.cpp */; };
3FCE2A971F58BE2200D4855B /* sync_permission.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3FCE2A951F58BE1D00D4855B /* sync_permission.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 */; }; 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 */; }; 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 */; }; 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 */; }; 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; }; 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 = "<group>"; }; 423737AF1F7E333400FAEDFF /* partial_sync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = partial_sync.cpp; path = src/sync/partial_sync.cpp; sourceTree = "<group>"; };
423737B01F7E333400FAEDFF /* partial_sync.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = partial_sync.hpp; path = src/sync/partial_sync.hpp; sourceTree = "<group>"; }; 423737B01F7E333400FAEDFF /* partial_sync.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = partial_sync.hpp; path = src/sync/partial_sync.hpp; sourceTree = "<group>"; };
425A120F20235A1400C2F932 /* work_queue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = work_queue.cpp; sourceTree = "<group>"; };
425A121020235A1400C2F932 /* work_queue.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = work_queue.hpp; sourceTree = "<group>"; };
426FCDFF1F7DA2F9005565DC /* sync_config.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sync_config.cpp; path = src/sync/sync_config.cpp; sourceTree = "<group>"; }; 426FCDFF1F7DA2F9005565DC /* sync_config.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = sync_config.cpp; path = src/sync/sync_config.cpp; sourceTree = "<group>"; };
502B07E31E2CD1FA007A84ED /* object.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = object.cpp; path = src/object.cpp; sourceTree = "<group>"; }; 502B07E31E2CD1FA007A84ED /* object.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = object.cpp; path = src/object.cpp; sourceTree = "<group>"; };
502B07E51E2CD20D007A84ED /* object.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = object.hpp; path = src/object.hpp; sourceTree = "<group>"; }; 502B07E51E2CD20D007A84ED /* object.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = object.hpp; path = src/object.hpp; sourceTree = "<group>"; };
@ -474,6 +478,8 @@
504CF8521EBCAE3600A9A4B6 /* impl */ = { 504CF8521EBCAE3600A9A4B6 /* impl */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
425A120F20235A1400C2F932 /* work_queue.cpp */,
425A121020235A1400C2F932 /* work_queue.hpp */,
504CF8531EBCAE3600A9A4B6 /* apple */, 504CF8531EBCAE3600A9A4B6 /* apple */,
504CF8581EBCAE3600A9A4B6 /* network_reachability.hpp */, 504CF8581EBCAE3600A9A4B6 /* network_reachability.hpp */,
504CF8591EBCAE3600A9A4B6 /* sync_client.hpp */, 504CF8591EBCAE3600A9A4B6 /* sync_client.hpp */,
@ -873,6 +879,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
4261AF8E203C42000052450D /* work_queue.cpp in Sources */,
F63FF2E21C15921A00B3B8E0 /* base64.cpp in Sources */, F63FF2E21C15921A00B3B8E0 /* base64.cpp in Sources */,
022BF1021E7266DF00F382F1 /* binding_callback_thread_observer.cpp in Sources */, 022BF1021E7266DF00F382F1 /* binding_callback_thread_observer.cpp in Sources */,
02414BA51CE6ABCF00A8669F /* collection_change_builder.cpp in Sources */, 02414BA51CE6ABCF00A8669F /* collection_change_builder.cpp in Sources */,
@ -934,6 +941,7 @@
F63FF31E1C1642BB00B3B8E0 /* GCDWebServerRequest.m in Sources */, F63FF31E1C1642BB00B3B8E0 /* GCDWebServerRequest.m in Sources */,
F63FF31F1C1642BB00B3B8E0 /* GCDWebServerResponse.m in Sources */, F63FF31F1C1642BB00B3B8E0 /* GCDWebServerResponse.m in Sources */,
F63FF3271C1642BB00B3B8E0 /* GCDWebServerStreamedResponse.m in Sources */, F63FF3271C1642BB00B3B8E0 /* GCDWebServerStreamedResponse.m in Sources */,
425A121120235A1400C2F932 /* work_queue.cpp in Sources */,
F63FF3231C1642BB00B3B8E0 /* GCDWebServerURLEncodedFormRequest.m in Sources */, F63FF3231C1642BB00B3B8E0 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -961,7 +969,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2.2.20; CURRENT_PROJECT_VERSION = 2.4.0;
CXX = "$(SRCROOT)/../scripts/ccache-clang++.sh"; CXX = "$(SRCROOT)/../scripts/ccache-clang++.sh";
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -1025,7 +1033,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2.2.20; CURRENT_PROJECT_VERSION = 2.4.0;
CXX = "$(SRCROOT)/../scripts/ccache-clang++.sh"; CXX = "$(SRCROOT)/../scripts/ccache-clang++.sh";
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -1115,10 +1123,7 @@
"-isystem", "-isystem",
"$(SRCROOT)/../vendor/sync/include", "$(SRCROOT)/../vendor/sync/include",
); );
OTHER_LIBTOOLFLAGS = ( OTHER_LIBTOOLFLAGS = "$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios-dbg.a $(SRCROOT)/../vendor/realm-ios/librealm-ios-dbg.a";
"$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios-dbg.a",
"$(SRCROOT)/../vendor/realm-ios/librealm-ios-dbg.a",
);
PRODUCT_NAME = RealmJS; PRODUCT_NAME = RealmJS;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
}; };
@ -1140,10 +1145,7 @@
"-isystem", "-isystem",
"$(SRCROOT)/../vendor/sync/include", "$(SRCROOT)/../vendor/sync/include",
); );
OTHER_LIBTOOLFLAGS = ( OTHER_LIBTOOLFLAGS = "$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios.a $(SRCROOT)/../vendor/realm-ios/librealm-ios.a";
"$(SRCROOT)/../vendor/realm-ios/librealm-parser-ios.a",
"$(SRCROOT)/../vendor/realm-ios/librealm-ios.a",
);
PRODUCT_NAME = RealmJS; PRODUCT_NAME = RealmJS;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
}; };

View File

@ -23,17 +23,49 @@
#include "io_realm_react_RealmReactModule.h" #include "io_realm_react_RealmReactModule.h"
#include "rpc.hpp" #include "rpc.hpp"
#include "platform.hpp" #include "platform.hpp"
#include "jni_utils.hpp"
using namespace realm::rpc; using namespace realm::rpc;
using namespace realm::jni_util;
static RPCServer *s_rpc_server; static RPCServer *s_rpc_server;
extern bool realmContextInjected; extern bool realmContextInjected;
jclass ssl_helper_class;
namespace realm { namespace realm {
// set the AssetManager used to access bundled files within the APK // set the AssetManager used to access bundled files within the APK
void set_asset_manager(AAssetManager* assetManager); 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<jclass>(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 JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_setDefaultRealmFileDirectory
(JNIEnv *env, jclass, jstring fileDir, jobject javaAssetManager) (JNIEnv *env, jclass, jstring fileDir, jobject javaAssetManager)
{ {

50
src/android/jni_utils.cpp Normal file
View File

@ -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 <memory>
using namespace realm::jni_util;
static std::unique_ptr<JniUtils> s_instance;
void JniUtils::initialize(JavaVM* vm, jint vm_version) noexcept
{
s_instance = std::unique_ptr<JniUtils>(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<void**>(&env), s_instance->m_vm_version) != JNI_OK) {
if (attach_if_needed) {
jint ret = s_instance->m_vm->AttachCurrentThread(reinterpret_cast<void**>(&env), nullptr);
}
}
return env;
}
void JniUtils::detach_current_thread()
{
s_instance->m_vm->DetachCurrentThread();
}

59
src/android/jni_utils.hpp Normal file
View File

@ -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 <jni.h>
#include <vector>
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

View File

@ -23,6 +23,9 @@
#include "js_observable.hpp" #include "js_observable.hpp"
#include "collection_notifications.hpp" #include "collection_notifications.hpp"
#if REALM_ENABLE_SYNC
#include "sync/subscription_state.hpp"
#endif
namespace realm { namespace realm {
namespace js { namespace js {
@ -39,7 +42,7 @@ struct CollectionClass : ClassDefinition<T, Collection, ObservableClass<T>> {
using Value = js::Value<T>; using Value = js::Value<T>;
std::string const name = "Collection"; std::string const name = "Collection";
static inline ValueType create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set); static inline ValueType create_collection_change_set(ContextType ctx, const CollectionChangeSet &change_set);
}; };
@ -48,7 +51,7 @@ typename T::Value CollectionClass<T>::create_collection_change_set(ContextType c
{ {
ObjectType object = Object::create_empty(ctx); ObjectType object = Object::create_empty(ctx);
std::vector<ValueType> deletions, insertions, modifications; std::vector<ValueType> deletions, insertions, modifications;
if (change_set.deletions.count() == std::numeric_limits<size_t>::max()) { if (change_set.deletions.count() == std::numeric_limits<size_t>::max()) {
deletions.push_back(Value::from_null(ctx)); deletions.push_back(Value::from_null(ctx));
} }
@ -58,12 +61,12 @@ typename T::Value CollectionClass<T>::create_collection_change_set(ContextType c
} }
} }
Object::set_property(ctx, object, "deletions", Object::create_array(ctx, deletions)); Object::set_property(ctx, object, "deletions", Object::create_array(ctx, deletions));
for (auto index : change_set.insertions.as_indexes()) { for (auto index : change_set.insertions.as_indexes()) {
insertions.push_back(Value::from_number(ctx, index)); insertions.push_back(Value::from_number(ctx, index));
} }
Object::set_property(ctx, object, "insertions", Object::create_array(ctx, insertions)); Object::set_property(ctx, object, "insertions", Object::create_array(ctx, insertions));
for (auto index : change_set.modifications.as_indexes()) { for (auto index : change_set.modifications.as_indexes()) {
modifications.push_back(Value::from_number(ctx, index)); modifications.push_back(Value::from_number(ctx, index));
} }

View File

@ -35,7 +35,6 @@
#include "js_sync.hpp" #include "js_sync.hpp"
#include "sync/sync_config.hpp" #include "sync/sync_config.hpp"
#include "sync/sync_manager.hpp" #include "sync/sync_manager.hpp"
#include "sync/partial_sync.hpp"
#endif #endif
#include "shared_realm.hpp" #include "shared_realm.hpp"
@ -184,11 +183,10 @@ public:
static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &);
static void close(ContextType, ObjectType, Arguments, ReturnValue &); static void close(ContextType, ObjectType, Arguments, ReturnValue &);
static void compact(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 delete_model(ContextType, ObjectType, Arguments, ReturnValue &);
static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&); static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&);
#if REALM_ENABLE_SYNC static void privileges(ContextType, ObjectType, Arguments, ReturnValue&);
static void subscribe_to_objects(ContextType, ObjectType, Arguments, ReturnValue &);
#endif
// properties // properties
static void get_empty(ContextType, ObjectType, ReturnValue &); static void get_empty(ContextType, ObjectType, ReturnValue &);
@ -244,11 +242,12 @@ public:
{"removeAllListeners", wrap<remove_all_listeners>}, {"removeAllListeners", wrap<remove_all_listeners>},
{"close", wrap<close>}, {"close", wrap<close>},
{"compact", wrap<compact>}, {"compact", wrap<compact>},
{"writeCopyTo", wrap<writeCopyTo>},
{"deleteModel", wrap<delete_model>}, {"deleteModel", wrap<delete_model>},
{"privileges", wrap<privileges>},
{"_objectForObjectId", wrap<object_for_object_id>}, {"_objectForObjectId", wrap<object_for_object_id>},
#if REALM_ENABLE_SYNC #if REALM_ENABLE_SYNC
{"_waitForDownload", wrap<wait_for_download_completion>}, {"_waitForDownload", wrap<wait_for_download_completion>},
{"_subscribeToObjects", wrap<subscribe_to_objects>},
#endif #endif
}; };
@ -295,7 +294,8 @@ public:
return name; 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)) { if (Value::is_constructor(ctx, value)) {
FunctionType constructor = Value::to_constructor(ctx, value); FunctionType constructor = Value::to_constructor(ctx, value);
@ -545,7 +545,7 @@ void RealmClass<T>::constructor(ContextType ctx, ObjectType this_object, size_t
template<typename T> template<typename T>
SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Config config, bool schema_updated, SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Config config, bool schema_updated,
ObjectDefaultsMap && defaults, ConstructorMap && constructors) { ObjectDefaultsMap&& defaults, ConstructorMap&& constructors) {
config.execution_context = Context<T>::get_execution_context_id(ctx); config.execution_context = Context<T>::get_execution_context_id(ctx);
SharedRealm realm; SharedRealm realm;
@ -555,9 +555,6 @@ SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Co
catch (const RealmFileException& ex) { catch (const RealmFileException& ex) {
handleRealmFileException(ctx, config, ex); handleRealmFileException(ctx, config, ex);
} }
catch (...) {
throw;
}
GlobalContextType global_context = Context<T>::get_global_context(ctx); GlobalContextType global_context = Context<T>::get_global_context(ctx);
if (!realm->m_binding_context) { if (!realm->m_binding_context) {
@ -794,10 +791,8 @@ void RealmClass<T>::objects(ContextType ctx, ObjectType this_object, Arguments a
args.validate_maximum(1); args.validate_maximum(1);
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
std::string object_type; auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
validated_object_schema_for_value(ctx, realm, args[0], object_type); return_value.set(ResultsClass<T>::create_instance(ctx, realm, object_schema.name));
return_value.set(ResultsClass<T>::create_instance(ctx, realm, object_type));
} }
template<typename T> template<typename T>
@ -806,7 +801,7 @@ void RealmClass<T>::object_for_primary_key(ContextType ctx, ObjectType this_obje
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
std::string object_type; 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); NativeAccessor accessor(ctx, realm, object_schema);
auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]); auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]);
@ -824,8 +819,7 @@ void RealmClass<T>::create(ContextType ctx, ObjectType this_object, Arguments ar
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
realm->verify_open(); realm->verify_open();
std::string object_type; auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type);
ObjectType object = Value::validated_to_object(ctx, args[1], "properties"); ObjectType object = Value::validated_to_object(ctx, args[1], "properties");
if (Value::is_array(ctx, args[1])) { if (Value::is_array(ctx, args[1])) {
@ -902,7 +896,13 @@ void RealmClass<T>::delete_all(ContextType ctx, ObjectType this_object, Argument
} }
for (auto objectSchema : realm->schema()) { 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<T>::compact(ContextType ctx, ObjectType this_object, Arguments a
return_value.set(realm->compact()); return_value.set(realm->compact());
} }
#if REALM_ENABLE_SYNC template<typename T>
namespace { void RealmClass<T>::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 if (args.count == 0) {
inline sync::ObjectID object_id_from_string(std::string const& string) throw std::runtime_error("At least path has to be provided for 'writeCopyTo'");
{
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<int(*)(int)>(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.");
} }
return sync::ObjectID(strtoull(high_string.c_str(), nullptr, 16), strtoull(low_string.c_str(), nullptr, 16));
}
} // unnamed namespace SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object);
#endif // REALM_ENABLE_SYNC
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<const char *>(key_data.data()), key_data.size() };
}
realm->write_copy(path, key);
}
template<typename T> template<typename T>
void RealmClass<T>::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) { void RealmClass<T>::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) {
@ -1045,67 +1045,67 @@ void RealmClass<T>::object_for_object_id(ContextType ctx, ObjectType this_object
if (!sync::has_object_ids(realm->read_group())) if (!sync::has_object_ids(realm->read_group()))
throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms.");
std::string object_type = Value::validated_to_string(ctx, args[0]); auto& object_schema = validated_object_schema_for_value(ctx, realm, args[0]);
validated_object_schema_for_value(ctx, realm, args[0], object_type);
std::string object_id_string = Value::validated_to_string(ctx, args[1]); 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(); 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) { if (ndx != realm::npos) {
return_value.set(RealmObjectClass<T>::create_instance(ctx, realm::Object(realm, object_type, ndx))); return_value.set(RealmObjectClass<T>::create_instance(ctx, realm::Object(realm, object_schema.name, ndx)));
} }
#else #else
throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms.");
#endif // REALM_ENABLE_SYNC #endif // REALM_ENABLE_SYNC
} }
#if REALM_ENABLE_SYNC
template<typename T> template<typename T>
void RealmClass<T>::subscribe_to_objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { void RealmClass<T>::privileges(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_count(3); args.validate_maximum(1);
SharedRealm realm = *get_internal<T, RealmClass<T>>(this_object); using Privilege = realm::ComputedPrivileges;
std::string object_type = Value::validated_to_string(ctx, args[0]); auto has_privilege = [](Privilege actual, Privilege expected) {
std::string query = Value::validated_to_string(ctx, args[1]); return (static_cast<int>(actual) & static_cast<int>(expected)) == static_cast<int>(expected);
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<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
Protected<FunctionType> 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<T>::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<T>::create_instance(protected_ctx, results);
Function<T>::callback(protected_ctx, protected_callback, protected_this, 2, callback_arguments);
}; };
partial_sync::register_query(realm, object_type, query, std::move(cb)); SharedRealm realm = *get_internal<T, RealmClass<T>>(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<RealmObjectClass<T>>(ctx, arg)) {
auto obj = get_internal<T, RealmObjectClass<T>>(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 } // js
} // realm } // realm

View File

@ -28,6 +28,11 @@
#include <realm/parser/parser.hpp> #include <realm/parser/parser.hpp>
#include <realm/parser/query_builder.hpp> #include <realm/parser/query_builder.hpp>
#include <realm/util/optional.hpp>
#ifdef REALM_ENABLE_SYNC
#include "js_sync.hpp"
#include "sync/partial_sync.hpp"
#endif
namespace realm { namespace realm {
namespace js { namespace js {
@ -82,6 +87,9 @@ struct ResultsClass : ClassDefinition<T, realm::js::Results<T>, CollectionClass<
static void filtered(ContextType, ObjectType, Arguments, ReturnValue &); static void filtered(ContextType, ObjectType, Arguments, ReturnValue &);
static void sorted(ContextType, ObjectType, Arguments, ReturnValue &); static void sorted(ContextType, ObjectType, Arguments, ReturnValue &);
static void is_valid(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 &); static void index_of(ContextType, ObjectType, Arguments, ReturnValue &);
@ -107,6 +115,9 @@ struct ResultsClass : ClassDefinition<T, realm::js::Results<T>, CollectionClass<
{"filtered", wrap<filtered>}, {"filtered", wrap<filtered>},
{"sorted", wrap<sorted>}, {"sorted", wrap<sorted>},
{"isValid", wrap<is_valid>}, {"isValid", wrap<is_valid>},
#if REALM_ENABLE_SYNC
{"subscribe", wrap<subscribe>},
#endif
{"min", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Min>>}, {"min", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Min>>},
{"max", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Max>>}, {"max", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Max>>},
{"sum", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Sum>>}, {"sum", wrap<compute_aggregate_on_collection<ResultsClass<T>, AggregateFunc::Sum>>},
@ -141,6 +152,22 @@ typename T::Object ResultsClass<T>::create_instance(ContextType ctx, SharedRealm
return create_object<T, ResultsClass<T>>(ctx, new realm::js::Results<T>(realm, *table)); return create_object<T, ResultsClass<T>>(ctx, new realm::js::Results<T>(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<typename T> template<typename T>
template<typename U> template<typename U>
typename T::Object ResultsClass<T>::create_filtered(ContextType ctx, const U &collection, Arguments args) { typename T::Object ResultsClass<T>::create_filtered(ContextType ctx, const U &collection, Arguments args) {
@ -152,13 +179,17 @@ typename T::Object ResultsClass<T>::create_filtered(ContextType ctx, const U &co
auto query = collection.get_query(); auto query = collection.get_query();
auto const &realm = collection.get_realm(); auto const &realm = collection.get_realm();
auto const &object_schema = collection.get_object_schema(); 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<T> accessor(ctx, realm, object_schema); NativeAccessor<T> accessor(ctx, realm, object_schema);
query_builder::ArgumentConverter<ValueType, NativeAccessor<T>> converter(accessor, &args.value[1], args.count - 1); query_builder::ArgumentConverter<ValueType, NativeAccessor<T>> 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<typename T> template<typename T>
@ -252,6 +283,28 @@ void ResultsClass<T>::is_valid(ContextType ctx, ObjectType this_object, Argument
return_value.set(get_internal<T, ResultsClass<T>>(this_object)->is_valid()); return_value.set(get_internal<T, ResultsClass<T>>(this_object)->is_valid());
} }
#if REALM_ENABLE_SYNC
template<typename T>
void ResultsClass<T>::subscribe(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(1);
auto results = get_internal<T, ResultsClass<T>>(this_object);
auto realm = results->get_realm();
auto sync_config = realm->config().sync_config;
util::Optional<std::string> subscription_name;
if (args.count == 1) {
subscription_name = util::Optional<std::string>(Value::validated_to_string(ctx, args[0]));
}
else {
subscription_name = util::none;
}
auto subscription = partial_sync::subscribe(*results, subscription_name);
return_value.set(SubscriptionClass<T>::create_instance(ctx, std::move(subscription)));
}
#endif
template<typename T> template<typename T>
template<typename Fn> template<typename Fn>
void ResultsClass<T>::index_of(ContextType ctx, Fn& fn, Arguments args, ReturnValue &return_value) { void ResultsClass<T>::index_of(ContextType ctx, Fn& fn, Arguments args, ReturnValue &return_value) {
@ -324,13 +377,13 @@ void ResultsClass<T>::add_listener(ContextType ctx, U& collection, ObjectType th
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx)); Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
auto token = collection.add_notification_callback([=](CollectionChangeSet const& change_set, std::exception_ptr exception) { auto token = collection.add_notification_callback([=](CollectionChangeSet const& change_set, std::exception_ptr exception) {
HANDLESCOPE HANDLESCOPE
ValueType arguments[] { ValueType arguments[] {
static_cast<ObjectType>(protected_this), static_cast<ObjectType>(protected_this),
CollectionClass<T>::create_collection_change_set(protected_ctx, change_set) CollectionClass<T>::create_collection_change_set(protected_ctx, change_set)
}; };
Function<T>::callback(protected_ctx, protected_callback, protected_this, 2, arguments); Function<T>::callback(protected_ctx, protected_callback, protected_this, 2, arguments);
}); });
collection.m_notification_tokens.emplace_back(protected_callback, std::move(token)); collection.m_notification_tokens.emplace_back(protected_callback, std::move(token));
} }

View File

@ -29,9 +29,18 @@
#include "sync/sync_config.hpp" #include "sync/sync_config.hpp"
#include "sync/sync_session.hpp" #include "sync/sync_session.hpp"
#include "sync/sync_user.hpp" #include "sync/sync_user.hpp"
#include "sync/partial_sync.hpp"
#include "realm/util/logger.hpp" #include "realm/util/logger.hpp"
#include "realm/util/uri.hpp" #include "realm/util/uri.hpp"
#if REALM_ANDROID
#include <jni.h>
#include "./android/io_realm_react_RealmReactModule.h"
#include "./android/jni_utils.hpp"
extern jclass ssl_helper_class;
#endif
namespace realm { namespace realm {
namespace js { namespace js {
@ -102,7 +111,7 @@ public:
static void session_for_on_disk_path(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void session_for_on_disk_path(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &);
MethodMap<T> const methods = { MethodMap<T> const methods = {
{"logout", wrap<logout>}, {"_logout", wrap<logout>},
{"_sessionForOnDiskPath", wrap<session_for_on_disk_path>} {"_sessionForOnDiskPath", wrap<session_for_on_disk_path>}
}; };
}; };
@ -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) 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}; const std::string pem_certificate {pem_data, pem_size};
{ {
std::lock_guard<std::mutex> lock {*m_mutex}; std::lock_guard<std::mutex> lock {*m_mutex};
m_ssl_certificate_callback_done = false; m_ssl_certificate_callback_done = false;
@ -555,6 +563,135 @@ void SessionClass<T>::override_server(ContextType ctx, ObjectType this_object, A
} }
} }
template<typename T>
class Subscription : public partial_sync::Subscription {
public:
Subscription(partial_sync::Subscription s) : partial_sync::Subscription(std::move(s)) {}
Subscription(Subscription &&) = default;
std::vector<std::pair<Protected<typename T::Function>, partial_sync::SubscriptionNotificationToken>> m_notification_tokens;
};
template<typename T>
class SubscriptionClass : public ClassDefinition<T, Subscription<T>> {
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<T>;
using Object = js::Object<T>;
using Value = js::Value<T>;
using Function = js::Function<T>;
using ReturnValue = js::ReturnValue<T>;
using Arguments = js::Arguments<T>;
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<T> const properties = {
{"state", {wrap<get_state>, nullptr}},
{"error", {wrap<get_error>, nullptr}}
};
MethodMap<T> const methods = {
{"unsubscribe", wrap<unsubscribe>},
{"addListener", wrap<add_listener>},
{"removeListener", wrap<remove_listener>},
{"removeAllListeners", wrap<remove_all_listeners>},
};
};
template<typename T>
typename T::Object SubscriptionClass<T>::create_instance(ContextType ctx, partial_sync::Subscription subscription) {
return create_object<T, SubscriptionClass<T>>(ctx, new Subscription<T>(std::move(subscription)));
}
template<typename T>
void SubscriptionClass<T>::get_state(ContextType ctx, ObjectType object, ReturnValue &return_value) {
auto subscription = get_internal<T, SubscriptionClass<T>>(object);
return_value.set(static_cast<int8_t>(subscription->state()));
}
template<typename T>
void SubscriptionClass<T>::get_error(ContextType ctx, ObjectType object, ReturnValue &return_value) {
auto subscription = get_internal<T, SubscriptionClass<T>>(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<typename T>
void SubscriptionClass<T>::unsubscribe(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(0);
auto subscription = get_internal<T, SubscriptionClass<T>>(this_object);
partial_sync::unsubscribe(*subscription);
return_value.set_undefined();
}
template<typename T>
void SubscriptionClass<T>::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(1);
auto subscription = get_internal<T, SubscriptionClass<T>>(this_object);
auto callback = Value::validated_to_function(ctx, args[0]);
Protected<FunctionType> protected_callback(ctx, callback);
Protected<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
auto token = subscription->add_notification_callback([=]() {
HANDLESCOPE
ValueType arguments[2];
arguments[0] = static_cast<ObjectType>(protected_this),
arguments[1] = Value::from_number(ctx, static_cast<double>(subscription->state()));
Function::callback(protected_ctx, protected_callback, protected_this, 2, arguments);
});
subscription->m_notification_tokens.emplace_back(protected_callback, std::move(token));
}
template<typename T>
void SubscriptionClass<T>::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(1);
auto subscription = get_internal<T, SubscriptionClass<T>>(this_object);
auto callback = Value::validated_to_function(ctx, args[0]);
auto protected_function = Protected<FunctionType>(ctx, callback);
auto& tokens = subscription->m_notification_tokens;
auto compare = [&](auto&& token) {
return typename Protected<FunctionType>::Comparator()(token.first, protected_function);
};
tokens.erase(std::remove_if(tokens.begin(), tokens.end(), compare), tokens.end());
}
template<typename T>
void SubscriptionClass<T>::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(0);
auto subscription = get_internal<T, SubscriptionClass<T>>(this_object);
subscription->m_notification_tokens.clear();
}
template<typename T> template<typename T>
class SyncClass : public ClassDefinition<T, void*> { class SyncClass : public ClassDefinition<T, void*> {
using GlobalContextType = typename T::GlobalContext; using GlobalContextType = typename T::GlobalContext;
@ -694,14 +831,52 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
SSLVerifyCallbackSyncThreadFunctor<T> ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)}; SSLVerifyCallbackSyncThreadFunctor<T> ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)};
ssl_verify_callback = std::move(ssl_verify_functor); 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; bool is_partial = false;
ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial"); ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial");
if (!Value::is_undefined(ctx, partial_value)) { if (!Value::is_undefined(ctx, partial_value)) {
is_partial = Value::validated_to_boolean(ctx, partial_value); is_partial = Value::validated_to_boolean(ctx, partial_value);
} }
config.sync_config = std::make_shared<SyncConfig>(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<SyncConfig>(shared_user, std::move(""));
config.sync_config->reference_realm_url = std::move(raw_realm_url);
}
else {
config.sync_config = std::make_shared<SyncConfig>(shared_user, std::move(raw_realm_url));
}
config.sync_config->bind_session_handler = std::move(bind); config.sync_config->bind_session_handler = std::move(bind);
config.sync_config->error_handler = std::move(error_handler); config.sync_config->error_handler = std::move(error_handler);
config.sync_config->client_validate_ssl = client_validate_ssl; config.sync_config->client_validate_ssl = client_validate_ssl;

@ -1 +1 @@
Subproject commit d5fe9a626c0adc0345d128201e0df8bb4004725d Subproject commit f2a536d29de48e34e60799a5bf3f36e13806387e

View File

@ -37,7 +37,8 @@ module.exports = {
} }
else if (type === 'object') { else if (type === 'object') {
for (const key of Object.keys(val1)) { 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') { else if (type === 'list') {

View File

@ -11,7 +11,8 @@ const Realm = require(realmModule);
function createObjects(user) { function createObjects(user) {
const config = { const config = {
sync: { user, sync: {
user: user,
url: `realm://localhost:9080/~/${realmName}`, url: `realm://localhost:9080/~/${realmName}`,
error: err => console.log(err) error: err => console.log(err)
}, },
@ -19,7 +20,6 @@ function createObjects(user) {
}; };
const realm = new Realm(config); const realm = new Realm(config);
realm.write(() => { realm.write(() => {
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
realm.create('Dog', { name: `Lassy ${i}` }); realm.create('Dog', { name: `Lassy ${i}` });

View File

@ -55,7 +55,7 @@ if (global.enableSyncTests) {
// FIXME: Permission tests currently fail in chrome debugging mode. // FIXME: Permission tests currently fail in chrome debugging mode.
if (typeof navigator === 'undefined' || if (typeof navigator === 'undefined' ||
!/Chrome/.test(navigator.userAgent)) { // eslint-disable-line no-undef !/Chrome/.test(navigator.userAgent)) { // eslint-disable-line no-undef
TESTS.PermissionTests = require('./permission-tests'); TESTS.PermissionTests = require('./permission-tests');
} }
} }

View File

@ -83,6 +83,52 @@ module.exports = {
TestCase.assertArraysEqual(names(resultsC), ['JP']); 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() { testMethod: function() {
var realm = new Realm({schema: [schemas.PersonObject]}); var realm = new Realm({schema: [schemas.PersonObject]});

View File

@ -9,6 +9,8 @@ const realmName = process.argv[4];
const realmModule = process.argv[5]; const realmModule = process.argv[5];
const Realm = require(realmModule); 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]); let schemas = require(process.argv[2]);
function createObjects(user) { function createObjects(user) {

View File

@ -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));

View File

@ -16,33 +16,33 @@
// //
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
'use strict'; 'use strict';
var Realm = require('realm'); var Realm = require('realm');
var TestCase = require('./asserts'); var TestCase = require('./asserts');
function uuid() { function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16); return v.toString(16);
}); });
} }
function createUsersWithTestRealms(count) { function createUsersWithTestRealms(count) {
const createUserWithTestRealm = () => { const createUserWithTestRealm = () => {
return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password') return Realm.Sync.User
.then(user => { .register('http://localhost:9080', uuid(), 'password')
new Realm({sync: {user, url: 'realm://localhost:9080/~/test'}}).close(); .then(user => {
return 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) { function wait(t) {
return new Promise(resolve => setTimeout(resolve, t)); return new Promise(resolve => setTimeout(resolve, t));
} }
function repeatUntil(fn, predicate) { function repeatUntil(fn, predicate) {
@ -57,66 +57,179 @@ function repeatUntil(fn, predicate) {
return check; 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 = { module.exports = {
testApplyAndGetGrantedPermissions() { testApplyAndGetGrantedPermissions() {
return createUsersWithTestRealms(1) return createUsersWithTestRealms(1).then(([user]) => {
.then(([user]) => { const path = `/${user.identity}/test`;
return user.applyPermissions({ userId: `${user.identity}` }, `/${user.identity}/test`, 'read') return user
.then(repeatUntil(() => user.getGrantedPermissions('any'), .applyPermissions({userId: `${user.identity}`},
permissions => permissions.length > 1)) `/${user.identity}/test`, 'read')
.then(permissions => { .then(repeatUntil(() => user.getGrantedPermissions('any'),
TestCase.assertEqual(permissions[0].path, `/${user.identity}/test`); permissions => {
TestCase.assertEqual(permissions[0].mayRead, true); let permission = permissionForPath(permissions, path);
TestCase.assertEqual(permissions[0].mayWrite, false); return permission && !permission.mayWrite;
TestCase.assertEqual(permissions[0].mayManage, false); }))
}); .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() { testOfferPermissions() {
return createUsersWithTestRealms(2) return createUsersWithTestRealms(2).then(([user1, user2]) => {
.then(([user1, user2]) => { const path = `/${user1.identity}/test`;
return user1.offerPermissions(`/${user1.identity}/test`, 'read') return user1.offerPermissions(`/${user1.identity}/test`, 'read')
.then(token => user2.acceptPermissionOffer(token)) .then(token => user2.acceptPermissionOffer(token))
.then(realmUrl => { .then(realmUrl => {
TestCase.assertEqual(realmUrl, `/${user1.identity}/test`); TestCase.assertEqual(realmUrl, path);
return realmUrl; return realmUrl;
}) })
.then(repeatUntil(() => user2.getGrantedPermissions('any'), .then(repeatUntil(() => user2.getGrantedPermissions('any'),
permissions => permissions.length > 1)) permissions => permissions.length > 2
.then(permissions => { && permissionForPath(permissions, path)))
TestCase.assertEqual(permissions[2].path, `/${user1.identity}/test`); .then(permissions => {
TestCase.assertEqual(permissions[2].mayRead, true); let permission = permissionForPath(permissions, path)
TestCase.assertEqual(permissions[2].mayWrite, false); TestCase.assertDefined(permission);
TestCase.assertEqual(permissions[2].mayManage, false); TestCase.assertEqual(permission.mayRead, true);
}); TestCase.assertEqual(permission.mayWrite, false);
TestCase.assertEqual(permission.mayManage, false);
});
}); });
}, },
testInvalidatePermissionOffer() { testInvalidatePermissionOffer() {
let user1, user2, token; let user1, user2, token;
return createUsersWithTestRealms(2) return createUsersWithTestRealms(2)
.then(users => { .then(users => {
user1 = users[0]; user1 = users[0];
user2 = users[1]; user2 = users[1];
return user1.offerPermissions(`/${user1.identity}/test`, 'read'); return user1.offerPermissions(`/${user1.identity}/test`, 'read');
}) })
.then(t => { token = t; return user1.invalidatePermissionOffer(token); }) .then(t => {
// Since we don't yet support notification when the invalidation has gone through, token = t;
// wait for a bit and hope the server is done processing. return user1.invalidatePermissionOffer(token);
.then(wait(100)) })
.then(() => user2.acceptPermissionOffer(token)) // Since we don't yet support notification when the invalidation has
// We want the call to fail, i.e. the catch() below should be called. // gone through, wait for a bit and hope the server is done
.then(() => { throw new Error("User was able to accept an invalid permission offer token"); }) // processing.
.catch(error => { .then(() => wait(100))
try { .then(() => user2.acceptPermissionOffer(token))
TestCase.assertEqual(error.message, 'The permission offer is expired.'); // We want the call to fail, i.e. the catch() below should be
TestCase.assertEqual(error.statusCode, 701); // called.
} .then(() => {
catch (e) { throw new Error("User was able to accept an invalid permission offer token");
throw new Error(e); })
} .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();
});
}
}

View File

@ -154,5 +154,8 @@ module.exports = {
}, },
testOptionalQueries: function() { testOptionalQueries: function() {
runQuerySuite(testCases.optionalTests); runQuerySuite(testCases.optionalTests);
},
testOrderingQueries: function() {
runQuerySuite(testCases.orderingTests);
} }
}; };

View File

@ -1,7 +1,7 @@
{ {
"dateTests" : { "dateTests" : {
"schema" : [{ "schema" : [{
"name": "DateObject", "name": "DateObject",
"properties": [{ "name": "date", "type": "date" }] "properties": [{ "name": "date", "type": "date" }]
}], }],
@ -25,7 +25,7 @@
}, },
"boolTests" : { "boolTests" : {
"schema" : [{ "schema" : [{
"name": "BoolObject", "name": "BoolObject",
"properties": [{ "name": "boolCol", "type": "bool" }] "properties": [{ "name": "boolCol", "type": "bool" }]
}], }],
@ -57,11 +57,11 @@
["QueryThrows", "BoolObject", "boolCol BEGINSWITH true"], ["QueryThrows", "BoolObject", "boolCol BEGINSWITH true"],
["QueryThrows", "BoolObject", "boolCol CONTAINS true"], ["QueryThrows", "BoolObject", "boolCol CONTAINS true"],
["QueryThrows", "BoolObject", "boolCol ENDSWITH true"] ["QueryThrows", "BoolObject", "boolCol ENDSWITH true"]
] ]
}, },
"intTests" : { "intTests" : {
"schema" : [{ "schema" : [{
"name": "IntObject", "name": "IntObject",
"properties": [{ "name": "intCol", "type": "int" }] "properties": [{ "name": "intCol", "type": "int" }]
}], }],
@ -92,7 +92,7 @@
}, },
"floatTests" : { "floatTests" : {
"schema" : [{ "schema" : [{
"name": "FloatObject", "name": "FloatObject",
"properties": [{ "name": "floatCol", "type": "float" }] "properties": [{ "name": "floatCol", "type": "float" }]
}], }],
@ -126,7 +126,7 @@
}, },
"doubleTests" : { "doubleTests" : {
"schema" : [{ "schema" : [{
"name": "DoubleObject", "name": "DoubleObject",
"properties": [{ "name": "doubleCol", "type": "double" }] "properties": [{ "name": "doubleCol", "type": "double" }]
}], }],
@ -157,7 +157,7 @@
}, },
"stringTests" : { "stringTests" : {
"schema" : [{ "schema" : [{
"name": "StringObject", "name": "StringObject",
"properties": [{ "name": "stringCol", "type": "string" }] "properties": [{ "name": "stringCol", "type": "string" }]
}], }],
@ -205,7 +205,7 @@
}, },
"binaryTests" : { "binaryTests" : {
"schema" : [{ "schema" : [{
"name": "BinaryObject", "name": "BinaryObject",
"properties": [{ "name": "binaryCol", "type": "data" }] "properties": [{ "name": "binaryCol", "type": "data" }]
}], }],
@ -256,14 +256,14 @@
"compoundTests" : { "compoundTests" : {
"schema" : [ "schema" : [
{ "name": "IntObject", { "name": "IntObject",
"properties": [{ "name": "intCol", "type": "int" }], "properties": [{ "name": "intCol", "type": "int" }],
"primaryKey" : "intCol" } "primaryKey" : "intCol" }
], ],
"objects": [ "objects": [
{ "type": "IntObject", "value": [0] }, { "type": "IntObject", "value": [0] },
{ "type": "IntObject", "value": [1] }, { "type": "IntObject", "value": [1] },
{ "type": "IntObject", "value": [2] }, { "type": "IntObject", "value": [2] },
{ "type": "IntObject", "value": [3] } { "type": "IntObject", "value": [3] }
], ],
"tests": [ "tests": [
@ -281,14 +281,14 @@
["ObjectSet", [0, 1, 2],"IntObject", "intCol == 0 || intCol == 1 || intCol <= 2"], ["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 >= 1 || intCol == 0"],
["ObjectSet", [0, 1], "IntObject", "intCol == 1 || intCol == 0 && intCol <= 0 && 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" : { "keyPathTests" : {
"schema" : [ "schema" : [
{ {
"name": "BasicTypesObject", "name": "BasicTypesObject",
"properties": [ "properties": [
{ "name": "intCol", "type": "int" }, { "name": "intCol", "type": "int" },
{ "name": "floatCol", "type": "float" }, { "name": "floatCol", "type": "float" },
@ -296,7 +296,7 @@
{ "name": "stringCol", "type": "string" }, { "name": "stringCol", "type": "string" },
{ "name": "dateCol", "type": "date?" } { "name": "dateCol", "type": "date?" }
] ]
}, },
{ {
"name": "LinkTypesObject", "name": "LinkTypesObject",
"primaryKey": "primaryKey", "primaryKey": "primaryKey",
@ -326,8 +326,8 @@
"optionalTests" : { "optionalTests" : {
"schema" : [ "schema" : [
{ {
"name": "OptionalTypesObject", "name": "OptionalTypesObject",
"primaryKey": "primaryKey", "primaryKey": "primaryKey",
"properties": [ "properties": [
{ "name": "primaryKey", "type": "int" }, { "name": "primaryKey", "type": "int" },
@ -338,7 +338,7 @@
{ "name": "dateCol", "type": "date", "optional": true }, { "name": "dateCol", "type": "date", "optional": true },
{ "name": "dataCol", "type": "data", "optional": true } { "name": "dataCol", "type": "data", "optional": true }
] ]
}, },
{ {
"name": "LinkTypesObject", "name": "LinkTypesObject",
"primaryKey": "primaryKey", "primaryKey": "primaryKey",
@ -375,9 +375,35 @@
["ObjectSet", [1], "LinkTypesObject", "basicLink.stringCol == null"], ["ObjectSet", [1], "LinkTypesObject", "basicLink.stringCol == null"],
["ObjectSet", [0], "LinkTypesObject", "basicLink.stringCol != null"], ["ObjectSet", [0], "LinkTypesObject", "basicLink.stringCol != null"],
["ObjectSet", [1], "LinkTypesObject", "basicLink.dateCol == null"], ["ObjectSet", [1], "LinkTypesObject", "basicLink.dateCol == null"],
["ObjectSet", [0], "LinkTypesObject", "basicLink.dateCol != null"], ["ObjectSet", [0], "LinkTypesObject", "basicLink.dateCol != null"]
["QueryThrows", "LinkTypesObject", "basicLink.dataCol == 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)"]
] ]
} }
} }

View File

@ -1200,8 +1200,54 @@ module.exports = {
testDisableFileFormatUpgrade: function() { testDisableFileFormatUpgrade: function() {
Realm.copyBundledRealmFiles(); Realm.copyBundledRealmFiles();
TestCase.assertThrowsContaining(() => { TestCase.assertThrowsContaining(() => {
new Realm({ path: 'dates-v3.realm', disableFormatUpgrade: true } ); new Realm({ path: 'dates-v3.realm', disableFormatUpgrade: true } );
}, 'The Realm file format must be allowed to be upgraded in order to proceed.'); }, '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();
} }
}; };

View File

@ -307,3 +307,20 @@ exports.MultiListObject = {
'list2': 'string[]' '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[]',
}
};

View File

@ -61,27 +61,23 @@ function copyFileToTempDir(filename) {
return tmpFile.name; return tmpFile.name;
} }
function runOutOfProcess(nodeJsFilePath) { function runOutOfProcess() {
var nodeArgs = Array.prototype.slice.call(arguments); const args = Array.prototype.slice.call(arguments);
let tmpDir = tmp.dirSync(); let tmpDir = tmp.dirSync();
let content = fs.readFileSync(nodeJsFilePath, 'utf8'); console.log(`runOutOfProcess : ${args.join(' ')}`);
let tmpFile = tmp.fileSync({ dir: tmpDir.name });
fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' });
nodeArgs[0] = tmpFile.name;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
console.log('runOutOfProcess command\n node ' + nodeArgs.join(" ")); execFile(process.execPath, args, {cwd: tmpDir.name}, (error, stdout, stderr) => {
const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => {
if (error) { if (error) {
console.error("runOutOfProcess failed\n" + error); console.error("runOutOfProcess failed\n", error, stdout, stderr);
reject(new Error(`Running ${nodeJsFilePath} failed. error: ${error}`)); reject(new Error(`Running ${args[0]} failed. error: ${error}`));
return; return;
} }
console.log('runOutOfProcess success\n' + stdout); console.log('runOutOfProcess success\n' + stdout);
resolve(); resolve();
}); });
} }
catch (e) { catch (e) {
reject(e); reject(e);
} }
@ -275,43 +271,40 @@ module.exports = {
const expectedObjectsCount = 3; const expectedObjectsCount = 3;
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { const accessTokenRefreshed = this;
const accessTokenRefreshed = this; let successCounter = 0;
let successCounter = 0;
let config = { let config = {
sync: { user, url: `realm://localhost:9080/~/${realmName}` } 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) => { let firstDog = realm.objects('Dog')[0];
try { TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema");
if (error) { TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value");
reject(error); TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value");
}
let actualObjectsCount = realm.objects('Dog').length; const session = realm.syncSession;
TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Synced realm does not contain the expected objects count"); TestCase.assertInstanceOf(session, Realm.Sync.Session);
TestCase.assertEqual(session.user.identity, user.identity);
let firstDog = realm.objects('Dog')[0]; TestCase.assertEqual(session.config.url, config.sync.url);
TestCase.assertTrue(({}).hasOwnProperty.call(firstDog, 'name'), "Synced realm does not have an inffered schema"); TestCase.assertEqual(session.config.user.identity, config.sync.user.identity);
TestCase.assertTrue(firstDog.name, "Synced realm object's property should have a value"); TestCase.assertEqual(session.state, 'active');
TestCase.assertTrue(firstDog.name.indexOf('Lassy') !== -1, "Synced realm object's property should contain the actual written value"); resolve();
}
catch (e) {
const session = realm.syncSession; reject(e);
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 username = uuid();
const expectedObjectsCount = 3; const expectedObjectsCount = 3;
const accessTokenRefreshed = this;
let successCounter = 0;
return new Promise((resolve, reject) => { let config = {
const accessTokenRefreshed = this; schema: [{ name: 'Dog', properties: { name: 'string' } }],
let successCounter = 0; };
let config = { return Realm.open(config).then(realm => {
schema: [{ name: 'Dog', properties: { name: 'string' } }], realm.write(() => {
}; for (let i = 1; i <= 3; i++) {
realm.create('Dog', { name: `Lassy ${i}` });
}
});
Realm.open(config).then(realm => { let actualObjectsCount = realm.objects('Dog').length;
realm.write(() => { TestCase.assertEqual(actualObjectsCount, expectedObjectsCount, "Local realm does not contain the expected objects count");
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));
}); });
}, },
@ -412,37 +401,33 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/nested-list-helper.js', __dirname + '/schemas.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/nested-list-helper.js', __dirname + '/schemas.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { let config = {
let config = { schema: [schemas.ParentObject, schemas.NameObject],
schema: [schemas.ParentObject, schemas.NameObject], sync: { user, url: `realm://localhost:9080/~/${realmName}` }
sync: { user, url: `realm://localhost:9080/~/${realmName}` } };
}; return Realm.open(config)
Realm.open(config).then(realm => { }).then(realm => {
let objects = realm.objects('ParentObject'); let objects = realm.objects('ParentObject');
let json = JSON.stringify(objects); 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(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.length, 2);
TestCase.assertEqual(objects[0].name.length, 2); TestCase.assertEqual(objects[0].name.length, 2);
TestCase.assertEqual(objects[0].name[0].given.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].prefix.length, 0);
TestCase.assertEqual(objects[0].name[0].given[0], 'Hans'); TestCase.assertEqual(objects[0].name[0].given[0], 'Hans');
TestCase.assertEqual(objects[0].name[0].given[1], 'Jørgen') 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.length, 1);
TestCase.assertEqual(objects[0].name[1].given[0], 'Ib'); TestCase.assertEqual(objects[0].name[1].given[0], 'Ib');
TestCase.assertEqual(objects[0].name[1].prefix.length, 0); TestCase.assertEqual(objects[0].name[1].prefix.length, 0);
TestCase.assertEqual(objects[1].name.length, 1); TestCase.assertEqual(objects[1].name.length, 1);
TestCase.assertEqual(objects[1].name[0].given.length, 2); TestCase.assertEqual(objects[1].name[0].given.length, 2);
TestCase.assertEqual(objects[1].name[0].prefix.length, 0); 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[0], 'Gurli');
TestCase.assertEqual(objects[1].name[0].given[1], 'Margrete'); TestCase.assertEqual(objects[1].name[0].given[1], 'Margrete');
resolve();
}).catch(() => reject());
});
});
}); });
}, },
@ -567,7 +552,7 @@ module.exports = {
}); });
}, },
testProgressNotificationsForRealmConstructor() { /* testProgressNotificationsForRealmConstructor() {
if (!isNodeProccess) { if (!isNodeProccess) {
return; return;
} }
@ -599,7 +584,7 @@ module.exports = {
}); });
}); });
}); });
}, },*/
testProgressNotificationsUnregisterForRealmConstructor() { testProgressNotificationsUnregisterForRealmConstructor() {
if (!isNodeProccess) { if (!isNodeProccess) {
@ -610,60 +595,59 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { let config = {
let config = { sync: {
sync: { user,
user, url: `realm://localhost:9080/~/${realmName}`
url: `realm://localhost:9080/~/${realmName}` },
}, schema: [{ name: 'Dog', properties: { name: 'string' } }],
schema: [{ name: 'Dog', properties: { name: 'string' } }], };
};
let realm = new Realm(config); let realm = new Realm(config);
let unregisterFunc; let unregisterFunc;
let writeDataFunc = () => { let writeDataFunc = () => {
realm.write(() => { realm.write(() => {
for (let i = 1; i <= 3; i++) { for (let i = 1; i <= 3; i++) {
realm.create('Dog', { name: `Lassy ${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; syncFinished = transferred === total;
let failOnCall = false;
const progressCallback = (transferred, total) => {
if (failOnCall) {
reject(new Error("Progress callback should not be called after removeProgressNotification"));
}
syncFinished = transferred === total; //unregister and write some new data.
if (syncFinished) {
failOnCall = true;
unregisterFunc();
//unregister and write some new data. //use second callback to wait for sync finished
if (syncFinished) { realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => {
failOnCall = true; if (transferred === transferable) {
unregisterFunc(); resolve();
}
});
writeDataFunc();
}
};
//use second callback to wait for sync finished realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback);
realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (transferred, transferable) => {
if (transferred === transferable) {
resolve();
}
});
writeDataFunc();
}
};
realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); unregisterFunc = () => {
realm.syncSession.removeProgressNotification(progressCallback);
};
unregisterFunc = () => { writeDataFunc();
realm.syncSession.removeProgressNotification(progressCallback);
};
writeDataFunc();
});
}); });
}); });
}, },
@ -675,36 +659,24 @@ module.exports = {
const username = uuid(); const username = uuid();
const realmName = uuid(); const realmName = uuid();
let progressCalled = false;
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH)
.then(() => { .then(() => Realm.Sync.User.login('http://localhost:9080', username, 'password'))
return Realm.Sync.User.login('http://localhost:9080', username, 'password').then(user => { .then(user => {
return new Promise((resolve, reject) => { let config = {
let config = { sync: {
sync: { user,
user, url: `realm://localhost:9080/~/${realmName}`
url: `realm://localhost:9080/~/${realmName}` },
}, schema: [{ name: 'Dog', properties: { name: 'string' } }],
schema: [{ name: 'Dog', properties: { name: 'string' } }], };
};
let progressCalled = false; return Promise.race([
Realm.open(config) Realm.open(config).progress((transferred, total) => { progressCalled = true; }),
.progress((transferred, total) => { new Promise((_, reject) => setTimeout(() => reject("Progress Notifications API failed to call progress callback for Realm constructor"), 5000))
progressCalled = true; ]);
}) }).then(() => TestCase.assertTrue(progressCalled));
.then(() => {
TestCase.assertTrue(progressCalled);
resolve();
})
.catch((e) => reject(e));
setTimeout(function() {
reject("Progress Notifications API failed to call progress callback for Realm constructor");
}, 5000);
});
});
});
}, },
testProgressNotificationsForRealmOpenAsync() { testProgressNotificationsForRealmOpenAsync() {
@ -716,73 +688,182 @@ module.exports = {
const realmName = uuid(); const realmName = uuid();
return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) 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(() => { .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) => { return new Promise((resolve, reject) => {
let config = { let config = Realm.automaticSyncConfiguration();
sync: { config.schema = [{ name: 'Dog', properties: { name: 'string' } }];
user, Realm.deleteFile(config);
url: `realm://localhost:9080/~/${realmName}`
},
schema: [{ name: 'Dog', properties: { name: 'string' } }],
};
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, var results1 = realm.objects('Dog').filtered("name == 'Lassy 1'");
(error, realm) => { var results2 = realm.objects('Dog').filtered("name == 'Lassy 2'");
if (error) {
reject(error); var subscription1 = results1.subscribe();
return; 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); subscription2.addListener((subscription, state) => {
resolve(); if (state === Realm.Sync.SubscriptionState.Complete) {
}, results2.addListener((collection, changeset) => {
(transferred, total) => { TestCase.assertEqual(collection.length, 1);
progressCalled = true; 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() { setTimeout(() => {
reject("Progress Notifications API failed to call progress callback for Realm constructor"); reject("listeners never called");
}, 5000); }, 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() { testClientReset() {
// FIXME: try to enable for React Native // FIXME: try to enable for React Native