diff --git a/CHANGELOG.md b/CHANGELOG.md index ee3c008c..bee1733c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +X.Y.Z Release notes +============================================================= +### Breaking changes +* None + +### Enhancements +* Improve performance of the RPC worker for chrome debugging. +* Added `Realm.deleteFile` for deleting a Realm (#363). + +### Bug fixes +* Adding missing TypeScript declation (#1283). + +1.11.1 Release notes (2017-9-1) +============================================================= +### Breaking changes +* None + +### Enhancements +* None + +### Bug fixes +* Fix accessToken. + +1.11.0 Release notes (2017-8-31) +============================================================= +### Breaking changes +* None + +### Enhancements +* Added methods `Realm.beginTransaction()`, `Realm.commitTransaction()`, `Realm.cancelTransaction()` to manually control write transactions. +* Added property `Realm.isInTransaction` which indicates if write transaction is in progress. +* Added `shouldCompactOnLaunch` to configuration (#507). +* Added `Realm.compact()` for manually compacting Realm files. +* Added various methods for permission management (#1204). + +### Bug fixes +* None + + 1.10.3 Release notes (2017-8-16) ============================================================= ### Breaking changes @@ -7,7 +46,7 @@ * None ### Bug fixes -* none +* None 1.10.2 Release notes (2017-8-16) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f49b825e..0b18df5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,3 +38,53 @@ Below are some guidelines about the format of the commit message itself: Realm welcomes all contributions! The only requirement we have is that, like many other projects, we need to have a [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) (CLA) in place before we can accept any external code. Our own CLA is a modified version of the Apache Software Foundation’s CLA. [Please submit your CLA electronically using our Google form](https://docs.google.com/forms/d/1bVp-Wp5nmNFz9Nx-ngTmYBVWVdwTyKj4T0WtfVm0Ozs/viewform?fbzx=4154977190905366979) so we can accept your submissions. The GitHub username you file there will need to match that of your Pull Requests. If you have any questions or cannot file the CLA electronically, you can email . + +### Guidelines + +Adding new functionality to Realm JavaScript requires that you modify a few places in the repository. As an example, consider adding a function `crashOnStart()` to the class `Realm`. The subsections below guides you through where and what to add. + +#### Add the function + +First, add a prototype of function to `src/js_realm.hpp`; look for a section marked by the comment `// method`. The prototype looks like: + +``` +static void crashOnStart(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); +``` + +You have to implement the function. Find a place in `src/js_realm.hpp` to add it (maybe at the end): + +``` +template +void RealmClass::crashOnStart(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); // <- the function doesn't take any arguments + + SharedRealm realm = *get_internal>(this_object); // <- unwrap the Realm instance + + // add the actual implement ... +} +``` + +Testing is important, and in `tests/js/realm-tests.js` you can add the tests you need. + +Note: If your new API and/or test cases are not getting picked up when running the Android or iOS tests, remove the corresponding installed package from react-test-app and try again. + +``` +rm -rf tests/react-test-app/node_modules/realm +rm -rf tests/react-test-app/node_modules/realm-tests +``` + +#### Wrap the function + +In order to call the C++ implementation, the JavaScript engine has to know about the function. You must simply add it to the map of methods/functions. Find `MethodMap const methods` declaration in `src/js_realm.hpp` and add your function to it: + +``` +{"crashOnStart", wrap}, +``` + +#### The final details + +To finish adding your new function, you will have to add your function a few places: + +* In `lib/index.d.ts` you add the TypeScript declaration +* Documentation is added in `docs/realm.js` +* Add your function to `lib/browser/index.js` in order to enable it in the Chrome Debugger \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 9837b2fa..9848c78f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,8 +68,8 @@ stage('build') { macos_node_release: doMacBuild('node Release'), //macos_realmjs_debug: doMacBuild('realmjs Debug'), //macos_realmjs_release: doMacBuild('realmjs Release'), - macos_react_tests_debug: doReactBuild('react-tests Debug'), - macos_react_tests_release: doReactBuild('react-tests Release'), + macos_react_tests_debug: doMacBuild('react-tests Debug'), + macos_react_tests_release: doMacBuild('react-tests Release'), macos_react_example_debug: doMacBuild('react-example Debug'), macos_react_example_release: doMacBuild('react-example Release'), //android_react_tests: doAndroidBuild('react-tests-android', { @@ -188,23 +188,9 @@ def doMacBuild(target, postStep = null) { } } -def doReactBuild(target, postStep = null) { - return { - node('xamarin-mac') { - try { - lock("${env.NODE_NAME} iOS Simulator") { - doInside("./scripts/test.sh", target, postStep) - } - } finally { - deleteDir() - } - } - } -} - def doWindowsBuild() { return { - node('windows') { + node('windows && nodejs') { unstash 'source' try { bat 'npm install --build-from-source=realm' diff --git a/README.md b/README.md index f8700a25..94017b98 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,17 @@ The API reference is located at [realm.io/docs/javscript/latest/api](https://rea - **Need help with your code?**: Look for previous questions on the [#realm tag](https://stackoverflow.com/questions/tagged/realm?sort=newest) — or [ask a new question](https://stackoverflow.com/questions/ask?tags=realm). We actively monitor and answer questions on SO! - **Have a bug to report?** [Open an issue](https://github.com/realm/realm-js/issues/new). If possible, include the version of Realm, a full log, the Realm file, and a project that shows the issue. - **Have a feature request?** [Open an issue](https://github.com/realm/realm-js/issues/new). Tell us what the feature should do, and why you want the feature. -- Sign up for our [**Community Newsletter**](https://www2.realm.io/l/210132/2016-12-05/fy9m) to get regular tips, learn about other use-cases and get alerted of blog posts and tutorials about Realm. +- Sign up for our [**Community Newsletter**](https://go.pardot.com/l/210132/2017-04-26/3j74l) to get regular tips, learn about other use-cases and get alerted of blog posts and tutorials about Realm. ## Building Realm In case you don't want to use the precompiled version on npm, you can build Realm yourself from source. You’ll need an Internet connection the first time you build in order to download the core library. Prerequisites: -- Node 4.0+ +- Node: 4.0 <= version < 7.0 - Xcode 7.2+ - Android SDK 23+ -- Android NDK 10e+ +- Android NDK 10e First clone this repository: diff --git a/docs/realm.js b/docs/realm.js index 0d7b5fca..05d5ead5 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -64,6 +64,14 @@ class Realm { */ get schemaVersion() {} + /** + * Indicates if this Realm is in a write transaction. + * @type {boolean} + * @readonly + * @since 1.10.3 + */ + get isInTransaction() {} + /** * Gets the sync session if this is a synced Realm * @type {Session} @@ -182,6 +190,40 @@ class Realm { * @param {function()} callback */ write(callback) {} + + /** + * Initiate a write transaction. + * @throws {Error} When already in write transaction + */ + beginTransaction() {} + + /** + * Commit a write transaction. + */ + commitTransaction() {} + + /** + * Cancel a write transaction. + */ + cancelTransaction() {} + + /* + * Replaces all string columns in this Realm with a string enumeration column and compacts the + * database file. + * + * Cannot be called from a write transaction. + * + * Compaction will not occur if other `Realm` instances exist. + * + * While compaction is in progress, attempts by other threads or processes to open the database will + * wait. + * + * Be warned that resource requirements for compaction is proportional to the amount of live data in + * the database. Compaction works by writing the database contents to a temporary database file and + * then replacing the database with the temporary one. + * @returns {true} if compaction succeeds. + */ + compact() {} } /** @@ -195,6 +237,13 @@ class Realm { */ Realm.schemaVersion = function(path, encryptionKey) {}; +/** + * Delete the Realm file for the given configuration. + * @param {Realm~Configuration} config + * @throws {Error} If anything in the provided `config` is invalid. + */ +Realm.deleteFile = function(config) {}; + /** * The default path where to create and access the Realm file. * @type {string} @@ -213,6 +262,13 @@ Realm.defaultPath; * This function takes two arguments: * - `oldRealm` - The Realm before migration is performed. * - `newRealm` - The Realm that uses the latest `schema`, which should be modified as necessary. + * @property {callback(number, number)} [shouldCompactOnLaunch] - The function called when opening + * a Realm for the first time during the life of a process to determine if it should be compacted + * before being returned to the user. The function takes two arguments: + * - `totalSize` - The total file size (data + free space) + * - `unusedSize` - The total bytes used by data in the file. + * It returns `true` to indicate that an attempt to compact the file should be made. The compaction + * will be skipped if another process is accessing it. * @property {string} [path={@link Realm.defaultPath}] - The path to the file where the * Realm database should be stored. * @property {boolean} [readOnly=false] - Specifies if this Realm should be opened as read-only. diff --git a/docs/sync.js b/docs/sync.js index 770d8583..b8137ef0 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -242,6 +242,60 @@ class User { * } */ retrieveAccount(provider, username) {} + + /** + * Asynchronously retrieves all permissions associated with the user calling this method. + * @param {string} recipient the optional recipient of the permission. Can be either + * 'any' which is the default, or 'currentUser' or 'otherUser' if you want only permissions + * belonging to the user or *not* belonging to the user. + * @returns {Results} a queryable collection of permission objects that provides detailed + * information regarding the granted access. + * The collection is a live query similar to what you would get by callig Realm.objects, + * so the same features apply - you can listen for notifications or filter it. + */ + getGrantedPermissions(recipient) { } + + /** + * Changes the permissions of a Realm. + * @param {object} condition - A condition that will be used to match existing users against. + * This should be an object, containing either the key 'userId', or 'metadataKey' and 'metadataValue'. + * @param {string} realmUrl - The path to the Realm that you want to apply permissions to. + * @param {string} accessLevel - The access level you want to set: 'none', 'read', 'write' or 'admin'. + * @returns {Promise} a Promise that, upon completion, indicates that the permissions have been + * successfully applied by the server. It will be resolved with the + * {@link PermissionChange PermissionChange} object that refers to the applied permission. + */ + applyPermissions(condition, realmUrl, accessLevel) { } + + /** + * Generates a token that can be used for sharing a Realm. + * @param {string} realmUrl - The Realm URL whose permissions settings should be changed. Use * to change + * the permissions of all Realms managed by this user. + * @param {string} accessLevel - The access level to grant matching users. Note that the access level + * setting is additive, i.e. you cannot revoke permissions for users who previously had a higher access level. + * Can be 'read', 'write' or 'admin'. + * @param {Date} [expiresAt] - Optional expiration date of the offer. If set to null, the offer doesn't expire. + * @returns {string} - A token that can be shared with another user, e.g. via email or message and then consumed by + * User.acceptPermissionOffer to obtain permissions to a Realm. + */ + offerPermissions(realmUrl, accessLevel, expiresAt) { } + + /** + * Consumes a token generated by {@link Realm#Sync#User#offerPermissions offerPermissions} to obtain permissions to a shared Realm. + * @param {string} token - The token, generated by User.offerPermissions + * @returns {string} The url of the Realm that the token has granted permissions to. + */ + acceptPermissionOffer(token) { } + + /** + * Invalidates a permission offer. + * Invalidating an offer prevents new users from consuming its token. It doesn't revoke any permissions that have + * already been granted. + * @param {string|PermissionOffer} permissionOfferOrToken - Either the token or the entire + * {@link PermissionOffer PermissionOffer} object that was generated with + * {@link Realm#Sync#User#offerPermissions offerPermissions}. + */ + invalidatePermissionOffer(permissionOfferOrToken) { } } /** diff --git a/lib/browser/index.js b/lib/browser/index.js index 81b2a804..8caff788 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -58,6 +58,7 @@ function setupRealm(realm, realmId) { 'schema', 'schemaVersion', 'syncSession', + 'isInTransaction', ].forEach((name) => { Object.defineProperty(realm, name, {get: util.getterForProperty(name)}); }); @@ -132,6 +133,10 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ 'delete', 'deleteAll', 'write', + 'compact', + 'beginTransaction', + 'commitTransaction', + 'cancelTransaction', ], true); const Sync = { @@ -164,6 +169,11 @@ Object.defineProperties(Realm, { return rpc.callMethod(undefined, Realm[keys.id], 'schemaVersion', Array.from(arguments)); } }, + deleteFile: { + value: function(config) { + return rpc.callMethod(undefined, Realm[keys.id], 'deleteFile', Array.from(arguments)); + } + }, copyBundledRealmFiles: { value: function() { return rpc.callMethod(undefined, Realm[keys.id], 'copyBundledRealmFiles', []); diff --git a/lib/extensions.js b/lib/extensions.js index f2f034c5..7b086554 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -107,7 +107,7 @@ module.exports = function(realmConstructor) { if (realmConstructor.Sync._setFeatureToken) { realmConstructor.Sync.setFeatureToken = function(featureToken) { - if (typeof featureToken !== 'string' || !featureToken instanceof String) { + if (typeof featureToken !== 'string' && !(featureToken instanceof String)) { throw new Error("featureToken should be a string"); } diff --git a/lib/index.d.ts b/lib/index.d.ts index 196b83d5..f1f2039a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -77,6 +77,7 @@ declare namespace Realm { interface Configuration { encryptionKey?: ArrayBuffer | ArrayBufferView | Int8Array; migration?: (oldRealm: Realm, newRealm: Realm) => void; + shouldCompactOnLaunch?: (totalBytes: number, usedBytes: number) => boolean; path?: string; readOnly?: boolean; schema?: ObjectClass[] | ObjectSchema[]; @@ -270,6 +271,58 @@ declare namespace Realm.Sync { logout(): void; openManagementRealm(): Realm; retrieveAccount(provider: string, username: string): Promise; + + getGrantedPermissions(recipient: 'any' | 'currentUser' | 'otherUser'): Results; + applyPermissions(condition: PermissionCondition, realmUrl: string, accessLevel: AccessLevel): Promise; + offerPermissions(realmUrl: string, accessLevel: AccessLevel, expiresAt?: Date): Promise; + acceptPermissionOffer(token: string): Promise + invalidatePermissionOffer(permissionOfferOrToken: PermissionOffer | string): Promise; + } + + type PermissionCondition = { + userId: string | + { metadataKey: string, metadataValue: string } + }; + + type AccessLevel = 'none' | 'read' | 'write' | 'admin'; + + class Permission { + readonly id: string; + readonly updatedAt: Date; + readonly userId: string; + readonly path: string; + readonly mayRead?: boolean; + readonly mayWrite?: boolean; + readonly mayManage?: boolean; + } + + class PermissionChange { + id: string; + createdAt: Date; + updatedAt: Date; + statusCode?: number; + statusMessage?: string; + userId: string; + metadataKey?: string; + metadataValue?: string; + realmUrl: string; + mayRead?: boolean; + mayWrite?: boolean; + mayManage?: boolean; + } + + class PermissionOffer { + id: string; + createdAt: Date; + updatedAt: Date; + statusCode?: number; + statusMessage?: string; + token?: string; + realmUrl: string; + mayRead?: boolean; + mayWrite?: boolean; + mayManage?: boolean; + expiresAt?: Date; } interface SyncConfiguration { @@ -313,7 +366,7 @@ declare namespace Realm.Sync { function addListener(serverURL: string, adminUser: Realm.Sync.User, regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): void; function removeAllListeners(name?: string): void; function removeListener(regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): void; - function setLogLevel(logLevel: 'error' | 'info' | 'debug'): void; + function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void; function setAccessToken(accessToken: string): void; type Instruction = { @@ -357,6 +410,7 @@ declare class Realm { readonly readOnly: boolean; readonly schema: Realm.ObjectSchema[]; readonly schemaVersion: number; + readonly isInTransaction: boolean; readonly syncSession: Realm.Sync.Session | null; @@ -380,6 +434,12 @@ declare class Realm { */ static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void): void + /** + * Delete the Realm file for the given configuration. + * @param {Configuration} config + */ + static deleteFile(config: Realm.Configuration): void + /** * @param {Realm.Configuration} config? */ @@ -452,6 +512,26 @@ declare class Realm { * @returns void */ write(callback: () => void): void; + + /** + * @returns void + */ + beginTransaction(): void; + + /** + * @returns void + */ + commitTransaction(): void; + + /** + * @returns void + */ + cancelTransaction(): void; + + /** + * @returns boolean + */ + compact(): boolean; } declare module 'realm' { diff --git a/lib/management-schema.js b/lib/management-schema.js index d5975645..3d14b80e 100644 --- a/lib/management-schema.js +++ b/lib/management-schema.js @@ -28,6 +28,8 @@ module.exports = [ statusCode: { type: 'int', optional: true }, statusMessage: { type: 'string', optional: true }, userId: { type: 'string' }, + metadataKey: { type: 'string', optional: true }, + metadataValue: { type: 'string', optional: true }, realmUrl: { type: 'string' }, mayRead: { type: 'bool', optional: true }, mayWrite: { type: 'bool', optional: true }, diff --git a/lib/permission-api.js b/lib/permission-api.js new file mode 100644 index 00000000..4ff5b843 --- /dev/null +++ b/lib/permission-api.js @@ -0,0 +1,274 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2017 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'; + +const url_parse = require('url-parse'); +const managementSchema = require('./management-schema'); + +function generateUniqueId() { + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + return uuid; +} + +const permissionSchema = [ + { + name: 'Permission', + properties: { + id: { type: 'string' }, + updatedAt: { type: 'date' }, + userId: { type: 'string' }, + path: { type: 'string' }, + mayRead: { type: 'bool' }, + mayWrite: { type: 'bool' }, + mayManage: { type: 'bool' }, + } + } +]; + +// Symbols are not supported on RN yet, so we use this for now: +const specialPurposeRealmsKey = '_specialPurposeRealms'; + +function getSpecialPurposeRealm(user, realmName, schema) { + if (!user.hasOwnProperty(specialPurposeRealmsKey)) { + user[specialPurposeRealmsKey] = {}; + } + + if (user[specialPurposeRealmsKey].hasOwnProperty(realmName)) { + return Promise.resolve(user[specialPurposeRealmsKey][realmName]); + } + + const url = url_parse(user.server); + if (url.protocol === 'http:') { + url.set('protocol', 'realm:'); + } else if (url.protocol === 'https:') { + url.set('protocol', 'realms:'); + } else { + throw new Error(`Unexpected user auth url: ${user.server}`); + } + + url.set('pathname', `/~/${realmName}`); + + const config = { + schema: schema, + sync: { + user, + url: url.href + } + }; + + const _Realm = user.constructor._realmConstructor; + + return new Promise((resolve, reject) => { + _Realm._waitForDownload(config, (error) => { + + // FIXME: I don't understand why, but removing the following setTimeout causes the subsequent + // setTimeout call (when resolving the promise) to hang on RN iOS. + // This might be related to our general makeCallback issue: #1255. + setTimeout(() => {}, 1); + + if (error) { + reject(error); + } + else { + try { + let syncedRealm = new _Realm(config); + user[specialPurposeRealmsKey][realmName] = syncedRealm; + //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented (#1255) + setTimeout(() => resolve(syncedRealm), 1); + } catch (e) { + reject(e); + } + } + }); + }); +} + +function createInManagementRealm(user, modelName, modelInitializer) { + return getSpecialPurposeRealm(user, '__management', managementSchema) + .then(managementRealm => { + return new Promise((resolve, reject) => { + try { + let o; + + const listener = () => { + if (!o) { + return; + } + + const statusCode = o.statusCode; + if (typeof statusCode === 'number') { + managementRealm.removeListener('change', listener); + + if (statusCode === 0) { + setTimeout(() => resolve(o), 1); + } + else { + const e = new Error(o.statusMessage); + e.statusCode = statusCode; + e.managementObject = o; + setTimeout(() => reject(e), 1); + } + } + } + + managementRealm.addListener('change', listener); + + managementRealm.write(() => { + o = managementRealm.create(modelName, modelInitializer); + }); + } + catch (e) { + reject(e); + } + }); + }); +} + +const accessLevels = ['none', 'read', 'write', 'admin']; +const offerAccessLevels = ['read', 'write', 'admin']; + +module.exports = { + getGrantedPermissions(recipient) { + if (recipient && ['currentUser', 'otherUser', 'any'].indexOf(recipient) === -1) { + return Promise.reject(new Error(`'${recipient}' is not a valid recipient type. Must be 'any', 'currentUser' or 'otherUser'.`)); + } + + return getSpecialPurposeRealm(this, '__permission', permissionSchema) + .then(permissionRealm => { + let permissions = permissionRealm.objects('Permission') + .filtered('NOT path ENDSWITH "__permission" AND NOT path ENDSWITH "__management"'); + + if (recipient === 'currentUser') { + permissions = permissions.filtered('userId = $0', this.identity); + } + else if (recipient === 'otherUser') { + permissions = permissions.filtered('userId != $0', this.identity); + } + return permissions; + }); + }, + + applyPermissions(condition, realmUrl, accessLevel) { + if (!realmUrl) { + return Promise.reject(new Error('realmUrl must be specified')); + } + + if (accessLevels.indexOf(accessLevel) === -1) { + return Promise.reject(new Error(`'${accessLevel}' is not a valid access level. Must be ${accessLevels.join(', ')}.`)); + } + + const mayRead = accessLevel === 'read' || accessLevel === 'write' || accessLevel === 'admin'; + const mayWrite = accessLevel === 'write' || accessLevel === 'admin'; + const mayManage = accessLevel === 'admin'; + + const permissionChange = { + id: generateUniqueId(), + createdAt: new Date(), + updatedAt: new Date(), + realmUrl, + mayRead, + mayWrite, + mayManage + }; + + if (condition.hasOwnProperty('userId')) { + permissionChange.userId = condition.userId; + } + else { + permissionChange.userId = ''; + permissionChange.metadataKey = condition.metadataKey; + permissionChange.metadataValue = condition.metadataValue; + } + + return createInManagementRealm(this, 'PermissionChange', permissionChange); + }, + + offerPermissions(realmUrl, accessLevel, expiresAt) { + if (!realmUrl) { + return Promise.reject(new Error('realmUrl must be specified')); + } + + if (offerAccessLevels.indexOf(accessLevel) === -1) { + return Promise.reject(new Error(`'${accessLevel}' is not a valid access level. Must be ${offerAccessLevels.join(', ')}.`)); + } + + const mayRead = true; + const mayWrite = accessLevel === 'write' || accessLevel === 'admin'; + const mayManage = accessLevel === 'admin'; + + const permissionOffer = { + id: generateUniqueId(), + createdAt: new Date(), + updatedAt: new Date(), + expiresAt, + realmUrl, + mayRead, + mayWrite, + mayManage + }; + + return createInManagementRealm(this, 'PermissionOffer', permissionOffer) + .then(appliedOffer => appliedOffer.token); + }, + + acceptPermissionOffer(token) { + if (!token) { + return Promise.reject(new Error('Offer token must be specified')); + } + + const permissionOfferResponse = { + id: generateUniqueId(), + createdAt: new Date(), + updatedAt: new Date(), + token + }; + + return createInManagementRealm(this, 'PermissionOfferResponse', permissionOfferResponse) + .then(appliedReponse => appliedReponse.realmUrl); + }, + + invalidatePermissionOffer(permissionOfferOrToken) { + return getSpecialPurposeRealm(this, '__management', managementSchema) + .then(managementRealm => { + let permissionOffer; + + if (typeof permissionOfferOrToken === 'string') { + // We were given a token, not an object. Find the matching object. + const q = managementRealm.objects('PermissionOffer') + .filtered('token = $0', permissionOfferOrToken); + + if (q.length === 0) { + throw new Error("No permission offers with the given token were found"); + } + + permissionOffer = q[0]; + } + else { + permissionOffer = permissionOfferOrToken; + } + + managementRealm.write(() => { + permissionOffer.expiresAt = new Date(); + }); + }); + } +} diff --git a/lib/user-methods.js b/lib/user-methods.js index 34815730..03363026 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -19,6 +19,7 @@ 'use strict'; const AuthError = require('./errors').AuthError; +const permissionApis = require('./permission-api'); function node_require(module) { return require(module); @@ -139,8 +140,7 @@ function _authenticate(userConstructor, server, json, callback) { .catch(callback); } -module.exports = { - static: { +const staticMethods = { get current() { const allUsers = this.all; const keys = Object.keys(allUsers); @@ -208,8 +208,9 @@ module.exports = { }, _refreshAccessToken: refreshAccessToken - }, - instance: { + }; + +const instanceMethods = { openManagementRealm() { let url = url_parse(this.server); if (url.protocol === 'http:') { @@ -257,5 +258,12 @@ module.exports = { } }); }, - }, -}; \ No newline at end of file + }; + +// Append the permission apis +Object.assign(instanceMethods, permissionApis); + +module.exports = { + static: staticMethods, + instance: instanceMethods +}; diff --git a/package-lock.json b/package-lock.json index aa5f2159..54eee255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "realm", - "version": "1.10.2", + "version": "1.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -12,7 +12,7 @@ "acorn": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", - "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==", + "integrity": "sha1-U/4WERH5EquZnuiHqQoLxSgi/XU=", "dev": true }, "acorn-jsx": { @@ -67,7 +67,7 @@ "aproba": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz", - "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==" + "integrity": "sha1-RcZikJTeTpb2k+9+q3SuB5wkD8E=" }, "are-we-there-yet": { "version": "1.1.4", @@ -322,7 +322,7 @@ "circular-json": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "integrity": "sha1-gVyZ6oT2gJUp0vRXkb34JxE1LWY=", "dev": true }, "cli-cursor": { @@ -521,7 +521,7 @@ "es-abstract": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.8.0.tgz", - "integrity": "sha512-Cf9/h5MrXtExM20gSS55YFrGKCyPrRBjIVBtVyy8vmlsDfe0NPKMWj65tPLgzyfPuapWxh5whpXCtW4+AW5mRg==", + "integrity": "sha1-OwA4XoVymTK+/6kWO76hI06TKRQ=", "dev": true, "requires": { "es-to-primitive": "1.1.1", @@ -723,7 +723,7 @@ "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", "dev": true }, "esquery": { @@ -800,6 +800,27 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fbjs": { + "version": "0.8.14", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.14.tgz", + "integrity": "sha1-0dviviVMNakeCfMfnNUKQLKg7Rw=", + "requires": { + "core-js": "1.2.7", + "isomorphic-fetch": "2.2.1", + "loose-envify": "1.3.1", + "object-assign": "4.1.1", + "promise": "7.3.1", + "setimmediate": "1.0.5", + "ua-parser-js": "0.7.14" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, "fd-slicer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", @@ -941,7 +962,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -954,7 +975,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=", "dev": true }, "globby": { @@ -1032,7 +1053,7 @@ "hosted-git-info": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=", "dev": true }, "http-basic": { @@ -1070,7 +1091,7 @@ "iconv-lite": { "version": "0.4.18", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", - "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" + "integrity": "sha1-I9hlaxaq5nQqwpcy6o8DNqR4nPI=" }, "ignore": { "version": "3.3.3", @@ -1249,6 +1270,15 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.2", + "whatwg-fetch": "2.0.3" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1263,13 +1293,12 @@ "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "js-yaml": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", - "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", + "integrity": "sha1-CHdc69/dNZIJ8NKs04PI+GppBKA=", "dev": true, "requires": { "argparse": "1.0.9", @@ -1294,7 +1323,7 @@ "jsdoc": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.4.tgz", - "integrity": "sha512-VmTw0J+2L16IxAe0JSDSAcH0F+DbZxaj8wN1AjHtKMQU/hO0ciIl5ZE93XqrrFIbknobuqHKJCXZj6+Hk57MjA==", + "integrity": "sha1-zu73xLrEM1yxD/QeOg9Yk5pTRCg=", "dev": true, "requires": { "babylon": "7.0.0-beta.19", @@ -1512,7 +1541,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "dev": true, "requires": { "js-tokens": "3.0.2" } @@ -1539,7 +1567,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "1.1.8" } @@ -1560,7 +1588,7 @@ "mockery": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mockery/-/mockery-2.1.0.tgz", - "integrity": "sha512-9VkOmxKlWXoDO/h1jDZaS4lH33aWfRiJiNT/tKj+8OGzrcFDLo8d0syGdbsc3Bc4GvRXPb+NMMvojotmuGJTvA==", + "integrity": "sha1-WwrvH/Vk8PgTlEXhZVNseQlxNHA=", "dev": true }, "ms": { @@ -1588,7 +1616,7 @@ "node-fetch": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.2.tgz", - "integrity": "sha512-xZZUq2yDhKMIn/UgG5q//IZSNLJIwW2QxS14CNH5spuiXkITM2pUitjdq58yLSaU7m4M0wBNaM2Gh/ggY4YJig==", + "integrity": "sha1-xU6arFfkModSM1JfPIkcQVn/79c=", "requires": { "encoding": "0.1.12", "is-stream": "1.1.0" @@ -1632,7 +1660,7 @@ "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "dev": true, "requires": { "hosted-git-info": "2.5.0", @@ -1644,7 +1672,7 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", "requires": { "are-we-there-yet": "1.1.4", "console-control-strings": "1.1.0", @@ -1805,11 +1833,20 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", "requires": { "asap": "2.0.6" } }, + "prop-types": { + "version": "15.5.10", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", + "integrity": "sha1-J5ffwxJhguOpXj37suiT3ddFYVQ=", + "requires": { + "fbjs": "0.8.14", + "loose-envify": "1.3.1" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -1873,7 +1910,7 @@ "readable-stream": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "integrity": "sha1-No8lEtefnUb9/HE0mueHi7weuVw=", "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -1986,7 +2023,7 @@ "resolve": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", - "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "integrity": "sha1-p1vgHFPaJdk0qY69DkxKcxL5KoY=", "dev": true, "requires": { "path-parse": "1.0.5" @@ -2034,18 +2071,23 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=" }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, "shelljs": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", @@ -2153,7 +2195,7 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { "safe-buffer": "5.1.1" } @@ -2237,7 +2279,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0", @@ -2372,6 +2414,11 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "ua-parser-js": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.14.tgz", + "integrity": "sha1-EQ1T+kw/MmwSEpK76skE0uAzh8o=" + }, "uid-number": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", @@ -2432,7 +2479,7 @@ "uuid": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=" }, "validate-npm-package-license": { "version": "3.0.1", @@ -2461,10 +2508,15 @@ } } }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + }, "wide-align": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "integrity": "sha1-Vx4PGwYEY268DfwhsDObvjE0FxA=", "requires": { "string-width": "1.0.2" } diff --git a/package.json b/package.json index 21eeadce..2caa513a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,11 @@ { "name": "realm", "description": "Realm is a mobile database: an alternative to SQLite and key-value stores", +<<<<<<< HEAD "version": "2.0.0-rc5", +======= + "version": "1.11.1", +>>>>>>> 38bceddfb19fd9f8ac5cc62ddb8bfa626270aa5c "license": "Apache-2.0", "homepage": "https://realm.io", "keywords": [ diff --git a/react-native/android/build.gradle b/react-native/android/build.gradle index 01052378..53fceccc 100644 --- a/react-native/android/build.gradle +++ b/react-native/android/build.gradle @@ -137,6 +137,41 @@ def findNdkBuildFullPath() { return null } +def checkNdkVersion(ndkBuildFullPath) { + def ndkPath = new File(ndkBuildFullPath).getParent() + def detectedNdkVersion + def releaseFile = new File(ndkPath, 'RELEASE.TXT') + def propertyFile = new File(ndkPath, 'source.properties') + if (releaseFile.isFile()) { + detectedNdkVersion = releaseFile.text.trim().split()[0].split('-')[0] + } else if (propertyFile.isFile()) { + detectedNdkVersion = getValueFromPropertiesFile(propertyFile, 'Pkg.Revision') + if (detectedNdkVersion == null) { + throw new GradleException("Failed to obtain the NDK version information from ${ndkPath}/source.properties") + } + } else { + throw new GradleException("Neither ${releaseFile.getAbsolutePath()} nor ${propertyFile.getAbsolutePath()} is a file.") + } + if (detectedNdkVersion != project.ndkVersion) { + throw new GradleException("Your NDK version: ${detectedNdkVersion}." + + " Realm JNI must be compiled with the version ${project.ndkVersion} of NDK.") + } +} + +static def getValueFromPropertiesFile(File propFile, String key) { + if (!propFile.isFile() || !propFile.canRead()) { + return null + } + def prop = new Properties() + def reader = propFile.newReader() + try { + prop.load(reader) + } finally { + reader.close() + } + return prop.get(key) +} + def getNdkBuildFullPath() { def ndkBuildFullPath = findNdkBuildFullPath() if (ndkBuildFullPath == null) { @@ -153,6 +188,9 @@ def getNdkBuildFullPath() { "(On Windows, make sure you escape backslashes in local.properties or use forward slashes, e.g. C:\\\\ndk or C:/ndk rather than C:\\ndk)", null) } + + checkNdkVersion(ndkBuildFullPath); + return ndkBuildFullPath } diff --git a/react-native/android/gradle.properties b/react-native/android/gradle.properties index 645750a5..9194dd13 100644 --- a/react-native/android/gradle.properties +++ b/react-native/android/gradle.properties @@ -6,3 +6,4 @@ POM_PACKAGING=aar POM_DESCRIPTION=Android Realm React Native module. Realm is a mobile database: a replacement for SQLite & ORMs DEBUG_BUILD=true android.useDeprecatedNdk=true +ndkVersion=r10e diff --git a/react-native/android/src/main/jni/Android.mk b/react-native/android/src/main/jni/Android.mk index 1fe4eafa..9fe404aa 100644 --- a/react-native/android/src/main/jni/Android.mk +++ b/react-native/android/src/main/jni/Android.mk @@ -70,6 +70,7 @@ LOCAL_C_INCLUDES += src/object-store/external/pegtl LOCAL_C_INCLUDES += vendor LOCAL_C_INCLUDES += $(JAVA_HOME)/include LOCAL_C_INCLUDES += $(JAVA_HOME)/include/darwin +LOCAL_C_INCLUDES += $(JAVA_HOME)/include/linux LOCAL_C_INCLUDES += core/include ifeq ($(strip $(BUILD_TYPE_SYNC)),1) LOCAL_C_INCLUDES += src/object-store/src/sync diff --git a/react-native/listview.js b/react-native/listview.js index 96d64c3a..1c3dcec1 100644 --- a/react-native/listview.js +++ b/react-native/listview.js @@ -19,6 +19,7 @@ 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { ListView as BaseListView } from 'react-native'; function hashObjects(array) { @@ -196,8 +197,8 @@ export default class ListView extends React.Component { } ListView.propTypes = { - dataSource: React.PropTypes.instanceOf(ListViewDataSource).isRequired, - renderRow: React.PropTypes.func.isRequired, + dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired, + renderRow: PropTypes.func.isRequired, }; ListView.DataSource = ListViewDataSource; diff --git a/scripts/publish.sh b/scripts/publish.sh index c4f96788..eac9288d 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -61,18 +61,8 @@ VERSION_BRANCH="${RELEASE_VERSION%.*}.x" git fetch origin || die 'Failed to fetch from git origin.' [ -z "$(git rev-list origin/$BRANCH..HEAD)" ] || die 'Local commits are not pushed to origin.' -# Run all tests that must pass before publishing. -for test in eslint jsdoc license-check react-example react-tests-android react-tests; do - for configuration in Debug Release; do - echo "RUNNING TEST: $test ($configuration)" - echo '----------------------------------------' - npm test "$test" "$configuration" || die "Test Failed: $test ($configuration)" - echo - done -done - # Double check before actually publishing. -confirm "Are you sure you want to publish $VERSION?" || die "Aborted publishing $VERSION" +confirm "Are you sure you want to publish $VERSION? (Did you run all tests)?" || die "Aborted publishing $VERSION" # This should fail if this tag already exists. git tag "v$VERSION" @@ -88,3 +78,5 @@ REALM_BUILD_ANDROID=1 npm publish ${PRERELEASE:+--tag $PRERELEASE} # Only push the tag to GitHub if the publish was successful. echo "Pushing v$VERSION tag to GitHub..." git push origin "v$VERSION" + +echo "Done. Now, you should update the documentation!" diff --git a/scripts/test.sh b/scripts/test.sh index 840deb68..75e10535 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -396,6 +396,17 @@ case "$TARGET" in npm install --build-from-source=realm npm run test-runners ;; +"all") + # Run all tests that must pass before publishing. + for test in eslint license-check react-example react-tests-android react-tests; do + for configuration in Debug Release; do + echo "RUNNING TEST: $test ($configuration)" + echo '----------------------------------------' + npm test "$test" "$configuration" || die "Test Failed: $test ($configuration)" + echo + done + done + ;; "object-store") pushd src/object-store cmake -DCMAKE_BUILD_TYPE="$CONFIGURATION" . diff --git a/src/android/platform.cpp b/src/android/platform.cpp index e008169f..e26ba655 100644 --- a/src/android/platform.cpp +++ b/src/android/platform.cpp @@ -93,4 +93,19 @@ namespace realm { s_default_realm_directory + "/*.realm.lock"; system(cmd.c_str()); } + + void remove_directory(const std::string &path) + { + std::string cmd_clear_dir = "rm " + path + "/*"; + system(cmd_clear_dir.c_str()); + + std::string cmd_rmdir = "rmdir " + path; + system(cmd_rmdir.c_str()); + } + + void remove_file(const std::string &path) + { + std::string cmd = "rm " + path; + system(cmd.c_str()); + } } diff --git a/src/concurrent_deque.hpp b/src/concurrent_deque.hpp index 4c05b372..260ff107 100644 --- a/src/concurrent_deque.hpp +++ b/src/concurrent_deque.hpp @@ -20,37 +20,26 @@ #include #include -#include #include +#include + namespace realm { -class ConcurrentDequeTimeout : public std::exception { - public: - ConcurrentDequeTimeout() : std::exception() {} -}; - template class ConcurrentDeque { - public: - T pop_front(size_t timeout = 0) { +public: + T pop_back() { std::unique_lock lock(m_mutex); - while (m_deque.empty()) { - wait(lock, timeout); - } - T item = std::move(m_deque.front()); - m_deque.pop_front(); - return item; + m_condition.wait(lock, [this] { return !m_deque.empty(); }); + return do_pop_back(); } - T pop_back(size_t timeout = 0) { + util::Optional try_pop_back(size_t timeout) { std::unique_lock lock(m_mutex); - while (m_deque.empty()) { - wait(lock, timeout); - } - T item = std::move(m_deque.back()); - m_deque.pop_back(); - return item; + m_condition.wait_for(lock, std::chrono::milliseconds(timeout), + [this] { return !m_deque.empty(); }); + return m_deque.empty() ? util::none : util::make_optional(do_pop_back()); } void push_front(T&& item) { @@ -72,19 +61,16 @@ class ConcurrentDeque { return m_deque.empty(); } - void wait(std::unique_lock &lock, size_t timeout = 0) { - if (!timeout) { - m_condition.wait(lock); - } - else if (m_condition.wait_for(lock, std::chrono::milliseconds(timeout)) == std::cv_status::timeout) { - throw ConcurrentDequeTimeout(); - } - } - - private: +private: std::condition_variable m_condition; std::mutex m_mutex; std::deque m_deque; + + T do_pop_back() { + T item = std::move(m_deque.back()); + m_deque.pop_back(); + return item; + } }; } // realm diff --git a/src/ios/platform.mm b/src/ios/platform.mm index 26d93075..c412f8d6 100644 --- a/src/ios/platform.mm +++ b/src/ios/platform.mm @@ -101,5 +101,20 @@ void remove_realm_files_from_directory(const std::string &directory) } } } - + +void remove_file(const std::string &path) +{ + NSFileManager *manager = [NSFileManager defaultManager]; + NSString *filePath = @(path.c_str()); + + if (![manager removeItemAtPath:filePath error:nil]) { + throw std::runtime_error((std::string)"Failed to delete file at path " + filePath.UTF8String); + } +} + +void remove_directory(const std::string &path) +{ + remove_file(path); // works for directories too +} + } diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 6e7ee452..c201ff1e 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -171,11 +171,16 @@ public: static void delete_one(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void delete_all(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void write(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void begin_transaction(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue&); + static void commit_transaction(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue&); + static void cancel_transaction(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue&); static void add_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void wait_for_download_completion(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_all_listeners(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void close(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void compact(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -183,6 +188,7 @@ public: static void get_schema_version(ContextType, ObjectType, ReturnValue &); static void get_schema(ContextType, ObjectType, ReturnValue &); static void get_read_only(ContextType, ObjectType, ReturnValue &); + static void get_is_in_transaction(ContextType, ObjectType, ReturnValue &); #if REALM_ENABLE_SYNC static void get_sync_session(ContextType, ObjectType, ReturnValue &); #endif @@ -194,6 +200,7 @@ public: static void schema_version(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void clear_test_state(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void copy_bundled_realm_files(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void delete_file(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); // static properties static void get_default_path(ContextType, ObjectType, ReturnValue &); @@ -205,6 +212,7 @@ public: {"schemaVersion", wrap}, {"clearTestState", wrap}, {"copyBundledRealmFiles", wrap}, + {"deleteFile", wrap}, {"_waitForDownload", wrap}, }; @@ -219,10 +227,14 @@ public: {"delete", wrap}, {"deleteAll", wrap}, {"write", wrap}, + {"beginTransaction", wrap}, + {"commitTransaction", wrap}, + {"cancelTransaction", wrap}, {"addListener", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, {"close", wrap}, + {"compact", wrap}, }; PropertyMap const properties = { @@ -231,6 +243,7 @@ public: {"schemaVersion", {wrap, nullptr}}, {"schema", {wrap, nullptr}}, {"readOnly", {wrap, nullptr}}, + {"isInTransaction", {wrap, nullptr}}, #if REALM_ENABLE_SYNC {"syncSession", {wrap, nullptr}}, #endif @@ -390,6 +403,28 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t config.schema_version = 0; } + static const String compact_on_launch_string = "shouldCompactOnLaunch"; + ValueType compact_value = Object::get_property(ctx, object, compact_on_launch_string); + if (!Value::is_undefined(ctx, compact_value)) { + if (config.schema_mode == SchemaMode::ReadOnly) { + throw std::invalid_argument("Cannot set 'shouldCompactOnLaunch' when 'readOnly' is set."); + } + if (config.sync_config) { + throw std::invalid_argument("Cannot set 'shouldCompactOnLaunch' when 'sync' is set."); + } + + FunctionType should_compact_on_launch_function = Value::validated_to_function(ctx, compact_value, "shouldCompactOnLaunch"); + config.should_compact_on_launch_function = [=](uint64_t total_bytes, uint64_t unused_bytes) { + ValueType arguments[2] = { + Value::from_number(ctx, total_bytes), + Value::from_number(ctx, unused_bytes) + }; + + ValueType should_compact = Function::callback(ctx, should_compact_on_launch_function, this_object, 2, arguments); + return Value::to_boolean(ctx, should_compact); + }; + } + static const String migration_string = "migration"; ValueType migration_value = Object::get_property(ctx, object, migration_string); if (!Value::is_undefined(ctx, migration_value)) { @@ -493,6 +528,37 @@ void RealmClass::copy_bundled_realm_files(ContextType ctx, FunctionType, Obje realm::copy_bundled_realm_files(); } +template +void RealmClass::delete_file(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + + ValueType value = arguments[0]; + if (!Value::is_object(ctx, value)) { + throw std::runtime_error("Invalid argument, expected a Realm configuration object"); + } + + ObjectType object = Value::validated_to_object(ctx, value); + realm::Realm::Config config; + + static const String path_string = "path"; + ValueType path_value = Object::get_property(ctx, object, path_string); + if (!Value::is_undefined(ctx, path_value)) { + config.path = Value::validated_to_string(ctx, path_value, "path"); + } + else if (config.path.empty()) { + config.path = js::default_path(); + } + + config.path = normalize_realm_path(config.path); + + std::string realm_file_path = config.path; + realm::remove_file(realm_file_path); + realm::remove_file(realm_file_path + ".lock"); + realm::remove_file(realm_file_path + ".note"); + realm::remove_directory(realm_file_path + ".management"); + +} + template void RealmClass::get_default_path(ContextType ctx, ObjectType object, ReturnValue &return_value) { return_value.set(realm::js::default_path()); @@ -533,6 +599,11 @@ void RealmClass::get_read_only(ContextType ctx, ObjectType object, ReturnValu return_value.set(get_internal>(object)->get()->config().read_only()); } +template +void RealmClass::get_is_in_transaction(ContextType ctx, ObjectType object, ReturnValue &return_value) { + return_value.set(get_internal>(object)->get()->is_in_transaction()); +} + #if REALM_ENABLE_SYNC template void RealmClass::get_sync_session(ContextType ctx, ObjectType object, ReturnValue &return_value) { @@ -784,6 +855,30 @@ void RealmClass::write(ContextType ctx, FunctionType, ObjectType this_object, realm->commit_transaction(); } +template +void RealmClass::begin_transaction(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + SharedRealm realm = *get_internal>(this_object); + realm->begin_transaction(); +} + +template +void RealmClass::commit_transaction(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + SharedRealm realm = *get_internal>(this_object); + realm->commit_transaction(); +} + +template +void RealmClass::cancel_transaction(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + SharedRealm realm = *get_internal>(this_object); + realm->cancel_transaction(); +} + template void RealmClass::add_listener(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { validate_argument_count(argc, 2); @@ -834,5 +929,17 @@ void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, realm->close(); } +template +void RealmClass::compact(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + SharedRealm realm = *get_internal>(this_object); + if (realm->is_in_transaction()) { + throw std::runtime_error("Cannot compact a Realm within a transaction."); + } + + return_value.set(realm->compact()); +} + } // js } // realm diff --git a/src/node/platform.cpp b/src/node/platform.cpp index 4d23e587..9a16fe85 100644 --- a/src/node/platform.cpp +++ b/src/node/platform.cpp @@ -134,4 +134,55 @@ void remove_realm_files_from_directory(const std::string &dir_path) } } +void remove_directory(const std::string &path) +{ + FileSystemRequest exists_req; + if (uv_fs_stat(uv_default_loop(), &exists_req, path.c_str(), nullptr) != 0) { + if (exists_req.result == UV_ENOENT) { + // path doesn't exist, ignore + return; + } else { + throw UVException(static_cast(exists_req.result)); + } + } + + uv_dirent_t dir_entry; + FileSystemRequest dir_scan_req; + if (uv_fs_scandir(uv_default_loop(), &dir_scan_req, path.c_str(), 0, nullptr) < 0) { + throw UVException(static_cast(dir_scan_req.result)); + } + + while (uv_fs_scandir_next(&dir_scan_req, &dir_entry) != UV_EOF) { + std::string dir_entry_path = path + '/' + dir_entry.name; + FileSystemRequest delete_req; + if (uv_fs_unlink(uv_default_loop(), &delete_req, dir_entry_path.c_str(), nullptr) != 0) { + throw UVException(static_cast(delete_req.result)); + } + } + + FileSystemRequest rmdir_req; + if (uv_fs_rmdir(uv_default_loop(), &rmdir_req, path.c_str(), nullptr)) { + throw UVException(static_cast(rmdir_req.result)); + } +} + + +void remove_file(const std::string &path) +{ + FileSystemRequest exists_req; + if (uv_fs_stat(uv_default_loop(), &exists_req, path.c_str(), nullptr) != 0) { + if (exists_req.result == UV_ENOENT) { + // path doesn't exist, ignore + return; + } else { + throw UVException(static_cast(exists_req.result)); + } + } + + FileSystemRequest delete_req; + if (uv_fs_unlink(uv_default_loop(), &delete_req, path.c_str(), nullptr) != 0) { + throw UVException(static_cast(delete_req.result)); + } +} + } // realm diff --git a/src/platform.hpp b/src/platform.hpp index 56afc299..b56037bd 100644 --- a/src/platform.hpp +++ b/src/platform.hpp @@ -41,4 +41,10 @@ void copy_bundled_realm_files(); // remove all realm files in the given directory void remove_realm_files_from_directory(const std::string &directory); +// remove file at the given path +void remove_file(const std::string &path); + +// remove directory at the given path +void remove_directory(const std::string &path); + } diff --git a/src/rpc.cpp b/src/rpc.cpp index 32e693ed..896fad8a 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -77,17 +77,16 @@ json RPCWorker::pop_task_result() { } void RPCWorker::try_run_task() { - try { - // Use a 10 millisecond timeout to keep this thread unblocked. - auto task = m_tasks.pop_back(10); - task(); + // Use a 10 millisecond timeout to keep this thread unblocked. + auto task = m_tasks.try_pop_back(10); + if (!task) { + return; + } - // Since this can be called recursively, it must be pushed to the front of the queue *after* running the task. - m_futures.push_front(task.get_future()); - } - catch (ConcurrentDequeTimeout &) { - // We tried. - } + (*task)(); + + // Since this can be called recursively, it must be pushed to the front of the queue *after* running the task. + m_futures.push_front(task->get_future()); } void RPCWorker::stop() { diff --git a/tests/js/index.js b/tests/js/index.js index d54534db..ec8c9908 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -40,6 +40,12 @@ if (!(typeof process === 'object' && process.platform === 'win32')) { if (Realm.Sync) { TESTS.UserTests = require('./user-tests'); TESTS.SessionTests = require('./session-tests'); + + // FIXME: Permission tests currently fail in chrome debugging mode. + if (typeof navigator === 'undefined' || + !/Chrome/.test(navigator.userAgent)) { // eslint-disable-line no-undef + TESTS.PermissionTests = require('./permission-tests'); + } } function node_require(module) { return require(module); } diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js new file mode 100644 index 00000000..02a9d10c --- /dev/null +++ b/tests/js/permission-tests.js @@ -0,0 +1,115 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2017 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'; + +var Realm = require('realm'); +var TestCase = require('./asserts'); + +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +function createUsersWithTestRealms(count) { + const createUserWithTestRealm = username => new Promise((resolve, reject) => { + Realm.Sync.User.register('http://localhost:9080', username, 'password', (error, user) => { + if (error) { + reject(error); + } + else { + new Realm({ sync: { user, url: 'realm://localhost:9080/~/test'}}).close(); + resolve(user); + } + }) + }); + + // Generate some usernames + const usernames = new Array(count).fill(undefined).map(uuid); + + // And turn them into users and realms + const userPromises = usernames.map(createUserWithTestRealm); + return Promise.all(userPromises); +} + +function wait(t) { + return new Promise(resolve => setTimeout(resolve, t)); +} + +module.exports = { + testApplyAndGetGrantedPermissions() { + return createUsersWithTestRealms(1) + .then(([user]) => { + return user.applyPermissions({ userId: '*' }, `/${user.identity}/test`, 'read') + .then(() => user.getGrantedPermissions('any')) + .then(permissions => { + TestCase.assertEqual(permissions[1].path, `/${user.identity}/test`); + TestCase.assertEqual(permissions[1].mayRead, true); + TestCase.assertEqual(permissions[1].mayWrite, false); + TestCase.assertEqual(permissions[1].mayManage, false); + }); + }); + }, + + testOfferPermissions() { + return createUsersWithTestRealms(2) + .then(([user1, user2]) => { + return user1.offerPermissions(`/${user1.identity}/test`, 'read') + .then(token => user2.acceptPermissionOffer(token)) + .then(realmUrl => { + TestCase.assertEqual(realmUrl, `/${user1.identity}/test`); + return user2.getGrantedPermissions('any') + .then(permissions => { + TestCase.assertEqual(permissions[1].path, `/${user1.identity}/test`); + TestCase.assertEqual(permissions[1].mayRead, true); + TestCase.assertEqual(permissions[1].mayWrite, false); + TestCase.assertEqual(permissions[1].mayManage, false); + }); + }); + }); + }, + + testInvalidatePermissionOffer() { + return createUsersWithTestRealms(2) + .then(([user1, user2]) => { + user1.offerPermissions(`/${user1.identity}/test`, 'read') + .then((token) => { + return user1.invalidatePermissionOffer(token) + // Since we don't yet support notification when the invalidation has gone through, + // wait for a bit and hope the server is done processing. + .then(wait(100)) + .then(user2.acceptPermissionOffer(token)) + // We want the call to fail, i.e. the catch() below should be called. + .then(() => { throw new Error("User was able to accept an invalid permission offer token"); }) + .catch(error => { + try { + TestCase.assertEqual(error.message, 'The permission offer is expired.'); + TestCase.assertEqual(error.statusCode, 701); + } + catch (e) { + throw new Error(e); + } + }); + }); + }); + }, +} + diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index a2c335e0..e98db623 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -943,5 +943,128 @@ module.exports = { realm.write(() => realm.delete(realm.objects('PersonObject'))); TestCase.assertTrue(realm.empty); + }, + + testManualTransaction: function() { + const realm = new Realm({schema: [schemas.TestObject]}); + TestCase.assertTrue(realm.empty); + + realm.beginTransaction(); + realm.create('TestObject', {doubleCol: 3.1415}); + realm.commitTransaction(); + + TestCase.assertEqual(realm.objects('TestObject').length, 1); + }, + + testCancelTransaction: function() { + const realm = new Realm({schema: [schemas.TestObject]}); + TestCase.assertTrue(realm.empty); + + realm.beginTransaction(); + realm.create('TestObject', {doubleCol: 3.1415}); + realm.cancelTransaction(); + + TestCase.assertTrue(realm.empty); + }, + + testIsInTransaction: function() { + const realm = new Realm({schema: [schemas.TestObject]}); + TestCase.assertTrue(!realm.isInTransaction); + realm.beginTransaction(); + TestCase.assertTrue(realm.isInTransaction); + realm.cancelTransaction(); + TestCase.assertTrue(!realm.isInTransaction); + }, + + testCompact: function() { + var wasCalled = false; + const count = 1000; + // create compactable Realm + const realm1 = new Realm({schema: [schemas.StringOnly]}); + realm1.write(() => { + realm1.create('StringOnlyObject', { stringCol: 'A' }); + for (var i = 0; i < count; i++) { + realm1.create('StringOnlyObject', { stringCol: 'ABCDEFG' }); + } + realm1.create('StringOnlyObject', { stringCol: 'B' }); + }); + realm1.close(); + + // open Realm and see if it is compacted + var shouldCompact = function(totalBytes, usedBytes) { + wasCalled = true; + const fiveHundredKB = 500*1024; + return (totalBytes > fiveHundredKB) && (usedBytes / totalBytes) < 0.2; + }; + const realm2 = new Realm({schema: [schemas.StringOnly], shouldCompactOnLaunch: shouldCompact}); + TestCase.assertTrue(wasCalled); + TestCase.assertEqual(realm2.objects('StringOnlyObject').length, count + 2); + // we don't check if the file is smaller as we assume that Object Store does it + realm2.close(); + }, + + testManualCompact: function() { + const realm1 = new Realm({schema: [schemas.StringOnly]}); + realm1.write(() => { + realm1.create('StringOnlyObject', { stringCol: 'A' }); + }); + TestCase.assertTrue(realm1.compact()); + realm1.close(); + + const realm2 = new Realm({schema: [schemas.StringOnly]}); + TestCase.assertEqual(realm2.objects('StringOnlyObject').length, 1); + realm2.close(); + }, + + testManualCompactInWrite: function() { + const realm = new Realm({schema: [schemas.StringOnly]}); + realm.write(() => { + TestCase.assertThrows(() => { + realm.compact(); + }); + }); + TestCase.assertTrue(realm.empty); + }, + + testManualCompactMultipleInstances: function() { + const realm1 = new Realm({schema: [schemas.StringOnly]}); + const realm2 = new Realm({schema: [schemas.StringOnly]}); + TestCase.assertThrows(realm1.compact()); + }, + + testRealmDeleteFileDefaultConfigPath: function() { + const config = {schema: [schemas.TestObject]}; + const realm = new Realm(config); + + realm.write(function() { + realm.create('TestObject', {doubleCol: 1}); + }); + + TestCase.assertEqual(realm.objects('TestObject').length, 1); + realm.close(); + + Realm.deleteFile(config); + + const realm2 = new Realm(config); + TestCase.assertEqual(realm2.objects('TestObject').length, 0); + realm.close(); + }, + + testRealmDeleteFileCustomConfigPath: function() { + const config = {schema: [schemas.TestObject], path: 'test-realm-delete-file.realm'}; + const realm = new Realm(config); + + realm.write(function() { + realm.create('TestObject', {doubleCol: 1}); + }); + + TestCase.assertEqual(realm.objects('TestObject').length, 1); + realm.close(); + + Realm.deleteFile(config); + + const realm2 = new Realm(config); + TestCase.assertEqual(realm2.objects('TestObject').length, 0); + realm.close(); } }; diff --git a/tests/js/schemas.js b/tests/js/schemas.js index a040963e..2dde4649 100644 --- a/tests/js/schemas.js +++ b/tests/js/schemas.js @@ -119,6 +119,13 @@ exports.StringPrimary = { } }; +exports.StringOnly = { + name: 'StringOnlyObject', + properties: { + stringCol: 'string', + } +}; + exports.AllTypes = { name: 'AllTypesObject', primaryKey: 'primaryCol', diff --git a/tests/react-test-app/index.android.js b/tests/react-test-app/index.android.js index 2b22be59..6e01d26b 100644 --- a/tests/react-test-app/index.android.js +++ b/tests/react-test-app/index.android.js @@ -51,7 +51,7 @@ async function runTests() { await runTest(suiteName, testName); } catch (e) { - itemTest.ele('error', {'message': ''}, e.message); + itemTest.ele('error', {'message': e.message, 'stacktrace': e.stack}, e.toString()); nbrFailures++; } }