diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 95a8b062..17ab0c7a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -40,6 +40,6 @@ Full projects that we can compile and run ourselves are ideal! ## Version of Realm and Tooling - Realm JS SDK Version: ? -- Node or React Nattive: ? +- Node or React Native: ? - Client OS & Version: ? - Which debugger for React Native: ?/None diff --git a/CHANGELOG.md b/CHANGELOG.md index b04281cc..8672dfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +2.0.0 Release notes (2017-9-26) +============================================================= +### Breaking changes +* None + +### Enhancements +* Add a callback function used to verify SSL certificates in the sync config. + +### Bug fixes +* Fixed port conflict between RN >= 0.48 inspector proxy and RPC server used for Chrome debugging (#1294). + +### Internal +* Alignment of permission schemas. +* Updating sync (2.0.0-rc24). + 2.0.0-rc10 Release notes (2017-9-19) ============================================================= ### Breaking changes @@ -15,7 +30,7 @@ ### Enhancements * Improve performance of the RPC worker for chrome debugging. * Added Progress API `realm.syncSession.addProgressNotification` and `realm.syncSession.removeProgressNotification` -* Added additional parameter for `Realm.open` and `Realm.openAsync` for download progress notifications +* Added additional parameter for `Realm.open` and `Realm.openAsync` for download progress notifications * Added `Realm.deleteFile` for deleting a Realm (#363). * Added `Realm.deleteModel` for deleting a Realm model in a migration (#573). * Added support for in-memory Realms. @@ -150,7 +165,7 @@ * Added `indexOf()` method on `Realm.Results` and `Realm.List` that returns the index of the object in the collection. ### Bug fixes -* Fix opening synced realms with a logged-in admin user. +* Fix opening synced realms with a logged-in admin user. 1.8.1 Release notes (2017-6-20) ============================================================= @@ -164,7 +179,7 @@ * Added `objectSchema()` method on `Realm.Object` that returns the schema for the object. ### Bug fixes -* Fix `Realm.Sync.User.prototype.isAdmin` returning `false` for logged-in admin users. +* Fix `Realm.Sync.User.prototype.isAdmin` returning `false` for logged-in admin users. 1.8.0 Release notes (2017-6-15) ============================================================= diff --git a/dependencies.list b/dependencies.list index 838e5511..d8a32030 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,5 +1,5 @@ PACKAGE_NAME=realm-js -VERSION=2.0.0-rc10 +VERSION=2.0.0-rc11 REALM_CORE_VERSION=3.2.1 -REALM_SYNC_VERSION=2.0.0-rc22 +REALM_SYNC_VERSION=2.0.0-rc24 REALM_OBJECT_SERVER_VERSION=2.0.0-alpha.36 diff --git a/docs/realm.js b/docs/realm.js index 63c14ad5..e379d1c7 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -92,17 +92,17 @@ class Realm { constructor(config) {} /** - * Open a realm asynchronously with a promise. If the realm is synced, it will be fully + * Open a Realm asynchronously with a promise. If the Realm is synced, it will be fully * synchronized before it is available. - * @param {Realm~Configuration} config + * @param {Realm~Configuration} config * @returns {ProgressPromise} - a promise that will be resolved with the realm instance when it's available. */ static open(config) {} /** - * Open a realm asynchronously with a callback. If the realm is synced, it will be fully + * Open a Realm asynchronously with a callback. If the Realm is synced, it will be fully * synchronized before it is available. - * @param {Realm~Configuration} config + * @param {Realm~Configuration} config * @param {callback(error, realm)} - will be called when the realm is ready. * @param {callback(transferred, transferable)} [progressCallback] - an optional callback for download progress notifications * @throws {Error} If anything in the provided `config` is invalid. @@ -217,7 +217,7 @@ class Realm { /* * 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. @@ -269,12 +269,12 @@ 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 + * @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) + * - `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 + * 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. @@ -288,14 +288,49 @@ Realm.defaultPath; * object types in this Realm. **Required** when first creating a Realm at this `path`. * @property {number} [schemaVersion] - **Required** (and must be incremented) after * changing the `schema`. - * @property {Object} [sync] - Sync configuration parameters with the following + * @property {Object} [sync] - Sync configuration parameters with the following * child properties: * - `user` - A `User` object obtained by calling `Realm.Sync.User.login` * - `url` - A `string` which contains a valid Realm Sync url - * - `error` - A callback function which is called in error situations + * - `error` - A callback function which is called in error situations. + * The `error` callback can take up to four optional arguments: `message`, `isFatal`, + * `category`, and `code`. * - `validate_ssl` - Indicating if SSL certificates must be validated * - `ssl_trust_certificate_path` - A path where to find trusted SSL certificates - * The `error` callback can take up to four optional arguments: `message`, `isFatal`, `category`, and `code`. + * - `open_ssl_verify_callback` - A callback function used to accept or reject the server's + * SSL certificate. open_ssl_verify_callback is called with an object of type + * + * { + * serverAddress: String, + * serverPort: Number, + * pemCertificate: String, + * acceptedByOpenSSL: Boolean, + * depth: Number + * } + * + * The return value of open_ssl_verify_callback decides whether the certificate is accepted (true) + * or rejected (false). The open_ssl_verify_callback function is only respected on platforms where + * OpenSSL is used for the sync client, e.g. Linux. The open_ssl_verify_callback function is not + * allowed to throw exceptions. If the operations needed to verify the certificate lead to an exception, + * the exception must be caught explicitly before returning. The return value would typically be false + * in case of an exception. + * + * When the sync client has received the server's certificate chain, it presents every certificate in + * the chain to the open_ssl_verify_callback function. The depth argument specifies the position of the + * certificate in the chain. depth = 0 represents the actual server certificate. The root + * certificate has the highest depth. The certificate of highest depth will be presented first. + * + * acceptedByOpenSSL is true if OpenSSL has accepted the certificate, and false if OpenSSL has rejected it. + * It is generally safe to return true when acceptedByOpenSSL is true. If acceptedByOpenSSL is false, an + * independent verification should be made. + * + * One possible way of using the open_ssl_verify_callback function is to embed the known server certificate + * in the client and accept the presented certificate if and only if it is equal to the known certificate. + * + * The purpose of open_ssl_verify_callback is to enable custom certificate handling and to solve cases where + * OpenSSL erroneously rejects valid certificates possibly because OpenSSL doesn't have access to the + * proper trust certificates. + * */ /** @@ -329,7 +364,7 @@ Realm.defaultPath; * otherwise specified. * @property {boolean} [optional] - Signals if this property may be assigned `null` or `undefined`. * @property {boolean} [indexed] - Signals if this property should be indexed. Only supported for - * `"string"`, `"int"`, and `"bool"` properties. + * `"string"`, `"int"`, and `"bool"` properties. */ /** diff --git a/docs/sync.js b/docs/sync.js index 76d20ef3..3b271d5a 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -30,9 +30,6 @@ class Sync { * _Currently only the 'change' event is supported_ * @param {function(change_event)} change_callback - called when changes are made to any Realm which * match the given regular expression - * @param {bool} validate_ssl=true - Validate the server's SSL chertificate. - * @param {string} ssl_trust_certificate_path=None - Path to a trust/anchor certificate used by the - * client to verify the server certificate. */ static addListener(server_url, admin_user, regex, name, change_callback) {} diff --git a/lib/browser/index.js b/lib/browser/index.js index 10bc7512..8ef9fc12 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -83,14 +83,14 @@ export default class Realm { if (typeof item == 'function') { let schema = item.schema; if (!schema || typeof schema != 'object') { - throw new Error("Realm object constructor must have 'schema' property"); + throw new Error("Realm object constructor must have a 'schema' property."); } let {name, properties} = schema; if (!name || typeof name != 'string') { - throw new Error("Realm object schema must have 'name' property"); + throw new Error(`Failed to read ObjectSchema: name must be of type 'string', got (${typeof name})`); } else if (!properties || typeof properties != 'object') { - throw new Error("Realm object schema must have 'properties' property"); + throw new Error(`Failed to read ObjectSchema: properties must be of type 'object', got (${typeof properties})`); } schemas.splice(i, 1, schema); diff --git a/lib/browser/objects.js b/lib/browser/objects.js index f0f5caf7..9c9ad87e 100644 --- a/lib/browser/objects.js +++ b/lib/browser/objects.js @@ -85,5 +85,5 @@ export function typeForConstructor(realmId, constructor) { } } - return null; + throw new Error("Constructor was not registered in the schema for this Realm") } diff --git a/lib/index.d.ts b/lib/index.d.ts index 941fe647..76106fe4 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -265,25 +265,25 @@ declare namespace Realm.Sync { readonly server: string; readonly token: string; static adminUser(adminToken: string, server?: string): User; - + /** * @deprecated, to be removed in future versions */ static login(server: string, username: string, password: string, callback: (error: any, user: User) => void): void; static login(server: string, username: string, password: string): Promise; - + /** * @deprecated, to be removed in future versions - */ + */ static register(server: string, username: string, password: string, callback: (error: any, user: User) => void): void; static register(server: string, username: string, password: string): Promise; - + /** * @deprecated, to be removed in versions - */ + */ 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; - + logout(): void; openManagementRealm(): Realm; retrieveAccount(provider: string, username: string): Promise; @@ -299,7 +299,7 @@ declare namespace Realm.Sync { userId: string | { metadataKey: string, metadataValue: string } }; - + type AccessLevel = 'none' | 'read' | 'write' | 'admin'; class Permission { @@ -310,7 +310,7 @@ declare namespace Realm.Sync { readonly mayRead?: boolean; readonly mayWrite?: boolean; readonly mayManage?: boolean; - } + } class PermissionChange { id: string; @@ -342,19 +342,21 @@ declare namespace Realm.Sync { } type ErrorCallback = (message?: string, isFatal?: boolean, category?: string, code?: number) => void; + type SSLVerifyCallback = (serverAddress: string, serverPort: number, pemCertificate: string, preverifyOk: number, depth: number) => boolean; interface SyncConfiguration { user: User; url: string; validate_ssl?: boolean; ssl_trust_certificate_path?: string; + ssl_verify_callback?: SSLVerifyCallback; error?: ErrorCallback; } type ProgressNotificationCallback = (transferred: number, transferable: number) => void; type ProgressDirection = 'download' | 'upload'; type ProgressMode = 'reportIndefinitely' | 'forCurrentlyOutstandingWork'; - + /** * Session * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Session.html } @@ -394,7 +396,7 @@ declare namespace Realm.Sync { function removeListener(regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): void; function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void; function setFeatureToken(token: string): void; - + /** * @deprecated, to be removed in 2.0 */ @@ -458,19 +460,19 @@ declare class Realm { */ static schemaVersion(path: string, encryptionKey?: ArrayBuffer | ArrayBufferView): number; - + /** * Open a realm asynchronously with a promise. If the realm is synced, it will be fully synchronized before it is available. - * @param {Configuration} config + * @param {Configuration} config */ static open(config: Realm.Configuration): ProgressPromise; /** * @deprecated in favor of `Realm.open` * Open a realm asynchronously with a callback. If the realm is synced, it will be fully synchronized before it is available. - * @param {Configuration} config + * @param {Configuration} config * @param {Function} callback will be called when the realm is ready. - * @param {ProgressNotificationCallback} progressCallback? a progress notification callback for 'download' direction and 'forCurrentlyOutstandingWork' mode + * @param {ProgressNotificationCallback} progressCallback? a progress notification callback for 'download' direction and 'forCurrentlyOutstandingWork' mode */ static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void, progressCallback?: Realm.Sync.ProgressNotificationCallback): void diff --git a/lib/management-schema.js b/lib/management-schema.js index 3d14b80e..bb7afa0b 100644 --- a/lib/management-schema.js +++ b/lib/management-schema.js @@ -21,50 +21,50 @@ module.exports = [ { name: 'PermissionChange', + primaryKey: 'id', properties: { - id: { type: 'string' }, - createdAt: { type: 'date' }, - updatedAt: { type: 'date' }, - statusCode: { type: 'int', optional: true }, - statusMessage: { type: 'string', optional: true }, - userId: { type: 'string' }, - metadataKey: { type: 'string', optional: true }, + id: {type: 'string'}, + createdAt: {type: 'date', default: new Date()}, + updatedAt: {type: 'date', default: new Date()}, + statusCode: { type: 'int', optional: true }, + statusMessage: {type: 'string', optional: true}, + userId: { type: 'string' }, + realmUrl: { 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 }, - mayManage: { type: 'bool', optional: true }, - }, - primaryKey: 'id' + mayRead: { type: 'bool', optional: true }, + mayWrite: { type: 'bool', optional: true }, + mayManage: { type: 'bool', optional: true }, + } }, { name: 'PermissionOffer', + primaryKey: 'id', properties: { - id: { type: 'string' }, - createdAt: { type: 'date' }, - updatedAt: { type: 'date' }, - statusCode: { type: 'int', optional: true }, - statusMessage: { type: 'string', optional: true }, - token: { type: 'string', optional: true, indexed: true }, - realmUrl: { type: 'string' }, - mayRead: { type: 'bool', default: false }, - mayWrite: { type: 'bool', default: false }, - mayManage: { type: 'bool', default: false }, - expiresAt: { type: 'date', optional: true }, - }, - primaryKey: 'id' + id: { type: 'string', optional: false, indexed: true }, + createdAt: {type: 'date', default: new Date()}, + updatedAt: {type: 'date', default: new Date()}, + statusCode: { type: 'int', optional: true }, + statusMessage: {type: 'string', optional: true}, + token: { type: 'string', optional: true, indexed: true }, + realmUrl: { type: 'string' }, + mayRead: { type: 'bool', default: false }, + mayWrite: { type: 'bool', default: false }, + mayManage: { type: 'bool', default: false }, + expiresAt: { type: 'date', optional: true } + } }, { name: 'PermissionOfferResponse', + primaryKey: 'id', properties: { - id: { type: 'string' }, - createdAt: { type: 'date' }, - updatedAt: { type: 'date' }, - statusCode: { type: 'int', optional: true }, - statusMessage: { type: 'string', optional: true }, - token: { type: 'string' }, - realmUrl: { type: 'string', optional: true }, - }, - primaryKey: 'id' + id: { type: 'string', optional: false }, + createdAt: {type: 'date', default: new Date()}, + updatedAt: {type: 'date', default: new Date()}, + statusCode: { type: 'int', optional: true }, + statusMessage: {type: 'string', optional: true}, + token: { type: 'string' }, + realmUrl: { type: 'string', optional: true } + } } ]; diff --git a/lib/permission-api.js b/lib/permission-api.js index 2df3491a..4cdff8da 100644 --- a/lib/permission-api.js +++ b/lib/permission-api.js @@ -29,20 +29,17 @@ function generateUniqueId() { return uuid; } -const permissionSchema = [ - { +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' }, + userId: {type: 'string' }, + path: { type: 'string' }, + mayRead: { type: 'bool', optional: false }, + mayWrite: { type: 'bool', optional: false }, + mayManage: { type: 'bool', optional: false }, + updatedAt: { type: 'date', optional: false }, } - } -]; +}]; // Symbols are not supported on RN yet, so we use this for now: const specialPurposeRealmsKey = '_specialPurposeRealms'; @@ -81,7 +78,7 @@ function getSpecialPurposeRealm(user, realmName, schema) { _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. + // setTimeout call (when resolving the promise) to hang on RN iOS. // This might be related to our general makeCallback issue: #1255. setTimeout(() => {}, 1); @@ -108,7 +105,7 @@ function createInManagementRealm(user, modelName, modelInitializer) { return new Promise((resolve, reject) => { try { let o; - + const listener = () => { if (!o) { return; @@ -156,7 +153,7 @@ module.exports = { .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); } @@ -234,7 +231,7 @@ module.exports = { if (!token) { return Promise.reject(new Error('Offer token must be specified')); } - + const permissionOfferResponse = { id: generateUniqueId(), createdAt: new Date(), @@ -255,7 +252,7 @@ module.exports = { // 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"); } diff --git a/package.json b/package.json index c14bf2e3..012a4e95 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "realm", "description": "Realm is a mobile database: an alternative to SQLite and key-value stores", - "version": "2.0.0-rc10", + "version": "2.0.0-rc11", "license": "Apache-2.0", "homepage": "https://realm.io", "keywords": [ @@ -59,9 +59,9 @@ "jsdoc": "npm install && npm run jsdoc:clean && jsdoc -u docs/tutorials -p package.json -c docs/conf.json", "prenode-tests": "npm install --build-from-source=realm && cd tests && npm install", "node-tests": "cd tests && npm run test && cd ..", - "test-runner:ava": "cd tests/test-runners/ava && npm install && npm test", - "test-runner:mocha": "cd tests/test-runners/mocha && npm install && npm test", - "test-runner:jest": "cd tests/test-runners/jest && npm install && npm test", + "test-runner:ava": "cd tests/test-runners/ava && npm install --build-from-source=realm && npm test", + "test-runner:mocha": "cd tests/test-runners/mocha && npm install --build-from-source=realm && npm test", + "test-runner:jest": "cd tests/test-runners/jest && npm install --build-from-source=realm && npm test", "test-runners": "npm run test-runner:ava && npm run test-runner:mocha && npm run test-runner:jest", "isMac": "node -p \"if (process.platform == 'darwin') { process.exit(0); } else { process.exit(-1); }\"", "testMac": "npm run isMac -s && echo this is mac || echo . ", diff --git a/react-native/android/publish_android_template b/react-native/android/publish_android_template index c4c0670b..7d1e6686 100644 --- a/react-native/android/publish_android_template +++ b/react-native/android/publish_android_template @@ -22,17 +22,17 @@ apply plugin: 'com.android.library' task forwardDebugPort(type: Exec) { def adb = android.getAdbExe()?.toString() ?: 'false' - commandLine adb, 'forward', 'tcp:8082', 'tcp:8082' + commandLine adb, 'forward', 'tcp:8083', 'tcp:8083' ignoreExitValue true doLast { if (execResult.getExitValue() != 0) { logger.error( '===========================================================================\n' + - 'WARNING: Failed to automatically forward port 8082.\n' + - 'In order to use Realm in Chrome debugging mode, port 8082 must be forwarded\n' + + 'WARNING: Failed to automatically forward port 8083.\n' + + 'In order to use Realm in Chrome debugging mode, port 8083 must be forwarded\n' + 'from localhost to the device or emulator being used to run the application.\n' + 'You may need to add the appropriate flags to the command that failed:\n' + - ' adb forward tcp:8082 tcp:8082\n' + + ' adb forward tcp:8083 tcp:8083\n' + '===========================================================================\n' ) } diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java index 696b978d..0ad49a2a 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java @@ -23,7 +23,7 @@ import java.util.Map; import fi.iki.elonen.NanoHTTPD; class RealmReactModule extends ReactContextBaseJavaModule { - private static final int DEFAULT_PORT = 8082; + private static final int DEFAULT_PORT = 8083; private static boolean sentAnalytics = false; private AndroidWebServer webServer; diff --git a/react-native/ios/RealmReact/RealmReact.mm b/react-native/ios/RealmReact/RealmReact.mm index 62be7893..26da98e2 100644 --- a/react-native/ios/RealmReact/RealmReact.mm +++ b/react-native/ios/RealmReact/RealmReact.mm @@ -38,7 +38,7 @@ #import "GCDWebServerErrorResponse.h" #import "rpc.hpp" -#define WEB_SERVER_PORT 8082 +#define WEB_SERVER_PORT 8083 using namespace realm::rpc; #endif diff --git a/scripts/test.sh b/scripts/test.sh index 6fb10a4a..0aac32ec 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,7 +9,7 @@ export NPM_CONFIG_PROGRESS=false TARGET=$1 CONFIGURATION=${2:-Release} -if echo $CONFIGURATION | grep -i "^Debug$" > /dev/null ; then +if echo "$CONFIGURATION" | grep -i "^Debug$" > /dev/null ; then CONFIGURATION="Debug" fi @@ -17,8 +17,6 @@ IOS_SIM_DEVICE=${IOS_SIM_DEVICE:-} # use preferentially, otherwise will be set a ios_sim_default_device_type=${IOS_SIM_DEVICE_TYPE:-iPhone 5s} ios_sim_default_ios_version=${IOS_SIM_OS:-iOS 10.1} -ACCEPTED_LICENSES='MIT, ISC, BSD, Apache-2.0, BSD-2-Clause, BSD-3-Clause, WTFPL, Unlicense, (MIT AND CC-BY-3.0)' - PATH="/opt/android-sdk-linux/platform-tools:$PATH" SRCROOT=$(cd "$(dirname "$0")/.." && pwd) XCPRETTY=$(which xcpretty || true) @@ -130,13 +128,18 @@ xctest() { echo " done" # - Run the build and test + xcrun xcodebuild -scheme "$1" -configuration "$CONFIGURATION" -sdk iphonesimulator -destination id="$IOS_SIM_DEVICE" build || { + EXITCODE=$? + echo "*** Failure (exit code $EXITCODE). ***" + exit $EXITCODE + } if [ -n "$XCPRETTY" ]; then log_temp=$(mktemp build.log.XXXXXX) if [ -e "$log_temp" ]; then rm "$log_temp" fi - xcrun xcodebuild -scheme "$1" -configuration "$CONFIGURATION" -sdk iphonesimulator -destination name="iPhone 5s" build test 2>&1 | tee "$log_temp" | "$XCPRETTY" -c --no-utf --report junit --output build/reports/junit.xml || { - EXITCODE=$? + xcrun xcodebuild -scheme "$1" -configuration "$CONFIGURATION" -sdk iphonesimulator -destination name="iPhone 5s" test 2>&1 | tee "$log_temp" | "$XCPRETTY" -c --no-utf --report junit --output build/reports/junit.xml || { + EXITCODE=$? printf "*** Xcode Failure (exit code %s). The full xcode log follows: ***\n\n" "$EXITCODE" cat "$log_temp" printf "\n\n*** End Xcode Failure ***\n" @@ -144,11 +147,11 @@ xctest() { } rm "$log_temp" else - xcrun xcodebuild -scheme "$1" -configuration "$CONFIGURATION" -sdk iphonesimulator -destination id="$IOS_SIM_DEVICE" build test || { - EXITCODE=$? + xcrun xcodebuild -scheme "$1" -configuration "$CONFIGURATION" -sdk iphonesimulator -destination id="$IOS_SIM_DEVICE" test || { + EXITCODE=$? echo "*** Failure (exit code $EXITCODE). ***" exit $EXITCODE - } + } fi } @@ -240,14 +243,22 @@ cleanup trap cleanup EXIT # Use a consistent version of Node if possible. -if [ -f "$NVM_DIR/nvm.sh" ]; then - . "$NVM_DIR/nvm.sh" -elif [ -x "$(command -v brew)" ] && [ -f "$(brew --prefix nvm)/nvm.sh" ]; then - # we must be on mac and nvm was installed with brew - # TODO: change the mac slaves to use manual nvm installation - . "$(brew --prefix nvm)/nvm.sh" +if [[ -z "$(command -v nvm)" ]]; then + set +e + if [ -f "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" '' || true + elif [ -x "$(command -v brew)" ] && [ -f "$(brew --prefix nvm)/nvm.sh" ]; then + # we must be on mac and nvm was installed with brew + # TODO: change the mac slaves to use manual nvm installation + . "$(brew --prefix nvm)/nvm.sh" '' || true + elif [ -f "$HOME/.nvm/nvm.sh" ]; then + . ~/.nvm/nvm.sh '' + fi + set -e +fi +if [[ "$(command -v nvm)" ]]; then + nvm install 7.10.0 fi -[[ "$(command -v nvm)" ]] && nvm install 7.10.0 && nvm use 7.10.0 || true # Remove cached packages rm -rf ~/.yarn-cache/npm-realm-* @@ -406,8 +417,6 @@ case "$TARGET" in ;; "test-runners") npm run check-environment - # Create a fake realm module that points to the source root so that test-runner tests can require('realm') - npm install --build-from-source=realm npm run test-runners ;; "all") diff --git a/src/js_class.hpp b/src/js_class.hpp index 70b28e2e..052ec8f5 100644 --- a/src/js_class.hpp +++ b/src/js_class.hpp @@ -33,6 +33,29 @@ using ConstructorType = void(typename T::Context, typename T::Object, size_t, co template using MethodType = void(typename T::Context, typename T::Function, typename T::Object, size_t, const typename T::Value[], ReturnValue &); +template +struct Arguments { + const typename T::Context ctx; + const size_t count; + const typename T::Value* const value; + + typename T::Value operator[](size_t index) const noexcept { + if (index >= count) { + return Value::from_undefined(ctx); + } + return value[index]; + } + + void validate_maximum(size_t max) const { + if (max < count) { + throw std::invalid_argument(util::format("Invalid arguments: at most %1 expected, but %2 supplied.", max, count)); + } + } +}; + +template +using ArgumentsMethodType = void(typename T::Context, typename T::Function, typename T::Object, Arguments, ReturnValue &); + template struct PropertyType { using GetterType = void(typename T::Context, typename T::Object, ReturnValue &); diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 5f22c677..90981bb9 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -123,9 +123,7 @@ class RealmDelegate : public BindingContext { } ObjectType realm_object = create_object>(m_context, new SharedRealm(realm)); - ValueType arguments[2]; - arguments[0] = realm_object; - arguments[1] = Value::from_string(m_context, notification_name); + ValueType arguments[] = {realm_object, Value::from_string(m_context, notification_name)}; std::list> notifications_copy(m_notifications); for (auto &callback : notifications_copy) { @@ -148,6 +146,7 @@ class RealmClass : public ClassDefinition> { using FunctionType = typename T::Function; using ObjectType = typename T::Object; using ValueType = typename T::Value; + using Arguments = js::Arguments; using String = js::String; using Object = js::Object; using Value = js::Value; @@ -165,22 +164,22 @@ public: static FunctionType create_constructor(ContextType); // methods - static void objects(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void object_for_primary_key(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void create(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - 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 &); - static void delete_model(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void objects(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void object_for_primary_key(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void create(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void delete_one(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void delete_all(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void write(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void begin_transaction(ContextType, FunctionType, ObjectType, Arguments, ReturnValue&); + static void commit_transaction(ContextType, FunctionType, ObjectType, Arguments, ReturnValue&); + static void cancel_transaction(ContextType, FunctionType, ObjectType, Arguments, ReturnValue&); + static void add_listener(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void wait_for_download_completion(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void remove_listener(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void remove_all_listeners(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void close(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void compact(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void delete_model(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -198,10 +197,10 @@ public: static void constructor(ContextType, ObjectType, size_t, const ValueType[]); static SharedRealm create_shared_realm(ContextType, realm::Realm::Config, bool, ObjectDefaultsMap &&, ConstructorMap &&); - 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 void schema_version(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void clear_test_state(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void copy_bundled_realm_files(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); + static void delete_file(ContextType, FunctionType, ObjectType, Arguments, ReturnValue &); // static properties static void get_default_path(ContextType, ObjectType, ReturnValue &); @@ -398,7 +397,7 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t static const String schema_string = "schema"; ValueType schema_value = Object::get_property(ctx, object, schema_string); if (!Value::is_undefined(ctx, schema_value)) { - ObjectType schema_object = Value::validated_to_object(ctx, schema_value, "schema"); + ObjectType schema_object = Value::validated_to_array(ctx, schema_value, "schema"); config.schema.emplace(Schema::parse_schema(ctx, schema_object, defaults, constructors)); schema_updated = true; } @@ -504,13 +503,13 @@ SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Co } template -void RealmClass::schema_version(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1, 2); +void RealmClass::schema_version(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(2); realm::Realm::Config config; - config.path = normalize_realm_path(Value::validated_to_string(ctx, arguments[0])); - if (argc == 2) { - auto encryption_key = Value::validated_to_binary(ctx, arguments[1], "encryptionKey"); + config.path = normalize_realm_path(Value::validated_to_string(ctx, args[0])); + if (args.count == 2) { + auto encryption_key = Value::validated_to_binary(ctx, args[1], "encryptionKey"); config.encryption_key.assign(encryption_key.data(), encryption_key.data() + encryption_key.size()); } @@ -525,23 +524,22 @@ void RealmClass::schema_version(ContextType ctx, FunctionType, ObjectType thi template -void RealmClass::clear_test_state(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); - +void RealmClass::clear_test_state(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); js::clear_test_state(); } template -void RealmClass::copy_bundled_realm_files(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +void RealmClass::copy_bundled_realm_files(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); 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); +void RealmClass::delete_file(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); - ValueType value = arguments[0]; + ValueType value = args[0]; if (!Value::is_object(ctx, value)) { throw std::runtime_error("Invalid argument, expected a Realm configuration object"); } @@ -569,9 +567,9 @@ void RealmClass::delete_file(ContextType ctx, FunctionType, ObjectType this_o } template -void RealmClass::delete_model(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - ValueType value = arguments[0]; +void RealmClass::delete_model(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); + ValueType value = args[0]; SharedRealm& realm = *get_internal>(this_object); @@ -643,14 +641,14 @@ void RealmClass::get_sync_session(ContextType ctx, ObjectType object, ReturnV #endif template -void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 2, 3); - auto config_object = Value::validated_to_object(ctx, arguments[0]); - auto callback_function = Value::validated_to_function(ctx, arguments[argc - 1]); +void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(3); + auto config_object = Value::validated_to_object(ctx, args[0]); + auto callback_function = Value::validated_to_function(ctx, args[1 + (args.count == 3)]); ValueType session_callback = Value::from_null(ctx); - if (argc == 3) { - session_callback = Value::validated_to_function(ctx, arguments[1]); + if (args.count == 3) { + session_callback = Value::validated_to_function(ctx, args[1]); } #if REALM_ENABLE_SYNC @@ -754,31 +752,33 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, return; } } +#else + static_cast(config_object); #endif Function::callback(ctx, callback_function, this_object, 0, nullptr); } template -void RealmClass::objects(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); +void RealmClass::objects(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); std::string object_type; - validated_object_schema_for_value(ctx, realm, arguments[0], object_type); + validated_object_schema_for_value(ctx, realm, args[0], object_type); return_value.set(ResultsClass::create_instance(ctx, realm, object_type)); } template -void RealmClass::object_for_primary_key(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 2); +void RealmClass::object_for_primary_key(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(2); SharedRealm realm = *get_internal>(this_object); std::string object_type; - auto &object_schema = validated_object_schema_for_value(ctx, realm, arguments[0], object_type); + auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type); NativeAccessor accessor(ctx, realm, object_schema); - auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, arguments[1]); + auto realm_object = realm::Object::get_for_primary_key(accessor, realm, object_schema, args[1]); if (realm_object.is_valid()) { return_value.set(RealmObjectClass::create_instance(ctx, std::move(realm_object))); @@ -789,21 +789,22 @@ void RealmClass::object_for_primary_key(ContextType ctx, FunctionType, Object } template -void RealmClass::create(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 2, 3); +void RealmClass::create(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(3); SharedRealm realm = *get_internal>(this_object); + realm->verify_open(); std::string object_type; - auto &object_schema = validated_object_schema_for_value(ctx, realm, arguments[0], object_type); + auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0], object_type); - ObjectType object = Value::validated_to_object(ctx, arguments[1], "properties"); - if (Value::is_array(ctx, arguments[1])) { + ObjectType object = Value::validated_to_object(ctx, args[1], "properties"); + if (Value::is_array(ctx, args[1])) { object = Schema::dict_for_property_array(ctx, object_schema, object); } bool update = false; - if (argc == 3) { - update = Value::validated_to_boolean(ctx, arguments[2], "update"); + if (args.count == 3) { + update = Value::validated_to_boolean(ctx, args[2], "update"); } NativeAccessor accessor(ctx, realm, object_schema); @@ -812,15 +813,16 @@ void RealmClass::create(ContextType ctx, FunctionType, ObjectType this_object } template -void RealmClass::delete_one(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); +void RealmClass::delete_one(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); + realm->verify_open(); if (!realm->is_in_transaction()) { throw std::runtime_error("Can only delete objects within a transaction."); } - ObjectType arg = Value::validated_to_object(ctx, arguments[0]); + ObjectType arg = Value::validated_to_object(ctx, args[0], "object"); if (Object::template is_instance>(ctx, arg)) { auto object = get_internal>(arg); @@ -859,10 +861,11 @@ void RealmClass::delete_one(ContextType ctx, FunctionType, ObjectType this_ob } template -void RealmClass::delete_all(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +void RealmClass::delete_all(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); + realm->verify_open(); if (!realm->is_in_transaction()) { throw std::runtime_error("Can only delete objects within a transaction."); @@ -874,18 +877,18 @@ void RealmClass::delete_all(ContextType ctx, FunctionType, ObjectType this_ob } template -void RealmClass::write(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); +void RealmClass::write(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); - FunctionType callback = Value::validated_to_function(ctx, arguments[0]); + FunctionType callback = Value::validated_to_function(ctx, args[0]); realm->begin_transaction(); try { Function::call(ctx, callback, this_object, 0, nullptr); } - catch (std::exception &e) { + catch (...) { realm->cancel_transaction(); throw; } @@ -894,82 +897,76 @@ void RealmClass::write(ContextType ctx, FunctionType, ObjectType this_object, } 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); +void RealmClass::begin_transaction(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(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); +void RealmClass::commit_transaction(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(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); +void RealmClass::cancel_transaction(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(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); +void RealmClass::add_listener(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(2); - validated_notification_name(ctx, arguments[0]); - auto callback = Value::validated_to_function(ctx, arguments[1]); + validated_notification_name(ctx, args[0]); + auto callback = Value::validated_to_function(ctx, args[1]); SharedRealm realm = *get_internal>(this_object); - if (realm->is_closed()) { - throw ClosedRealmException(); - } + realm->verify_open(); get_delegate(realm.get())->add_notification(callback); } template -void RealmClass::remove_listener(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 2); +void RealmClass::remove_listener(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(2); - validated_notification_name(ctx, arguments[0]); - auto callback = Value::validated_to_function(ctx, arguments[1]); + validated_notification_name(ctx, args[0]); + auto callback = Value::validated_to_function(ctx, args[1]); SharedRealm realm = *get_internal>(this_object); - if (realm->is_closed()) { - throw ClosedRealmException(); - } + realm->verify_open(); get_delegate(realm.get())->remove_notification(callback); } template -void RealmClass::remove_all_listeners(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0, 1); - if (argc) { - validated_notification_name(ctx, arguments[0]); +void RealmClass::remove_all_listeners(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); + if (args.count) { + validated_notification_name(ctx, args[0]); } SharedRealm realm = *get_internal>(this_object); - if (realm->is_closed()) { - throw ClosedRealmException(); - } + realm->verify_open(); get_delegate(realm.get())->remove_all_notifications(); } template -void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); SharedRealm realm = *get_internal>(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); +void RealmClass::compact(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); if (realm->is_in_transaction()) { diff --git a/src/js_schema.hpp b/src/js_schema.hpp index e96e0290..51ef3aa5 100644 --- a/src/js_schema.hpp +++ b/src/js_schema.hpp @@ -180,9 +180,9 @@ ObjectSchema Schema::parse_object_schema(ContextType ctx, ObjectType object_s ObjectDefaults object_defaults; ObjectSchema object_schema; - object_schema.name = Object::validated_get_string(ctx, object_schema_object, name_string); + object_schema.name = Object::validated_get_string(ctx, object_schema_object, name_string, "ObjectSchema"); - ObjectType properties_object = Object::validated_get_object(ctx, object_schema_object, properties_string, "ObjectSchema must have a 'properties' object."); + ObjectType properties_object = Object::validated_get_object(ctx, object_schema_object, properties_string, "ObjectSchema"); if (Value::is_array(ctx, properties_object)) { uint32_t length = Object::validated_get_length(ctx, properties_object); for (uint32_t i = 0; i < length; i++) { @@ -233,7 +233,8 @@ ObjectSchema Schema::parse_object_schema(ContextType ctx, ObjectType object_s } template -realm::Schema Schema::parse_schema(ContextType ctx, ObjectType schema_object, ObjectDefaultsMap &defaults, ConstructorMap &constructors) { +realm::Schema Schema::parse_schema(ContextType ctx, ObjectType schema_object, + ObjectDefaultsMap &defaults, ConstructorMap &constructors) { std::vector schema; uint32_t length = Object::validated_get_length(ctx, schema_object); diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 0ff96000..5d7c1d72 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include "event_loop_dispatcher.hpp" #include "platform.hpp" @@ -240,6 +242,98 @@ private: const Protected m_func; }; + +// An object of type SSLVerifyCallbackSyncThreadFunctor is registered with the sync client in order +// to verify SSL certificates. The SSLVerifyCallbackSyncThreadFunctor object's operator() is called +// on the sync client's event loop thread. +template +class SSLVerifyCallbackSyncThreadFunctor { +public: + SSLVerifyCallbackSyncThreadFunctor(typename T::Context ctx, typename T::Function ssl_verify_func) + : m_ctx(Context::get_global_context(ctx)) + , m_func(ctx, ssl_verify_func) + , m_event_loop_dispatcher {SSLVerifyCallbackSyncThreadFunctor::main_loop_handler} + , m_mutex{new std::mutex} + , m_cond_var{new std::condition_variable} + { + } + + // This function is called on the sync client's event loop thread. + bool operator ()(const std::string& server_address, sync::Session::port_type server_port, const char* pem_data, size_t pem_size, int preverify_ok, int depth) + { + const std::string pem_certificate {pem_data, pem_size}; + + { + std::lock_guard lock {*m_mutex}; + m_ssl_certificate_callback_done = false; + } + + // Dispatch the call to the main_loop_handler on the node.js thread. + m_event_loop_dispatcher(this, server_address, server_port, pem_certificate, preverify_ok, depth); + + bool ssl_certificate_accepted = false; + { + // Wait for the return value of the callback function on the node.js main thread. + // The sync client blocks during this wait. + std::unique_lock lock(*m_mutex); + m_cond_var->wait(lock, [this] { return this->m_ssl_certificate_callback_done; }); + ssl_certificate_accepted = m_ssl_certificate_accepted; + } + + return ssl_certificate_accepted; + } + + // main_loop_handler is called on the node.js main thread. + // main_loop_handler calls the user callback (m_func) and sends the return value + // back to the sync client's event loop thread through a condition variable. + static void main_loop_handler(SSLVerifyCallbackSyncThreadFunctor* this_object, + const std::string& server_address, + sync::Session::port_type server_port, + const std::string& pem_certificate, + int preverify_ok, + int depth) + { + HANDLESCOPE + + const Protected& ctx = this_object->m_ctx; + + typename T::Object ssl_certificate_object = Object::create_empty(ctx); + Object::set_property(ctx, ssl_certificate_object, "serverAddress", Value::from_string(ctx, server_address)); + Object::set_property(ctx, ssl_certificate_object, "serverPort", Value::from_number(ctx, double(server_port))); + Object::set_property(ctx, ssl_certificate_object, "pemCertificate", Value::from_string(ctx, pem_certificate)); + Object::set_property(ctx, ssl_certificate_object, "acceptedByOpenSSL", Value::from_boolean(ctx, preverify_ok != 0)); + Object::set_property(ctx, ssl_certificate_object, "depth", Value::from_number(ctx, double(depth))); + + const int argc = 1; + typename T::Value arguments[argc] = { ssl_certificate_object }; + typename T::Value ret_val = Function::callback(ctx, this_object->m_func, typename T::Object(), 1, arguments); + bool ret_val_bool = Value::to_boolean(ctx, ret_val); + + { + std::lock_guard lock {*this_object->m_mutex}; + this_object->m_ssl_certificate_callback_done = true; + this_object->m_ssl_certificate_accepted = ret_val_bool; + } + + this_object->m_cond_var->notify_one(); + }; + + +private: + const Protected m_ctx; + const Protected m_func; + EventLoopDispatcher* this_object, + const std::string& server_address, + sync::Session::port_type server_port, + const std::string& pem_certificate, + int preverify_ok, + int depth)> m_event_loop_dispatcher; + bool m_ssl_certificate_callback_done = false; + bool m_ssl_certificate_accepted = false; + std::shared_ptr m_mutex; + std::shared_ptr m_cond_var; +}; + template void UserClass::session_for_on_disk_path(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { auto user = *get_internal>(this_object); @@ -332,7 +426,7 @@ void SessionClass::add_progress_notification(ContextType ctx, FunctionType, O validate_argument_count(argc, 3); if (auto session = get_internal>(this_object)->lock()) { - + std::string direction = Value::validated_to_string(ctx, arguments[0], "direction"); std::string mode = Value::validated_to_string(ctx, arguments[1], "mode"); SyncSession::NotifierType notifierType; @@ -362,21 +456,21 @@ void SessionClass::add_progress_notification(ContextType ctx, FunctionType, O Protected protected_callback(ctx, callback_function); Protected protected_this(ctx, this_object); Protected protected_ctx(Context::get_global_context(ctx)); - std::function progressFunc; + std::function progressFunc; EventLoopDispatcher progress_handler([=](uint64_t transferred_bytes, uint64_t transferrable_bytes) { HANDLESCOPE ValueType callback_arguments[2]; callback_arguments[0] = Value::from_number(protected_ctx, transferred_bytes); callback_arguments[1] = Value::from_number(protected_ctx, transferrable_bytes); - + Function::callback(protected_ctx, protected_callback, typename T::Object(), 2, callback_arguments); }); progressFunc = std::move(progress_handler); - - auto registrationToken = session->register_progress_notifier(std::move(progressFunc), notifierType, false); + + auto registrationToken = session->register_progress_notifier(std::move(progressFunc), notifierType, is_streaming); auto syncSession = create_object>(ctx, new WeakSession(session)); PropertyAttributes attributes = ReadOnly | DontEnum | DontDelete; @@ -511,7 +605,7 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr static std::regex tilde("/~/"); raw_realm_url = std::regex_replace(raw_realm_url, tilde, "/__auth/"); } - + bool client_validate_ssl = true; ValueType validate_ssl_temp = Object::get_property(ctx, sync_config_object, "validate_ssl"); if (!Value::is_undefined(ctx, validate_ssl_temp)) { @@ -527,12 +621,23 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr ssl_trust_certificate_path = util::none; } + std::function ssl_verify_callback; + ValueType ssl_verify_func = Object::get_property(ctx, sync_config_object, "open_ssl_verify_callback"); + if (!Value::is_undefined(ctx, ssl_verify_func)) { + SSLVerifyCallbackSyncThreadFunctor ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)}; + ssl_verify_callback = std::move(ssl_verify_functor); + } + // FIXME - use make_shared config.sync_config = std::shared_ptr(new SyncConfig{shared_user, raw_realm_url, SyncSessionStopPolicy::AfterChangesUploaded, std::move(bind), std::move(error_handler), nullptr, util::none, - client_validate_ssl, ssl_trust_certificate_path}); + client_validate_ssl, ssl_trust_certificate_path, + std::move(ssl_verify_callback)}); + + + config.schema_mode = SchemaMode::Additive; config.path = realm::SyncManager::shared().path_for_realm(*shared_user, raw_realm_url); diff --git a/src/js_types.hpp b/src/js_types.hpp index e1cf6459..4f20b6cf 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -27,6 +27,7 @@ #include #include +#include #include #if defined(__GNUC__) && !(defined(DEBUG) && DEBUG) @@ -223,7 +224,7 @@ struct Object { return Value::validated_to_##type(ctx, get_property(ctx, object, key), std::string(key).c_str()); \ } \ catch (std::invalid_argument &e) { \ - throw message ? std::invalid_argument(message) : e; \ + throw message ? std::invalid_argument(util::format("Failed to read %1: %2", message, e.what())) : e; \ } \ } \ static return_t validated_get_##type(ContextType ctx, const ObjectType &object, uint32_t index, const char *message = nullptr) { \ @@ -231,7 +232,7 @@ struct Object { return Value::validated_to_##type(ctx, get_property(ctx, object, index)); \ } \ catch (std::invalid_argument &e) { \ - throw message ? std::invalid_argument(message) : e; \ + throw message ? std::invalid_argument(util::format("Failed to read %1: %2", message, e.what())) : e; \ } \ } diff --git a/src/jsc/jsc_class.hpp b/src/jsc/jsc_class.hpp index 6e6f7618..8accc341 100644 --- a/src/jsc/jsc_class.hpp +++ b/src/jsc/jsc_class.hpp @@ -30,7 +30,9 @@ template using ClassDefinition = js::ClassDefinition; using ConstructorType = js::ConstructorType; +using ArgumentsMethodType = js::ArgumentsMethodType; using MethodType = js::MethodType; +using Arguments = js::Arguments; using PropertyType = js::PropertyType; using IndexPropertyType = js::IndexPropertyType; using StringPropertyType = js::StringPropertyType; @@ -369,6 +371,19 @@ JSValueRef wrap(JSContextRef ctx, JSObjectRef function, JSObjectRef this_object, } } +template +JSValueRef wrap(JSContextRef ctx, JSObjectRef function, JSObjectRef this_object, size_t argc, const JSValueRef arguments[], JSValueRef* exception) { + jsc::ReturnValue return_value(ctx); + try { + F(ctx, function, this_object, jsc::Arguments{ctx, argc, arguments}, return_value); + return return_value; + } + catch (std::exception &e) { + *exception = jsc::Exception::value(ctx, e); + return nullptr; + } +} + template JSValueRef wrap(JSContextRef ctx, JSObjectRef object, JSStringRef property, JSValueRef* exception) { jsc::ReturnValue return_value(ctx); diff --git a/src/node/node_class.hpp b/src/node/node_class.hpp index 21cddad6..2b1c7557 100644 --- a/src/node/node_class.hpp +++ b/src/node/node_class.hpp @@ -31,6 +31,8 @@ using ClassDefinition = js::ClassDefinition; using ConstructorType = js::ConstructorType; using MethodType = js::MethodType; +using ArgumentsMethodType = js::ArgumentsMethodType; +using Arguments = js::Arguments; using PropertyType = js::PropertyType; using IndexPropertyType = js::IndexPropertyType; using StringPropertyType = js::StringPropertyType; @@ -308,6 +310,21 @@ void wrap(const v8::FunctionCallbackInfo& info) { } } +template +void wrap(const v8::FunctionCallbackInfo& info) { + v8::Isolate* isolate = info.GetIsolate(); + node::ReturnValue return_value(info.GetReturnValue()); + auto arguments = node::get_arguments(info); + + try { + F(isolate, info.Callee(), info.This(), node::Arguments{isolate, arguments.size(), arguments.data()}, return_value); + } + catch (std::exception &e) { + Nan::ThrowError(node::Exception::value(isolate, e)); + } +} + + template void wrap(v8::Local property, const v8::PropertyCallbackInfo& info) { v8::Isolate* isolate = info.GetIsolate(); diff --git a/tests/js/asserts.js b/tests/js/asserts.js index a3a3026e..95f3378c 100644 --- a/tests/js/asserts.js +++ b/tests/js/asserts.js @@ -120,6 +120,23 @@ module.exports = { } }, + assertThrowsContaining: function(func, expectedMessage) { + var caught = false; + try { + func(); + } + catch (e) { + caught = true; + if (!e.message.includes(expectedMessage)) { + throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown - instead caught: "${e}"`); + } + } + + if (!caught) { + throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown`); + } + }, + assertTrue: function(condition, errorMessage) { if (!condition) { throw new TestFailureError(errorMessage || `Condition ${condition} expected to be true`); diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index c1bf831e..5f33e95d 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -54,13 +54,25 @@ function wait(t) { return new Promise(resolve => setTimeout(resolve, t)); } +function repeatUntil(fn, predicate) { + let retries = 0 + function check() { + if (retries > 3) { + return Promise.reject(new Error("operation timed out")); + } + ++retries; + return fn().then(x => predicate(x) ? x : wait(100).then(check)); + } + return check; +} + module.exports = { testApplyAndGetGrantedPermissions() { return createUsersWithTestRealms(1) .then(([user]) => { return user.applyPermissions({ userId: '*' }, `/${user.identity}/test`, 'read') - .then(wait(100)) - .then(() => user.getGrantedPermissions('any')) + .then(repeatUntil(() => user.getGrantedPermissions('any'), + permissions => permissions.length > 1)) .then(permissions => { TestCase.assertEqual(permissions[1].path, `/${user.identity}/test`); TestCase.assertEqual(permissions[1].mayRead, true); @@ -77,17 +89,19 @@ module.exports = { .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); - }); + return realmUrl; + }) + .then(repeatUntil(() => user2.getGrantedPermissions('any'), + permissions => permissions.length > 1)) + .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]) => { diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 791d130f..251e8e8d 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -18,9 +18,9 @@ 'use strict'; -var Realm = require('realm'); -var TestCase = require('./asserts'); -var schemas = require('./schemas'); +const Realm = require('realm'); +const TestCase = require('./asserts'); +const schemas = require('./schemas'); let pathSeparator = '/'; if (typeof process === 'object' && process.platform === 'win32') { @@ -29,7 +29,7 @@ if (typeof process === 'object' && process.platform === 'win32') { module.exports = { testRealmConstructor: function() { - var realm = new Realm({schema: []}); + const realm = new Realm({schema: []}); TestCase.assertTrue(realm instanceof Realm); TestCase.assertEqual(typeof Realm, 'function'); @@ -37,52 +37,43 @@ module.exports = { }, testRealmConstructorPath: function() { - TestCase.assertThrows(function() { - new Realm(''); - }, 'Realm cannot be created with an invalid path'); - TestCase.assertThrows(function() { - new Realm('test1.realm', 'invalidArgument'); - }, 'Realm constructor can only have 0 or 1 argument(s)'); + TestCase.assertThrows(() => new Realm('')); // the message for this error is platform-specific + TestCase.assertThrowsContaining(() => new Realm('test1.realm', 'invalidArgument'), + "Invalid arguments when constructing 'Realm'"); - var defaultRealm = new Realm({schema: []}); + const defaultRealm = new Realm({schema: []}); TestCase.assertEqual(defaultRealm.path, Realm.defaultPath); - var defaultRealm2 = new Realm(); + const defaultRealm2 = new Realm(); TestCase.assertEqual(defaultRealm2.path, Realm.defaultPath); - var defaultDir = Realm.defaultPath.substring(0, Realm.defaultPath.lastIndexOf(pathSeparator) + 1) - var testPath = 'test1.realm'; - var realm = new Realm({schema: [], path: testPath}); + const defaultDir = Realm.defaultPath.substring(0, Realm.defaultPath.lastIndexOf(pathSeparator) + 1); + const testPath = 'test1.realm'; + const realm = new Realm({schema: [], path: testPath}); TestCase.assertEqual(realm.path, defaultDir + testPath); - var testPath2 = 'test2.realm'; - var realm2 = new Realm({schema: [], path: testPath2}); + const testPath2 = 'test2.realm'; + const realm2 = new Realm({schema: [], path: testPath2}); TestCase.assertEqual(realm2.path, defaultDir + testPath2); }, testRealmConstructorSchemaVersion: function() { - var defaultRealm = new Realm({schema: []}); + const defaultRealm = new Realm({schema: []}); TestCase.assertEqual(defaultRealm.schemaVersion, 0); - TestCase.assertThrows(function() { - new Realm({schemaVersion: 1, schema: []}); - }, "Realm already opened at a different schema version"); + TestCase.assertThrowsContaining(() => new Realm({schemaVersion: 1, schema: []}), + "already opened with different schema version."); TestCase.assertEqual(new Realm().schemaVersion, 0); TestCase.assertEqual(new Realm({schemaVersion: 0}).schemaVersion, 0); - var realm = new Realm({path: 'test1.realm', schema: [], schemaVersion: 1}); + let realm = new Realm({path: 'test1.realm', schema: [], schemaVersion: 1}); TestCase.assertEqual(realm.schemaVersion, 1); TestCase.assertEqual(realm.schema.length, 0); realm.close(); - // FIXME - enable once realm initialization supports schema comparison - // TestCase.assertThrows(function() { - // realm = new Realm({path: testPath, schema: [schemas.TestObject], schemaVersion: 1}); - // }, "schema changes require updating the schema version"); - realm = new Realm({path: 'test1.realm', schema: [schemas.TestObject], schemaVersion: 2}); - realm.write(function() { + realm.write(() => { realm.create('TestObject', {doubleCol: 1}); }); TestCase.assertEqual(realm.objects('TestObject')[0].doubleCol, 1); @@ -91,41 +82,31 @@ module.exports = { }, testRealmConstructorDynamicSchema: function() { - var realm = new Realm({schema: [schemas.TestObject]}); - realm.write(function() { + let realm = new Realm({schema: [schemas.TestObject]}); + realm.write(() => { realm.create('TestObject', [1]) }); realm.close(); realm = new Realm(); - var objects = realm.objects('TestObject'); + const objects = realm.objects('TestObject'); TestCase.assertEqual(objects.length, 1); TestCase.assertEqual(objects[0].doubleCol, 1.0); }, testRealmConstructorSchemaValidation: function() { - TestCase.assertThrows(function() { - new Realm({schema: schemas.AllTypes}); - }, 'The schema should be an array'); - - TestCase.assertThrows(function() { - new Realm({schema: ['SomeType']}); - }, 'The schema should be an array of objects'); - - TestCase.assertThrows(function() { - new Realm({schema: [{}]}); - }, 'The schema should be an array of ObjectSchema objects'); - - TestCase.assertThrows(function() { - new Realm({schema: [{name: 'SomeObject'}]}); - }, 'The schema should be an array of ObjectSchema objects'); - - TestCase.assertThrows(function() { - new Realm({schema: [{properties: {intCol: 'int'}}]}); - }, 'The schema should be an array of ObjectSchema objects'); + TestCase.assertThrowsContaining(() => new Realm({schema: schemas.AllTypes}), "schema must be of type 'array', got"); + TestCase.assertThrowsContaining(() => new Realm({schema: ['SomeType']}), + "Failed to read ObjectSchema: JS value must be of type 'object', got (SomeType)"); + TestCase.assertThrowsContaining(() => new Realm({schema: [{}]}), + "Failed to read ObjectSchema: name must be of type 'string', got (undefined)"); + TestCase.assertThrowsContaining(() => new Realm({schema: [{name: 'SomeObject'}]}), + "Failed to read ObjectSchema: properties must be of type 'object', got (undefined)"); + TestCase.assertThrowsContaining(() => new Realm({schema: [{properties: {intCol: 'int'}}]}), + "Failed to read ObjectSchema: name must be of type 'string', got (undefined)"); // linkingObjects property where the source property is missing - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { new Realm({schema: [{ name: 'InvalidObject', properties: { @@ -135,7 +116,7 @@ module.exports = { }, "Property 'InvalidObject.nosuchproperty' declared as origin of linking objects property 'InvalidObject.linkingObjects' does not exist"); // linkingObjects property where the source property is not a link - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { new Realm({schema: [{ name: 'InvalidObject', properties: { @@ -146,7 +127,7 @@ module.exports = { }, "Property 'InvalidObject.integer' declared as origin of linking objects property 'InvalidObject.linkingObjects' is not a link") // linkingObjects property where the source property links elsewhere - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { new Realm({schema: [{ name: 'InvalidObject', properties: { @@ -165,7 +146,7 @@ module.exports = { testRealmConstructorInMemory: function() { // open in-memory realm instance const realm1 = new Realm({inMemory: true, schema: [schemas.TestObject]}); - realm1.write(function() { + realm1.write(() => { realm1.create('TestObject', [1]) }); TestCase.assertEqual(realm1.inMemory, true); @@ -187,28 +168,25 @@ module.exports = { TestCase.assertEqual(realm3.schema.length, 0); // try to open the same realm in persistent mode (should fail as you cannot mix modes) - TestCase.assertThrows(function() { - const realm4 = new Realm({}); - }); + TestCase.assertThrowsContaining(() => new Realm({}), 'already opened with different inMemory settings.'); }, testRealmConstructorReadOnly: function() { - var realm = new Realm({schema: [schemas.TestObject]}); - realm.write(function() { + let realm = new Realm({schema: [schemas.TestObject]}); + realm.write(() => { realm.create('TestObject', [1]) }); TestCase.assertEqual(realm.readOnly, false); realm.close(); realm = new Realm({readOnly: true, schema: [schemas.TestObject]}); - var objects = realm.objects('TestObject'); + const objects = realm.objects('TestObject'); TestCase.assertEqual(objects.length, 1); TestCase.assertEqual(objects[0].doubleCol, 1.0); TestCase.assertEqual(realm.readOnly, true); - TestCase.assertThrows(function() { - realm.write(function() {}); - }); + TestCase.assertThrowsContaining(() => realm.write(() => {}), + "Can't perform transactions on read-only Realms."); realm.close(); realm = new Realm({readOnly: true}); @@ -217,12 +195,12 @@ module.exports = { }, testDefaultPath: function() { - var defaultPath = Realm.defaultPath; - var defaultRealm = new Realm({schema: []}); + const defaultPath = Realm.defaultPath; + let defaultRealm = new Realm({schema: []}); TestCase.assertEqual(defaultRealm.path, Realm.defaultPath); try { - var newPath = Realm.defaultPath.substring(0, defaultPath.lastIndexOf(pathSeparator) + 1) + 'default2.realm'; + const newPath = `${Realm.defaultPath.substring(0, defaultPath.lastIndexOf(pathSeparator) + 1)}default2.realm`; Realm.defaultPath = newPath; defaultRealm = new Realm({schema: []}); TestCase.assertEqual(defaultRealm.path, newPath, "should use updated default realm path"); @@ -235,7 +213,7 @@ module.exports = { testRealmSchemaVersion: function() { TestCase.assertEqual(Realm.schemaVersion(Realm.defaultPath), -1); - var realm = new Realm({schema: []}); + let realm = new Realm({schema: []}); TestCase.assertEqual(realm.schemaVersion, 0); TestCase.assertEqual(Realm.schemaVersion(Realm.defaultPath), 0); @@ -245,69 +223,64 @@ module.exports = { }, testRealmWrite: function() { - var realm = new Realm({schema: [schemas.IntPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); + const realm = new Realm({schema: [schemas.IntPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); // exceptions should be propogated - TestCase.assertThrows(function() { - realm.write(function() { - realm.invalid(); - }); - }); + TestCase.assertThrowsContaining(() => realm.write(() => { throw new Error('Inner exception message'); }), + 'Inner exception message'); // writes should be possible after caught exception - realm.write(function() { + realm.write(() => { realm.create('TestObject', {doubleCol: 1}); }); TestCase.assertEqual(1, realm.objects('TestObject').length); - realm.write(function() { + realm.write(() => { // nested transactions not supported - TestCase.assertThrows(function() { - realm.write(function() {}); - }); + TestCase.assertThrowsContaining(() => realm.write(() => {}), + 'The Realm is already in a write transaction'); }); }, testRealmCreate: function() { - var realm = new Realm({schema: [schemas.TestObject]}); + const realm = new Realm({schema: [schemas.TestObject]}); - TestCase.assertThrows(function() { - realm.create('TestObject', {doubleCol: 1}); - }, 'can only create inside a write transaction'); + TestCase.assertThrowsContaining(() => realm.create('TestObject', {doubleCol: 1}), + "Cannot modify managed objects outside of a write transaction."); - realm.write(function() { + realm.write(() => { realm.create('TestObject', {doubleCol: 1}); realm.create('TestObject', {doubleCol: 2}); }); - var objects = realm.objects('TestObject'); + const objects = realm.objects('TestObject'); TestCase.assertEqual(objects.length, 2, 'wrong object count'); TestCase.assertEqual(objects[0].doubleCol, 1, 'wrong object property value'); TestCase.assertEqual(objects[1].doubleCol, 2, 'wrong object property value'); }, testRealmCreatePrimaryKey: function() { - var realm = new Realm({schema: [schemas.IntPrimary]}); + const realm = new Realm({schema: [schemas.IntPrimary]}); - realm.write(function() { - var obj0 = realm.create('IntPrimaryObject', { + realm.write(() => { + const obj0 = realm.create('IntPrimaryObject', { primaryCol: 0, valueCol: 'val0', }); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { realm.create('IntPrimaryObject', { primaryCol: 0, valueCol: 'val0', }); - }, 'cannot create object with conflicting primary key'); + }, "Attempting to create an object of type 'IntPrimaryObject' with an existing primary key value '0'."); realm.create('IntPrimaryObject', { primaryCol: 1, valueCol: 'val1', }, true); - var objects = realm.objects('IntPrimaryObject'); + const objects = realm.objects('IntPrimaryObject'); TestCase.assertEqual(objects.length, 2); realm.create('IntPrimaryObject', { @@ -324,13 +297,13 @@ module.exports = { }, testRealmCreateOptionals: function() { - var realm = new Realm({schema: [schemas.NullableBasicTypes, schemas.LinkTypes, schemas.TestObject]}); - var basic, links; - realm.write(function() { + const realm = new Realm({schema: [schemas.NullableBasicTypes, schemas.LinkTypes, schemas.TestObject]}); + let basic, links; + realm.write(() => { basic = realm.create('NullableBasicTypesObject', {}); links = realm.create('LinkTypesObject', {}); }); - for (var name in schemas.NullableBasicTypes.properties) { + for (const name in schemas.NullableBasicTypes.properties) { TestCase.assertEqual(basic[name], null); } TestCase.assertEqual(links.objectCol, null); @@ -338,9 +311,9 @@ module.exports = { }, testRealmCreateUpsert: function() { - var realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); - realm.write(function() { - var values = { + const realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); + realm.write(() => { + const values = { primaryCol: '0', boolCol: true, intCol: 1, @@ -353,13 +326,12 @@ module.exports = { arrayCol: [], }; - var obj0 = realm.create('AllTypesObject', values); + const obj0 = realm.create('AllTypesObject', values); - TestCase.assertThrows(function() { - realm.create('AllTypesObject', values); - }, 'cannot create object with conflicting primary key'); + TestCase.assertThrowsContaining(() => realm.create('AllTypesObject', values), + "Attempting to create an object of type 'AllTypesObject' with an existing primary key value '0'."); - var obj1 = realm.create('AllTypesObject', { + const obj1 = realm.create('AllTypesObject', { primaryCol: '1', boolCol: false, intCol: 2, @@ -372,7 +344,7 @@ module.exports = { arrayCol: [{doubleCol: 2}], }, true); - var objects = realm.objects('AllTypesObject'); + const objects = realm.objects('AllTypesObject'); TestCase.assertEqual(objects.length, 2); realm.create('AllTypesObject', { @@ -427,7 +399,7 @@ module.exports = { TestCase.assertEqual(obj1.objectCol, null); // test with string primaries - var obj =realm.create('StringPrimaryObject', { + const obj =realm.create('StringPrimaryObject', { primaryCol: '0', valueCol: 0 }); @@ -442,12 +414,12 @@ module.exports = { }, testRealmWithIndexedProperties: function() { - var realm = new Realm({schema: [schemas.IndexedTypes]}); - realm.write(function() { + const realm = new Realm({schema: [schemas.IndexedTypes]}); + realm.write(() => { realm.create('IndexedTypesObject', {boolCol: true, intCol: 1, stringCol: '1', dateCol: new Date(1)}); }); - var NotIndexed = { + const NotIndexed = { name: 'NotIndexedObject', properties: { floatCol: {type: 'float', indexed: false} @@ -456,23 +428,23 @@ module.exports = { new Realm({schema: [NotIndexed], path: '1.realm'}); - var IndexedSchema = { + const IndexedSchema = { name: 'IndexedSchema', }; - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { IndexedSchema.properties = { floatCol: {type: 'float', indexed: true} }; new Realm({schema: [IndexedSchema], path: '2.realm'}); - }); + }, "Property 'IndexedSchema.floatCol' of type 'float' cannot be indexed."); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { IndexedSchema.properties = { doubleCol: {type: 'double', indexed: true} } new Realm({schema: [IndexedSchema], path: '3.realm'}); - }); + }, "Property 'IndexedSchema.doubleCol' of type 'double' cannot be indexed."); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { IndexedSchema.properties = { dataCol: {type: 'data', indexed: true} } new Realm({schema: [IndexedSchema], path: '4.realm'}); - }); + }, "Property 'IndexedSchema.dataCol' of type 'data' cannot be indexed."); // primary key IndexedSchema.properties = { intCol: {type: 'int', indexed: true} }; @@ -483,11 +455,11 @@ module.exports = { }, testRealmCreateWithDefaults: function() { - var realm = new Realm({schema: [schemas.DefaultValues, schemas.TestObject]}); + let realm = new Realm({schema: [schemas.DefaultValues, schemas.TestObject]}); - var createAndTestObject = function() { - var obj = realm.create('DefaultValuesObject', {}); - var properties = schemas.DefaultValues.properties; + const createAndTestObject = () => { + const obj = realm.create('DefaultValuesObject', {}); + const properties = schemas.DefaultValues.properties; TestCase.assertEqual(obj.boolCol, properties.boolCol.default); TestCase.assertEqual(obj.intCol, properties.intCol.default); @@ -510,17 +482,17 @@ module.exports = { }, testRealmCreateWithChangingDefaults: function() { - var objectSchema = { + const objectSchema = { name: 'IntObject', properties: { intCol: {type: 'int', default: 1}, } }; - var realm = new Realm({schema: [objectSchema]}); + let realm = new Realm({schema: [objectSchema]}); - var createAndTestObject = function() { - var object = realm.create('IntObject', {}); + const createAndTestObject = () => { + const object = realm.create('IntObject', {}); TestCase.assertEqual(object.intCol, objectSchema.properties.intCol.default); }; @@ -533,7 +505,7 @@ module.exports = { }, testRealmCreateWithConstructor: function() { - var customCreated = 0; + let customCreated = 0; function CustomObject() { customCreated++; @@ -548,9 +520,8 @@ module.exports = { function InvalidObject() { return {}; } - TestCase.assertThrows(function() { - new Realm({schema: [InvalidObject]}); - }); + TestCase.assertThrowsContaining(() => new Realm({schema: [InvalidObject]}), + "Realm object constructor must have a 'schema' property."); InvalidObject.schema = { name: 'InvalidObject', @@ -559,10 +530,10 @@ module.exports = { } }; - var realm = new Realm({schema: [CustomObject, InvalidObject]}); + let realm = new Realm({schema: [CustomObject, InvalidObject]}); - realm.write(function() { - var object = realm.create('CustomObject', {intCol: 1}); + realm.write(() => { + let object = realm.create('CustomObject', {intCol: 1}); TestCase.assertTrue(object instanceof CustomObject); TestCase.assertTrue(Object.getPrototypeOf(object) == CustomObject.prototype); TestCase.assertEqual(customCreated, 1); @@ -574,21 +545,21 @@ module.exports = { TestCase.assertEqual(customCreated, 2); }); - TestCase.assertThrows(function() { - realm.write(function() { + TestCase.assertThrowsContaining(() => { + realm.write(() => { realm.create('InvalidObject', {intCol: 1}); }); - }); + }, 'Realm object constructor must not return another value'); // Only the original constructor should be valid. function InvalidCustomObject() {} InvalidCustomObject.schema = CustomObject.schema; - TestCase.assertThrows(function() { - realm.write(function() { + TestCase.assertThrowsContaining(() => { + realm.write(() => { realm.create(InvalidCustomObject, {intCol: 1}); }); - }); + }, 'Constructor was not registered in the schema for this Realm'); // The constructor should still work when creating another Realm instance. realm = new Realm(); @@ -605,9 +576,9 @@ module.exports = { } }; - var realm = new Realm({schema: [CustomObject]}); - realm.write(function() { - var object = realm.create('CustomObject', {intCol: 1}); + let realm = new Realm({schema: [CustomObject]}); + realm.write(() => { + const object = realm.create('CustomObject', {intCol: 1}); TestCase.assertTrue(object instanceof CustomObject); }); @@ -615,30 +586,28 @@ module.exports = { NewCustomObject.schema = CustomObject.schema; realm = new Realm({schema: [NewCustomObject]}); - realm.write(function() { - var object = realm.create('CustomObject', {intCol: 1}); + realm.write(() => { + const object = realm.create('CustomObject', {intCol: 1}); TestCase.assertTrue(object instanceof NewCustomObject); }); }, testRealmDelete: function() { - var realm = new Realm({schema: [schemas.TestObject]}); + const realm = new Realm({schema: [schemas.TestObject]}); - realm.write(function() { - for (var i = 0; i < 10; i++) { + realm.write(() => { + for (let i = 0; i < 10; i++) { realm.create('TestObject', {doubleCol: i}); } }); - var objects = realm.objects('TestObject'); - TestCase.assertThrows(function() { - realm.delete(objects[0]); - }, 'can only delete in a write transaction'); + const objects = realm.objects('TestObject'); + TestCase.assertThrowsContaining(() => realm.delete(objects[0]), + "Can only delete objects within a transaction."); - realm.write(function() { - TestCase.assertThrows(function() { - realm.delete(); - }); + realm.write(() => { + TestCase.assertThrowsContaining(() => realm.delete(), + "object must be of type 'object', got (undefined)"); realm.delete(objects[0]); TestCase.assertEqual(objects.length, 9, 'wrong object count'); @@ -650,24 +619,23 @@ module.exports = { TestCase.assertEqual(objects[0].doubleCol, 7, "wrong property value"); TestCase.assertEqual(objects[1].doubleCol, 8, "wrong property value"); - var threeObjects = realm.objects('TestObject').filtered("doubleCol < 5"); + const threeObjects = realm.objects('TestObject').filtered("doubleCol < 5"); TestCase.assertEqual(threeObjects.length, 3, "wrong results count"); realm.delete(threeObjects); TestCase.assertEqual(objects.length, 4, 'wrong object count'); TestCase.assertEqual(threeObjects.length, 0, 'threeObject should have been deleted'); - var o = objects[0]; + const o = objects[0]; realm.delete(o); - TestCase.assertThrows(function() { - realm.delete(o); - }); + TestCase.assertThrowsContaining(() => realm.delete(o), + 'Object is invalid. Either it has been previously deleted or the Realm it belongs to has been closed.'); }); }, testDeleteAll: function() { - var realm = new Realm({schema: [schemas.TestObject, schemas.IntPrimary]}); + const realm = new Realm({schema: [schemas.TestObject, schemas.IntPrimary]}); - realm.write(function() { + realm.write(() => { realm.create('TestObject', {doubleCol: 1}); realm.create('TestObject', {doubleCol: 2}); realm.create('IntPrimaryObject', {primaryCol: 2, valueCol: 'value'}); @@ -676,11 +644,10 @@ module.exports = { TestCase.assertEqual(realm.objects('TestObject').length, 2); TestCase.assertEqual(realm.objects('IntPrimaryObject').length, 1); - TestCase.assertThrows(function() { - realm.deleteAll(); - }, 'can only deleteAll in a write transaction'); + TestCase.assertThrowsContaining(() => realm.deleteAll(), + "Can only delete objects within a transaction."); - realm.write(function() { + realm.write(() => { realm.deleteAll(); }); @@ -689,9 +656,9 @@ module.exports = { }, testRealmObjects: function() { - var realm = new Realm({schema: [schemas.PersonObject, schemas.DefaultValues, schemas.TestObject]}); + const realm = new Realm({schema: [schemas.PersonObject, schemas.DefaultValues, schemas.TestObject]}); - realm.write(function() { + realm.write(() => { realm.create('PersonObject', {name: 'Ari', age: 10}); realm.create('PersonObject', {name: 'Tim', age: 11}); realm.create('PersonObject', {name: 'Bjarne', age: 12}); @@ -699,77 +666,45 @@ module.exports = { }); // Should be able to pass constructor for getting objects. - var objects = realm.objects(schemas.PersonObject); + const objects = realm.objects(schemas.PersonObject); TestCase.assertTrue(objects[0] instanceof schemas.PersonObject); function InvalidPerson() {} InvalidPerson.schema = schemas.PersonObject.schema; - TestCase.assertThrows(function() { - realm.objects(); - }); - TestCase.assertThrows(function() { - realm.objects([]); - }); - TestCase.assertThrows(function() { - realm.objects('InvalidClass'); - }); - TestCase.assertThrows(function() { - realm.objects('PersonObject', 'truepredicate'); - }); - TestCase.assertThrows(function() { - realm.objects(InvalidPerson); - }); + TestCase.assertThrowsContaining(() => realm.objects(), "objectType must be of type 'string', got (undefined)"); + TestCase.assertThrowsContaining(() => realm.objects([]), "objectType must be of type 'string', got ()"); + TestCase.assertThrowsContaining(() => realm.objects('InvalidClass'), "Object type 'InvalidClass' not found in schema."); + TestCase.assertThrowsContaining(() => realm.objects('PersonObject', 'truepredicate'), + "Invalid arguments: at most 1 expected, but 2 supplied."); + TestCase.assertThrowsContaining(() => realm.objects(InvalidPerson), + 'Constructor was not registered in the schema for this Realm'); - var person = realm.objects('PersonObject')[0]; - var listenerCallback = () => {}; + const person = realm.objects('PersonObject')[0]; + const listenerCallback = () => {}; realm.addListener('change', listenerCallback); // The tests below assert that everthing throws when // operating on a closed realm realm.close(); - TestCase.assertThrows(function() { - console.log("Name: ", person.name); - }); + TestCase.assertThrowsContaining(() => console.log("Name: ", person.name), + 'Accessing object of type PersonObject which has been invalidated or deleted'); - TestCase.assertThrows(function() { - realm.objects('PersonObject'); - }); - - TestCase.assertThrows(function() { - realm.addListener('change', () => {}); - }); - - TestCase.assertThrows(function() { - realm.create('PersonObject', {name: 'Ari', age: 10}); - }); - - TestCase.assertThrows(function() { - realm.delete(person); - }); - - TestCase.assertThrows(function() { - realm.deleteAll(); - }); - - TestCase.assertThrows(function() { - realm.write(() => {}); - }); - - TestCase.assertThrows(function() { - realm.removeListener('change', listenerCallback); - }); - - TestCase.assertThrows(function() { - realm.removeAllListeners(); - }); + TestCase.assertThrowsContaining(() => realm.objects('PersonObject'), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.addListener('change', () => {}), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.create('PersonObject', {name: 'Ari', age: 10}), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.delete(person), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.deleteAll(), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.write(() => {}), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.removeListener('change', listenerCallback), 'Cannot access realm that has been closed'); + TestCase.assertThrowsContaining(() => realm.removeAllListeners(), 'Cannot access realm that has been closed'); }, testRealmObjectForPrimaryKey: function() { - var realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.TestObject]}); + const realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.TestObject]}); - realm.write(function() { + realm.write(() => { realm.create('IntPrimaryObject', {primaryCol: 0, valueCol: 'val0'}); realm.create('IntPrimaryObject', {primaryCol: 1, valueCol: 'val1'}); @@ -789,36 +724,32 @@ module.exports = { TestCase.assertEqual(realm.objectForPrimaryKey('StringPrimaryObject', 'val0').valueCol, 0); TestCase.assertEqual(realm.objectForPrimaryKey('StringPrimaryObject', 'val1').valueCol, 1); - TestCase.assertThrows(function() { - realm.objectForPrimaryKey('TestObject', 0); - }); - TestCase.assertThrows(function() { - realm.objectForPrimaryKey(); - }); - TestCase.assertThrows(function() { - realm.objectForPrimaryKey('IntPrimary'); - }); - TestCase.assertThrows(function() { - realm.objectForPrimaryKey('InvalidClass', 0); - }); + TestCase.assertThrowsContaining(() => realm.objectForPrimaryKey('TestObject', 0), + "'TestObject' does not have a primary key defined"); + TestCase.assertThrowsContaining(() => realm.objectForPrimaryKey(), + "objectType must be of type 'string', got (undefined)"); + TestCase.assertThrowsContaining(() => realm.objectForPrimaryKey('IntPrimaryObject'), + "Invalid null value for non-nullable primary key."); + TestCase.assertThrowsContaining(() => realm.objectForPrimaryKey('InvalidClass', 0), + "Object type 'InvalidClass' not found in schema."); }, testNotifications: function() { - var realm = new Realm({schema: []}); - var notificationCount = 0; - var notificationName; + const realm = new Realm({schema: []}); + let notificationCount = 0; + let notificationName; - realm.addListener('change', function(realm, name) { + realm.addListener('change', (realm, name) => { notificationCount++; notificationName = name; }); TestCase.assertEqual(notificationCount, 0); - realm.write(function() {}); + realm.write(() => {}); TestCase.assertEqual(notificationCount, 1); TestCase.assertEqual(notificationName, 'change'); - var secondNotificationCount = 0; + let secondNotificationCount = 0; function secondNotification() { secondNotificationCount++; } @@ -827,39 +758,37 @@ module.exports = { realm.addListener('change', secondNotification); realm.addListener('change', secondNotification); - realm.write(function() {}); + realm.write(() => {}); TestCase.assertEqual(notificationCount, 2); TestCase.assertEqual(secondNotificationCount, 1); realm.removeListener('change', secondNotification); - realm.write(function() {}); + realm.write(() => {}); TestCase.assertEqual(notificationCount, 3); TestCase.assertEqual(secondNotificationCount, 1); realm.removeAllListeners(); - realm.write(function() {}); + realm.write(() => {}); TestCase.assertEqual(notificationCount, 3); TestCase.assertEqual(secondNotificationCount, 1); - TestCase.assertThrows(function() { - realm.addListener('invalid', function() {}); + TestCase.assertThrowsContaining(() => realm.addListener('invalid', () => {}), + "Only the 'change' notification name is supported."); + + realm.addListener('change', () => { + throw new Error('expected error message'); }); - realm.addListener('change', function() { - throw new Error('error'); - }); - - TestCase.assertThrows(function() { - realm.write(function() {}); - }); + TestCase.assertThrowsContaining(() => realm.write(() => {}), + 'expected error message'); }, testSchema: function() { - var originalSchema = [schemas.TestObject, schemas.BasicTypes, schemas.NullableBasicTypes, schemas.IndexedTypes, schemas.IntPrimary, + const originalSchema = [schemas.TestObject, schemas.BasicTypes, schemas.NullableBasicTypes, schemas.IndexedTypes, schemas.IntPrimary, schemas.PersonObject, schemas.LinkTypes, schemas.LinkingObjectsObject]; - var schemaMap = {}; - originalSchema.forEach(function(objectSchema) { + const schemaMap = {}; + originalSchema.forEach(objectSchema => { if (objectSchema.schema) { // for PersonObject schemaMap[objectSchema.schema.name] = objectSchema; } else { @@ -867,9 +796,9 @@ module.exports = { } }); - var realm = new Realm({schema: originalSchema}); + const realm = new Realm({schema: originalSchema}); - var schema = realm.schema; + const schema = realm.schema; TestCase.assertEqual(schema.length, originalSchema.length); function isString(val) { @@ -877,15 +806,15 @@ module.exports = { } function verifyObjectSchema(returned) { - var original = schemaMap[returned.name]; + let original = schemaMap[returned.name]; if (original.schema) { original = original.schema; } TestCase.assertEqual(returned.primaryKey, original.primaryKey); - for (var propName in returned.properties) { - var prop1 = returned.properties[propName]; - var prop2 = original.properties[propName]; + for (const propName in returned.properties) { + const prop1 = returned.properties[propName]; + const prop2 = original.properties[propName]; if (prop1.type == 'object') { TestCase.assertEqual(prop1.objectType, isString(prop2) ? prop2 : prop2.objectType); TestCase.assertEqual(prop1.optional, true); @@ -908,7 +837,7 @@ module.exports = { } } - for (var i = 0; i < originalSchema.length; i++) { + for (let i = 0; i < originalSchema.length; i++) { verifyObjectSchema(schema[i]); } }, @@ -916,12 +845,12 @@ module.exports = { testCopyBundledRealmFiles: function() { Realm.copyBundledRealmFiles(); - var realm = new Realm({path: 'dates-v5.realm', schema: [schemas.DateObject]}); + let realm = new Realm({path: 'dates-v5.realm', schema: [schemas.DateObject]}); TestCase.assertEqual(realm.objects('Date').length, 2); TestCase.assertEqual(realm.objects('Date')[0].currentDate.getTime(), 1462500087955); - var newDate = new Date(1); - realm.write(function() { + const newDate = new Date(1); + realm.write(() => { realm.objects('Date')[0].currentDate = newDate; }); realm.close(); @@ -933,33 +862,33 @@ module.exports = { }, testErrorMessageFromInvalidWrite: function() { - var realm = new Realm({schema: [schemas.PersonObject]}); + const realm = new Realm({schema: [schemas.PersonObject]}); - TestCase.assertThrowsException(function() { - realm.write(function () { - var p1 = realm.create('PersonObject', { name: 'Ari', age: 10 }); + TestCase.assertThrowsException(() => { + realm.write(() => { + const p1 = realm.create('PersonObject', { name: 'Ari', age: 10 }); p1.age = "Ten"; }); }, new Error("PersonObject.age must be of type 'number', got (Ten)")); }, testErrorMessageFromInvalidCreate: function() { - var realm = new Realm({schema: [schemas.PersonObject]}); + const realm = new Realm({schema: [schemas.PersonObject]}); - TestCase.assertThrowsException(function() { - realm.write(function () { - var p1 = realm.create('PersonObject', { name: 'Ari', age: 'Ten' }); + TestCase.assertThrowsException(() => { + realm.write(() => { + const p1 = realm.create('PersonObject', { name: 'Ari', age: 'Ten' }); }); }, new Error("PersonObject.age must be of type 'number', got (Ten)")); }, testValidTypesForListProperties: function() { - var realm = new Realm({schema: [schemas.PersonObject]}); - realm.write(function () { - var p1 = realm.create('PersonObject', { name: 'Ari', age: 10 }); - var p2 = realm.create('PersonObject', { name: 'Harold', age: 55, children: realm.objects('PersonObject').filtered('age < 15') }); + const realm = new Realm({schema: [schemas.PersonObject]}); + realm.write(() => { + const p1 = realm.create('PersonObject', { name: 'Ari', age: 10 }); + const p2 = realm.create('PersonObject', { name: 'Harold', age: 55, children: realm.objects('PersonObject').filtered('age < 15') }); TestCase.assertEqual(p2.children.length, 1); - var p3 = realm.create('PersonObject', { name: 'Wendy', age: 52, children: p2.children }); + const p3 = realm.create('PersonObject', { name: 'Wendy', age: 52, children: p2.children }); TestCase.assertEqual(p3.children.length, 1); }); }, @@ -1007,13 +936,13 @@ module.exports = { }, testCompact: function() { - var wasCalled = false; + let 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++) { + for (let i = 0; i < count; i++) { realm1.create('StringOnlyObject', { stringCol: 'ABCDEFG' }); } realm1.create('StringOnlyObject', { stringCol: 'B' }); @@ -1021,7 +950,7 @@ module.exports = { realm1.close(); // open Realm and see if it is compacted - var shouldCompact = function(totalBytes, usedBytes) { + const shouldCompact = (totalBytes, usedBytes) => { wasCalled = true; const fiveHundredKB = 500*1024; return (totalBytes > fiveHundredKB) && (usedBytes / totalBytes) < 0.2; @@ -1049,9 +978,9 @@ module.exports = { testManualCompactInWrite: function() { const realm = new Realm({schema: [schemas.StringOnly]}); realm.write(() => { - TestCase.assertThrows(() => { + TestCase.assertThrowsContaining(() => { realm.compact(); - }); + }, 'Cannot compact a Realm within a transaction.'); }); TestCase.assertTrue(realm.empty); }, @@ -1059,14 +988,14 @@ module.exports = { testManualCompactMultipleInstances: function() { const realm1 = new Realm({schema: [schemas.StringOnly]}); const realm2 = new Realm({schema: [schemas.StringOnly]}); - TestCase.assertThrows(realm1.compact()); + TestCase.assertTrue(realm1.compact()); }, testRealmDeleteFileDefaultConfigPath: function() { const config = {schema: [schemas.TestObject]}; const realm = new Realm(config); - realm.write(function() { + realm.write(() => { realm.create('TestObject', {doubleCol: 1}); }); @@ -1084,7 +1013,7 @@ module.exports = { const config = {schema: [schemas.TestObject], path: 'test-realm-delete-file.realm'}; const realm = new Realm(config); - realm.write(function() { + realm.write(() => { realm.create('TestObject', {doubleCol: 1}); }); diff --git a/tests/js/results-tests.js b/tests/js/results-tests.js index b7b83159..ad7395e2 100644 --- a/tests/js/results-tests.js +++ b/tests/js/results-tests.js @@ -383,23 +383,23 @@ module.exports = { TestCase.assertEqual(snapshot.length, 0); }); }, - + testResultsFindIndexOfObject: function() { var realm = new Realm({schema: [schemas.TestObject]}); - + var object1, object2, object3; realm.write(function() { object1 = realm.create('TestObject', {doubleCol: 1}); object2 = realm.create('TestObject', {doubleCol: 2}); object3 = realm.create('TestObject', {doubleCol: 2}); }); - + // Search in base table const objects = realm.objects('TestObject'); TestCase.assertEqual(objects.indexOf(object1), 0); TestCase.assertEqual(objects.indexOf(object2), 1); TestCase.assertEqual(objects.indexOf(object3), 2); - + // Search in filtered query const results = objects.filtered("doubleCol == 2"); TestCase.assertEqual(results.indexOf(object1), -1); @@ -408,7 +408,7 @@ module.exports = { const nonRealmObject = {test: "this is an object"}; TestCase.assertEqual(objects.indexOf(nonRealmObject), -1); - + // Searching for object from the wrong realm var realm2 = new Realm({path: '2.realm', schema: realm.schema}); var object4; @@ -421,29 +421,40 @@ module.exports = { }, testAddListener: function() { - return new Promise((resolve, _reject) => { - var realm = new Realm({ schema: [schemas.TestObject] }); + if (typeof navigator !== 'undefined' && /Chrome/.test(navigator.userAgent)) { // eslint-disable-line no-undef + // FIXME: async callbacks do not work correctly in Chrome debugging mode + return; + } - realm.write(() => { - realm.create('TestObject', { doubleCol: 1 }); - realm.create('TestObject', { doubleCol: 2 }); - realm.create('TestObject', { doubleCol: 3 }); - }); + const realm = new Realm({ schema: [schemas.TestObject] }); + realm.write(() => { + realm.create('TestObject', { doubleCol: 1 }); + realm.create('TestObject', { doubleCol: 2 }); + realm.create('TestObject', { doubleCol: 3 }); + }); + let resolve, first = true; + return new Promise((r, _reject) => { + resolve = r; realm.objects('TestObject').addListener((testObjects, changes) => { - // TODO: First notification is empty, so perform these - // assertions on the second call. However, there is a race condition - // in React Native, so find a way to do this in a robust way. - //TestCase.assertEqual(testObjects.length, 4); - //TestCase.assertEqual(changes.insertions.length, 1); + if (first) { + TestCase.assertEqual(testObjects.length, 3); + TestCase.assertEqual(changes.insertions.length, 0); + } + else { + TestCase.assertEqual(testObjects.length, 4); + TestCase.assertEqual(changes.insertions.length, 1); + } + first = false; resolve(); }); - - realm.write(() => { - realm.create('TestObject', { doubleCol: 1 }); + }).then(() => { + return new Promise((r, _reject) => { + realm.write(() => { + realm.create('TestObject', { doubleCol: 1 }); + }); + resolve = r; }); - }) + }); } - - }; diff --git a/tests/react-test-app/ios/ReactTests/RealmReactTests.m b/tests/react-test-app/ios/ReactTests/RealmReactTests.m index 746e31a6..27bacbe8 100644 --- a/tests/react-test-app/ios/ReactTests/RealmReactTests.m +++ b/tests/react-test-app/ios/ReactTests/RealmReactTests.m @@ -168,6 +168,7 @@ extern NSMutableArray *RCTGetModuleClasses(void); + (void)waitForCondition:(BOOL *)condition description:(NSString *)description { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:30.0]; + RCTBridge *bridge = [self currentBridge]; while (!*condition) { if ([timeout timeIntervalSinceNow] < 0) { @@ -180,6 +181,7 @@ extern NSMutableArray *RCTGetModuleClasses(void); [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [NSThread sleepForTimeInterval:0.01]; // Bad things may happen without some sleep. + [bridge.eventDispatcher sendAppEventWithName:@"realm-dummy" body:nil]; // Ensure RN has an event loop running } } }