diff --git a/CHANGELOG.md b/CHANGELOG.md index 7297b559..5156483e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -x.x.x Release notes (yyyy-MM-dd) +x.x.x Release notes (yyyy-MM-dd) ============================================================= ### Breaking changes * None. @@ -7,10 +7,22 @@ x.x.x Release notes (yyyy-MM-dd) * None. ### Bug fixes -* Configuration of file system is delay to after module import (#1351). +* Configuration of sync file system is not done on module import but later when actually needed by sync (#1351) -2.0.0 Release notes (2017-9-26) +2.0.0 Release notes (2017-9-28) +============================================================= +### Breaking changes +* None. + +### Enhancements +* None. + +### Bug fixes +* An issue where access tokens were not refreshed correctly has been addressed. + + +2.0.0-rc11 Release notes (2017-9-26) ============================================================= ### Breaking changes * None @@ -25,6 +37,9 @@ x.x.x Release notes (yyyy-MM-dd) * Alignment of permission schemas. * Updating sync (2.0.0-rc24). + + + 2.0.0-rc10 Release notes (2017-9-19) ============================================================= ### Breaking changes diff --git a/dependencies.list b/dependencies.list index d8a32030..2d143cdb 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,5 +1,5 @@ PACKAGE_NAME=realm-js -VERSION=2.0.0-rc11 +VERSION=2.0.0-rc13 REALM_CORE_VERSION=3.2.1 REALM_SYNC_VERSION=2.0.0-rc24 REALM_OBJECT_SERVER_VERSION=2.0.0-alpha.36 diff --git a/docs/collection.js b/docs/collection.js index 5cb8852e..0db883aa 100644 --- a/docs/collection.js +++ b/docs/collection.js @@ -18,17 +18,39 @@ /** * Abstract base class containing methods shared by {@link Realm.List} and {@link Realm.Results}. + * + * A Realm Collection is a homogenous sequence of values of any of the types + * that can be stored as properties of Realm objects. A collection can be + * accessed in any of the ways that a normal Javascript Array can, including + * subscripting, enumerating with `for-of` and so on. + * * @memberof Realm * @since 0.11.0 */ class Collection { /** - * The number of objects in the collection. + * The number of values in the collection. * @type {number} * @readonly */ get length() {} + /** + * The {@linkplain Realm~PropertyType type} of values in the collection. + * @type {string} + * @readonly + * @since 2.0.0 + */ + get type() {} + + /** + * Whether `null` is a valid value for the collection. + * @type {boolean} + * @readonly + * @since 2.0.0 + */ + get optional() {} + /** * Checks if this collection has not been deleted and is part of a valid Realm. * @returns {boolean} indicating if the collection can be safely accessed. @@ -38,12 +60,15 @@ class Collection { /** * Returns new _Results_ that represent this collection being filtered by the provided query. + * * @param {string} query - Query used to filter objects from the collection. * @param {...any} [arg] - Each subsequent argument is used by the placeholders * (e.g. `$0`, `$1`, `$2`, …) in the query. * @throws {Error} If the query or any other argument passed into this method is invalid. - * @returns {Realm.Results} filtered according to the provided query. - * + * @returns {Realm.Results} filtered according to the provided query. + * + * This is currently only supported for collections of Realm Objects. + * * See {@tutorial query-language} for details about the query language. * @example * let merlots = wines.filtered('variety == "Merlot" && vintage <= $0', maxYear); @@ -51,43 +76,76 @@ class Collection { filtered(query, ...arg) {} /** - * Returns new _Results_ that represent this collection being sorted by the provided property - * (or properties) of each object. - * @param {string|Realm.Results~SortDescriptor[]} descriptor - The property name(s) to sort - * the objects in the collection. - * @param {boolean} [reverse=false] - May only be provided if `descriptor` is a string. + * Returns new _Results_ that represent a sorted view of this collection. + * + * A collection of Realm Objects can be sorted on one or more properties of + * those objects, or of properties of objects linked to by those objects. + * To sort by a single property, simply pass the name of that property to + * `sorted()`, optionally followed by a boolean indicating if the sort should be reversed. + * For more than one property, you must pass an array of + * {@linkplain Realm.Collection~SortDescriptor sort descriptors} which list + * which properties to sort on. + * + * Collections of other types sort on the values themselves rather than + * properties of the values, and so no property name or sort descriptors + * should be supplied. + * + * @example + * // Sort wines by age + * wines.sorted('age') + * @example + * // Sort wines by price in descending order, then sort ties by age in + * // ascending order + * wines.sorted([['price', false], ['age']) + * @example + * // Sort a list of numbers in ascending order + * let sortedPrices = wine.pricesSeen.sort() + * @example + * // Sort people by how expensive their favorite wine is + * people.sort("favoriteWine.price") + * + * @param {string|Realm.Collection~SortDescriptor[]} [descriptor] - The property name(s) to sort the collection on. + * @param {boolean} [reverse=false] - Sort in descending order rather than ascended. + * May not be supplied if `descriptor` is an array of sort descriptors. * @throws {Error} If a specified property does not exist. - * @returns {Realm.Results} sorted according to the arguments passed in + * @returns {Realm.Results} sorted according to the arguments passed in. */ sorted(descriptor, reverse) {} /** - * Create a frozen snapshot of the collection. This means objects added to and removed from the - * original collection will not be reflected in the _Results_ returned by this method. - * However, objects deleted from the Realm will become `null` at their respective indices. - * This is **not** a _deep_ snapshot, meaning the objects contained in this snapshot will - * continue to update as changes are made to them. - * @returns {Realm.Results} which will **not** live update. + * Create a frozen snapshot of the collection. + * + * Values added to and removed from the original collection will not be + * reflected in the _Results_ returned by this method, including if the + * values of properties are changed to make them match or not match any + * filters applied. + * + * This is **not** a _deep_ snapshot. Realm objects contained in this + * snapshot will continue to update as changes are made to them, and if + * they are deleted from the Realm they will be replaced by `null` at the + * respective indices. + * + * @returns {Realm.Results} which will **not** live update. */ snapshot() {} /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries Array.prototype.entries} - * @returns {Realm.Collection~Iterator} of each `[index, object]` pair in the collection + * @returns {Realm.Collection~Iterator} of each `[index, object]` pair in the collection * @since 0.11.0 */ entries() {} /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/keys Array.prototype.keys} - * @returns {Realm.Collection~Iterator} of each index in the collection + * @returns {Realm.Collection~Iterator} of each index in the collection * @since 0.11.0 */ keys() {} /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values Array.prototype.values} - * @returns {Realm.Collection~Iterator} of each Realm object in the collection + * @returns {Realm.Collection~Iterator} of each Realm object in the collection * @since 0.11.0 */ values() {} @@ -101,7 +159,7 @@ class Collection { * spread operators, and more. * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator Symbol.iterator} * and the {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#iterable iterable protocol} - * @returns {Realm.Collection~Iterator} of each Realm object in the collection + * @returns {Realm.Collection~Iterator} of each Realm object in the collection * @since 0.11.0 * @example * for (let object of collection) { @@ -128,7 +186,7 @@ class Collection { * index will be include in the return value. If negative, then the end index will be * counted from the end of the collection. If omitted, then all objects from the start * index will be included in the return value. - * @returns {Realm.Object[]} containing the objects from the start index up to, but not + * @returns {T[]} containing the objects from the start index up to, but not * including, the end index. * @since 0.11.0 */ @@ -143,7 +201,7 @@ class Collection { * - `index` – The index of the object being processed in the collection. * - `collection` – The collection itself. * @param {object} [thisArg] - The value of `this` when `callback` is called. - * @returns {Realm.Object|undefined} if the `callback` did not return `true` for any object + * @returns {T|undefined} if the `callback` did not return `true` for any object * in the collection. * @since 0.11.0 */ @@ -166,10 +224,11 @@ class Collection { /** Finds the index of the given object in the collection. - * @param {Realm.Object} [object] - The object to search for in the collection. - * @throws {Error} If the argument does not belong to the realm. - * @returns {number} representing the index where the object was found, or `-1` - * if not in collection. + * @param {T} object - The value to search for in the collection. + * @throws {Error} If the argument is a {@link Realm.Object} that does not + * belong to the same Realm as the collection. + * @returns {number} representing the index where the value was found, or + * `-1` if not in collection. * @since 1.8.2 */ indexOf(object) {} @@ -294,7 +353,7 @@ class Collection { /** * Remove the listener `callback` from the collection instance. * @param {function(collection, changes)} callback - Callback function that was previously - * added as a listener through the {@link Collection#addListener addListener} method. + * added as a listener through the {@link Collection#addListener addListener} method. * @throws {Error} If `callback` is not a function. */ removeListener(callback) {} @@ -324,8 +383,9 @@ class Collection { */ /** - * The sort descriptors may either just be a string representing the property name, **or** an - * array with two items: `[propertyName, reverse]` + * A sort descriptor is either a string containing one or more property names + * separate by dots, **or** an array with two items: `[propertyName, reverse]`. + * * @typedef Realm.Collection~SortDescriptor * @type {string|Array} */ diff --git a/docs/list.js b/docs/list.js index 66e14d06..500e4e4c 100644 --- a/docs/list.js +++ b/docs/list.js @@ -19,59 +19,70 @@ /** * Instances of this class will be returned when accessing object properties whose type is `"list"` * (see {@linkplain Realm~ObjectSchemaProperty ObjectSchemaProperty}). - * The objects contained in a list are accessible through its index properties and may only be - * modified inside a {@linkplain Realm#write write} transaction. + * + * Lists mostly behave like normal Javascript Arrays, except for that they can + * only store values of a single type (indicated by the `type` and `optional` + * properties of the List), and can only be modified inside a {@linkplain + * Realm#write write} transaction. + * * @extends Realm.Collection * @memberof Realm */ class List extends Collection { /** - * Remove the **last** object from the list and return it. + * Remove the **last** value from the list and return it. * @throws {Error} If not inside a write transaction. - * @returns {Realm.Object|undefined} if the list is empty. + * @returns {T|undefined} if the list is empty. */ pop() {} /** - * Add one or more objects to the _end_ of the list. - * @param {...Realm.Object} object - Each object’s type must match - * {@linkcode Realm~ObjectSchemaProperty objectType} specified in the schema. - * @throws {TypeError} If an `object` is of the wrong type. + * Add one or more values to the _end_ of the list. + * + * @param {...T} value - Values to add to the list. + * @throws {TypeError} If a `value` is not of a type which can be stored in + * the list, or if an object being added to the list does not match the + * {@linkcode Realm~ObjectSchema object schema} for the list. + * * @throws {Error} If not inside a write transaction. - * @returns {number} equal to the new {@link Realm.List#length length} of the list - * after adding objects. + * @returns {number} equal to the new {@link Realm.List#length length} of + * the list after adding the values. */ - push(...object) {} + push(...value) {} /** - * Remove the **first** object from the list and return it. + * Remove the **first** value from the list and return it. * @throws {Error} If not inside a write transaction. - * @returns {Realm.Object|undefined} if the list is empty. + * @returns {T|undefined} if the list is empty. */ shift() {} /** - * Changes the contents of the list by removing objects and/or inserting new objects. + * Changes the contents of the list by removing value and/or inserting new value. + * * @see {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice Array.prototype.splice} * @param {number} index - The start index. If greater than the length of the list, * the start index will be set to the length instead. If negative, then the start index * will be counted from the end of the list (e.g. `list.length - index`). - * @param {number} [count] - The number of objects to remove from the list. If not provided, - * then all objects from the start index through the end of the list will be removed. - * @param {...Realm.Object} [object] - Objects to insert into the list starting at `index`. - * @returns {Realm.Object[]} containing the objects that were removed from the list. The - * array is empty if no objects were removed. + * @param {number} [count] - The number of values to remove from the list. + * If not provided, then all values from the start index through the end of + * the list will be removed. + * @param {...T} [value] - Values to insert into the list starting at `index`. + * @returns {T[]} containing the value that were removed from the list. The + * array is empty if no value were removed. */ splice(index, count, ...object) {} /** - * Add one or more objects to the _beginning_ of the list. - * @param {...Realm.Object} object - Each object’s type must match - * {@linkcode Realm~ObjectSchemaProperty objectType} specified in the schema. - * @throws {TypeError} If an `object` is of the wrong type. + * Add one or more values to the _beginning_ of the list. + * + * @param {...T} value - Values to add to the list. + * @throws {TypeError} If a `value` is not of a type which can be stored in + * the list, or if an object being added to the list does not match the + * {@linkcode Realm~ObjectSchema object schema} for the list. * @throws {Error} If not inside a write transaction. - * @returns {number} equal to the new {@link Realm.List#length length} of the list - * after adding objects. + * @returns {number} equal to the new {@link Realm.List#length length} of + * the list after adding the values. */ unshift(...object) {} -} \ No newline at end of file +} diff --git a/docs/realm.js b/docs/realm.js index e379d1c7..8a3aa209 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -95,7 +95,7 @@ class Realm { * 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 - * @returns {ProgressPromise} - a promise that will be resolved with the realm instance when it's available. + * @returns {ProgressPromise} - a promise that will be resolved with the Realm instance when it's available. */ static open(config) {} @@ -103,7 +103,7 @@ class Realm { * 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 {callback(error, realm)} - will be called when the realm is ready. + * @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. */ @@ -286,6 +286,7 @@ Realm.defaultPath; * @property {boolean} [readOnly=false] - Specifies if this Realm should be opened as read-only. * @property {Array} [schema] - Specifies all the * object types in this Realm. **Required** when first creating a Realm at this `path`. + * If omitted, the schema will be read from the existing Realm file. * @property {number} [schemaVersion] - **Required** (and must be incremented) after * changing the `schema`. * @property {Object} [sync] - Sync configuration parameters with the following @@ -350,19 +351,39 @@ Realm.defaultPath; * that must be unique across all objects of this type within the same Realm. * @property {Object} properties - * An object where the keys are property names and the values represent the property type. + * + * @example + * let MyClassSchema = { + * name: 'MyClass', + * primaryKey: 'pk', + * properties: { + * pk: 'int', + * optionalFloatValue: 'float?' // or {type: 'float', optional: true} + * listOfStrings: 'string[]', + * listOfOptionalDates: 'date?[]', + * indexedInt: {type: 'int', indexed: true} + * + * linkToObject: 'MyClass', + * listOfObjects: 'MyClass[]', // or {type: 'list', objectType: 'MyClass'} + * objectsLinkingToThisObject: {type: 'linkingObjects', objectType: 'MyClass', property: 'linkToObject'} + * } + * }; */ /** * @typedef Realm~ObjectSchemaProperty * @type {Object} * @property {Realm~PropertyType} type - The type of this property. - * @property {string} [objectType] - **Required** when `type` is `"list"` or `"linkingObjects"`, - * and must match the type of an object in the same schema. + * @property {Realm~PropertyType} [objectType] - **Required** when `type` is `"list"` or `"linkingObjects"`, + * and must match the type of an object in the same schema, or, for `"list"` + * only, any other type which may be stored as a Realm property. * @property {string} [property] - **Required** when `type` is `"linkingObjects"`, and must match * the name of a property on the type specified in `objectType` that links to the type this property belongs to. * @property {any} [default] - The default value for this property on creation when not * otherwise specified. * @property {boolean} [optional] - Signals if this property may be assigned `null` or `undefined`. + * For `"list"` properties of non-object types, this instead signals whether the values inside the list may be assigned `null` or `undefined`. + * This is not supported for `"list"` properties of object types and `"linkingObjects"` properties. * @property {boolean} [indexed] - Signals if this property should be indexed. Only supported for * `"string"`, `"int"`, and `"bool"` properties. */ @@ -376,10 +397,21 @@ Realm.defaultPath; */ /** - * A property type may be specified as one of the standard builtin types, or as an object type - * inside the same schema. + * A property type may be specified as one of the standard builtin types, or as + * an object type inside the same schema. + * + * When specifying property types in an {@linkplain Realm~ObjectSchema object schema}, you + * may append `?` to any of the property types to indicate that it is optional + * (i.e. it can be `null` in addition to the normal values) and `[]` to + * indicate that it is instead a list of that type. For example, + * `optionalIntList: 'int?[]'` would declare a property which is a list of + * nullable integers. The property types reported by {@linkplain Realm.Collection + * collections} and in a Realm's schema will never + * use these forms. + * * @typedef Realm~PropertyType * @type {("bool"|"int"|"float"|"double"|"string"|"date"|"data"|"list"|"linkingObjects"|"")} + * * @property {boolean} "bool" - Property value may either be `true` or `false`. * @property {number} "int" - Property may be assigned any number, but will be stored as a * round integer, meaning anything after the decimal will be truncated. diff --git a/docs/results.js b/docs/results.js index d04818e6..2a406a33 100644 --- a/docs/results.js +++ b/docs/results.js @@ -23,6 +23,7 @@ * {@link Realm.Results#snapshot snapshot()}, however, will **not** live update * (and listener callbacks added through {@link Realm.Results#addListener addListener()} * will thus never be called). + * * @extends Realm.Collection * @memberof Realm */ diff --git a/lib/browser/collections.js b/lib/browser/collections.js index c3d89c0b..f258af01 100644 --- a/lib/browser/collections.js +++ b/lib/browser/collections.js @@ -54,7 +54,7 @@ export function fireMutationListeners(realmId) { } function isIndex(propertyName) { - return typeof propertyName === 'number' || (typeof propertyName === 'string' && /^\d+$/.test(propertyName)); + return typeof propertyName === 'number' || (typeof propertyName === 'string' && /^-?\d+$/.test(propertyName)); } const mutable = Symbol('mutable'); @@ -83,7 +83,10 @@ const traps = { return true; } - return Reflect.set(collection, property, value, collection); + if (!Reflect.set(collection, property, value, collection)) { + throw new TypeError(`Cannot assign to read only property '${property}'`) + } + return true; }, ownKeys(collection) { return Reflect.ownKeys(collection).concat(Array.from({ length: collection.length }, (value, key) => String(key))); @@ -117,8 +120,11 @@ export function createCollection(prototype, realmId, info, _mutable) { 'length': { get: getterForProperty('length'), }, - '-1': { - value: undefined, + 'type': { + get: getterForProperty('type'), + }, + 'optional': { + get: getterForProperty('optional'), }, }); diff --git a/lib/browser/rpc.js b/lib/browser/rpc.js index 427df21e..f71eeac5 100644 --- a/lib/browser/rpc.js +++ b/lib/browser/rpc.js @@ -72,6 +72,12 @@ export function createUser(args) { return deserialize(undefined, result); } +export function _adminUser(args) { + args = args.map((arg) => serialize(null, arg)); + const result = sendRequest('_adminUser', {arguments: args}); + return deserialize(undefined, result); +} + export function callMethod(realmId, id, name, args) { if (args) { args = args.map((arg) => serialize(realmId, arg)); diff --git a/lib/browser/user.js b/lib/browser/user.js index 3c4de66a..ca55b915 100644 --- a/lib/browser/user.js +++ b/lib/browser/user.js @@ -19,7 +19,7 @@ 'use strict'; -import { createUser as createUserRPC, getAllUsers as getAllUsersRPC } from './rpc'; +import { createUser as createUserRPC, _adminUser as _adminUserRPC, getAllUsers as getAllUsersRPC } from './rpc'; import { keys, objectTypes } from './constants'; import { createMethods } from './util'; @@ -28,6 +28,10 @@ export default class User { return createUserRPC(Array.from(arguments)); } + static _adminUser(adminToken, server) { + return _adminUserRPC(Array.from(arguments)); + } + static get all() { return getAllUsersRPC(); } diff --git a/lib/user-methods.js b/lib/user-methods.js index d296f988..de390447 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -76,7 +76,10 @@ function refreshAccessToken(user, localRealmPath, realmUrl) { provider: 'realm', app_id: '' }), - headers: postHeaders + headers: postHeaders, + // FIXME: This timeout appears to be necessary in order for some requests to be sent at all. + // See https://github.com/realm/realm-js-private/issues/338 for details. + timeout: 1000.0 }; performFetch(url, options) .then((response) => response.json().then((json) => { return { response, json }; })) diff --git a/package.json b/package.json index 012a4e95..cd60f701 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-rc11", + "version": "2.0.0-rc13", "license": "Apache-2.0", "homepage": "https://realm.io", "keywords": [ diff --git a/scripts/find-ios-device.rb b/scripts/find-ios-device.rb new file mode 100644 index 00000000..cb5a2a51 --- /dev/null +++ b/scripts/find-ios-device.rb @@ -0,0 +1,24 @@ +#!/usr/bin/ruby + +require 'json' + +ios_sim_default_device_type = ENV["IOS_SIM_DEVICE_TYPE"] or "iPhone 5s" +ios_sim_default_ios_version = ENV["IOS_SIM_OS"] or "iOS 10.1" +mode = ARGV[0] + +devices = JSON.parse(%x{xcrun simctl list devices --json})['devices'] + .each { |os, group| group.each{ |dev| dev['os'] = os } } + .flat_map { |x| x[1] } +if mode == "booted" then + device = devices.select{|x| x['state'] == 'Booted'} +else + device = devices + .select{ |x| x['availability'] == '(available)' } + .each { |x| x['score'] = (x['name'] == '$ios_sim_default_device_type' ? 1 : 0) + (x['os'] == '$ios_sim_default_ios_version' ? 1 : 0) } + .sort_by! { |x| [x['score'], x['name']] } + .reverse! +end + +if device and device[0] then + puts device[0]['udid'] +end diff --git a/scripts/test.sh b/scripts/test.sh index 0aac32ec..c12393db 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -14,8 +14,6 @@ if echo "$CONFIGURATION" | grep -i "^Debug$" > /dev/null ; then fi IOS_SIM_DEVICE=${IOS_SIM_DEVICE:-} # use preferentially, otherwise will be set and re-exported -ios_sim_default_device_type=${IOS_SIM_DEVICE_TYPE:-iPhone 5s} -ios_sim_default_ios_version=${IOS_SIM_OS:-iOS 10.1} PATH="/opt/android-sdk-linux/platform-tools:$PATH" SRCROOT=$(cd "$(dirname "$0")/.." && pwd) @@ -165,42 +163,43 @@ setup_ios_simulator() { # -- Ensure that the simulator is ready if [ $CI_RUN == true ]; then - # - Kill the Simulator to ensure we are running the correct one, only when running in CI - echo "Resetting simulator using toolchain from: $DEVELOPER_DIR" + # - Kill the Simulator to ensure we are running the correct one, only when running in CI + echo "Resetting simulator using toolchain from: $DEVELOPER_DIR" - # Quit Simulator.app to give it a chance to go down gracefully - local deadline=$((SECONDS+5)) - while pgrep -qx Simulator && [ $SECONDS -lt $deadline ]; do - osascript -e 'tell app "Simulator" to quit without saving' || true - sleep 0.25 # otherwise the pkill following will get it too early - done + # Quit Simulator.app to give it a chance to go down gracefully + local deadline=$((SECONDS+5)) + while pgrep -qx Simulator && [ $SECONDS -lt $deadline ]; do + osascript -e 'tell app "Simulator" to quit without saving' || true + sleep 0.25 # otherwise the pkill following will get it too early + done - # stop CoreSimulatorService - launchctl remove com.apple.CoreSimulator.CoreSimulatorService 2>/dev/null || true - sleep 0.25 # launchtl can take a small moment to kill services + # stop CoreSimulatorService + launchctl remove com.apple.CoreSimulator.CoreSimulatorService 2>/dev/null || true + sleep 0.25 # launchtl can take a small moment to kill services - # kill them with fire - while pgrep -qx Simulator com.apple.CoreSimulator.CoreSimulatorService; do - pkill -9 -x Simulator com.apple.CoreSimulator.CoreSimulatorService || true - sleep 0.05 - done + # kill them with fire + while pgrep -qx Simulator com.apple.CoreSimulator.CoreSimulatorService; do + pkill -9 -x Simulator com.apple.CoreSimulator.CoreSimulatorService || true + sleep 0.05 + done - # - Prod `simctl` a few times as sometimes it fails the first couple of times after switching XCode vesions - local deadline=$((SECONDS+5)) - while [ -z "$(xcrun simctl list devices 2>/dev/null)" ] && [ $SECONDS -lt $deadline ]; do - : # nothing to see here, will stop cycling on the first successful run - done + # - Prod `simctl` a few times as sometimes it fails the first couple of times after switching XCode vesions + local deadline=$((SECONDS+5)) + while [ -z "$(xcrun simctl list devices 2>/dev/null)" ] && [ $SECONDS -lt $deadline ]; do + : # nothing to see here, will stop cycling on the first successful run + done # - Choose a device, if it has not already been chosen local deadline=$((SECONDS+5)) - while [ -z "$IOS_SIM_DEVICE" ] && [ $SECONDS -lt $deadline ]; do - IOS_DEVICE=$(ruby -rjson -e "puts JSON.parse(%x{xcrun simctl list devices --json})['devices'].each{|os,group| group.each{|dev| dev['os'] = os}}.flat_map{|x| x[1]}.select{|x| x['availability'] == '(available)'}.each{|x| x['score'] = (x['name'] == '$ios_sim_default_device_type' ? 1 : 0) + (x['os'] == '$ios_sim_default_ios_version' ? 1 : 0)}.sort_by!{|x| [x['score'], x['name']]}.reverse![0]['udid']") - export IOS_SIM_DEVICE=$IOS_DEVICE + IOS_DEVICE="" + while [ -z "$IOS_DEVICE" ] && [ $SECONDS -lt $deadline ]; do + IOS_DEVICE="$(ruby $SRCROOT/scripts/find-ios-device.rb best)" done - if [ -z "$IOS_SIM_DEVICE" ]; then + if [ -z "$IOS_DEVICE" ]; then echo "*** Failed to determine the iOS Simulator device to use ***" exit 1 fi + export IOS_SIM_DEVICE=$IOS_DEVICE # - Reset the device we will be using if running in CI xcrun simctl shutdown "$IOS_SIM_DEVICE" 1>/dev/null 2>/dev/null || true # sometimes simctl gets confused @@ -214,19 +213,20 @@ setup_ios_simulator() { startedSimulator=true else - # - ensure that the simulator is running on a developer's workstation + # - ensure that the simulator is running on a developer's workstation open "$DEVELOPER_DIR/Applications/Simulator.app" # - Select the first device booted in the simulator, since it will boot something for us local deadline=$((SECONDS+10)) - while [ -z "$IOS_SIM_DEVICE" ] && [ $SECONDS -lt $deadline ]; do - IOS_DEVICE=$(ruby -rjson -e "puts JSON.parse(%x{xcrun simctl list devices --json})['devices'].each{|os,group| group.each{|dev| dev['os'] = os}}.flat_map{|x| x[1]}.select{|x| x['state'] == 'Booted'}[0]['udid']") - export IOS_SIM_DEVICE=$IOS_DEVICE + IOS_DEVICE="" + while [ -z "$IOS_DEVICE" ] && [ $SECONDS -lt $deadline ]; do + IOS_DEVICE="$(ruby $SRCROOT/scripts/find-ios-device.rb booted)" done - if [ -z "$IOS_SIM_DEVICE" ]; then + if [ -z "$IOS_DEVICE" ]; then echo "*** Failed to determine the iOS Simulator device in use ***" exit 1 fi + export IOS_SIM_DEVICE=$IOS_DEVICE fi # Wait until the boot completes diff --git a/src/js_class.hpp b/src/js_class.hpp index 052ec8f5..e7bd7d43 100644 --- a/src/js_class.hpp +++ b/src/js_class.hpp @@ -54,7 +54,7 @@ struct Arguments { }; template -using ArgumentsMethodType = void(typename T::Context, typename T::Function, typename T::Object, Arguments, ReturnValue &); +using ArgumentsMethodType = void(typename T::Context, typename T::Object, Arguments, ReturnValue &); template struct PropertyType { diff --git a/src/js_list.hpp b/src/js_list.hpp index c7d7e238..0bb33ef2 100644 --- a/src/js_list.hpp +++ b/src/js_list.hpp @@ -54,30 +54,33 @@ struct ListClass : ClassDefinition, CollectionClass> { using Object = js::Object; using Value = js::Value; using ReturnValue = js::ReturnValue; + using Arguments = js::Arguments; static ObjectType create_instance(ContextType, realm::List); // properties static void get_length(ContextType, ObjectType, ReturnValue &); + static void get_type(ContextType, ObjectType, ReturnValue &); + static void get_optional(ContextType, ObjectType, ReturnValue &); static void get_index(ContextType, ObjectType, uint32_t, ReturnValue &); static bool set_index(ContextType, ObjectType, uint32_t, ValueType); // methods - static void push(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void pop(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void unshift(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void shift(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void splice(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void snapshot(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void filtered(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void sorted(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void is_valid(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void index_of(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void push(ContextType, ObjectType, Arguments, ReturnValue &); + static void pop(ContextType, ObjectType, Arguments, ReturnValue &); + static void unshift(ContextType, ObjectType, Arguments, ReturnValue &); + static void shift(ContextType, ObjectType, Arguments, ReturnValue &); + static void splice(ContextType, ObjectType, Arguments, ReturnValue &); + static void snapshot(ContextType, ObjectType, Arguments, ReturnValue &); + static void filtered(ContextType, ObjectType, Arguments, ReturnValue &); + static void sorted(ContextType, ObjectType, Arguments, ReturnValue &); + static void is_valid(ContextType, ObjectType, Arguments, ReturnValue &); + static void index_of(ContextType, ObjectType, Arguments, ReturnValue &); // observable - static void add_listener(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 add_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); std::string const name = "List"; @@ -99,9 +102,14 @@ struct ListClass : ClassDefinition, CollectionClass> { PropertyMap const properties = { {"length", {wrap, nullptr}}, + {"type", {wrap, nullptr}}, + {"optional", {wrap, nullptr}}, }; IndexPropertyType const index_accessor = {wrap, wrap}; + +private: + static void validate_value(ContextType, realm::List&, ValueType); }; template @@ -115,70 +123,83 @@ void ListClass::get_length(ContextType, ObjectType object, ReturnValue &retur return_value.set((uint32_t)list->size()); } +template +void ListClass::get_type(ContextType, ObjectType object, ReturnValue &return_value) { + auto list = get_internal>(object); + return_value.set(string_for_property_type(list->get_type() & ~realm::PropertyType::Flags)); +} + +template +void ListClass::get_optional(ContextType, ObjectType object, ReturnValue &return_value) { + auto list = get_internal>(object); + return_value.set(is_nullable(list->get_type())); +} + template void ListClass::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { auto list = get_internal>(object); - auto realm_object = realm::Object(list->get_realm(), list->get_object_schema(), list->get(index)); - - return_value.set(RealmObjectClass::create_instance(ctx, std::move(realm_object))); + NativeAccessor accessor(ctx, *list); + return_value.set(list->get(accessor, index)); } template bool ListClass::set_index(ContextType ctx, ObjectType object, uint32_t index, ValueType value) { auto list = get_internal>(object); - NativeAccessor accessor(ctx, list->get_realm(), list->get_object_schema()); + validate_value(ctx, *list, value); + NativeAccessor accessor(ctx, *list); list->set(accessor, index, value); return true; } template -void ListClass::push(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count_at_least(argc, 1); - +void ListClass::push(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); - NativeAccessor accessor(ctx, list->get_realm(), list->get_object_schema()); - for (size_t i = 0; i < argc; i++) { - list->add(accessor, arguments[i]); + for (size_t i = 0; i < args.count; i++) { + validate_value(ctx, *list, args[i]); + } + + NativeAccessor accessor(ctx, *list); + for (size_t i = 0; i < args.count; i++) { + list->add(accessor, args[i]); } return_value.set((uint32_t)list->size()); } template -void ListClass::pop(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +void ListClass::pop(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); auto list = get_internal>(this_object); - size_t size = list->size(); + auto size = static_cast(list->size()); if (size == 0) { list->verify_in_transaction(); return_value.set_undefined(); } else { - size_t index = size - 1; - auto realm_object = realm::Object(list->get_realm(), list->get_object_schema(), list->get(index)); - - return_value.set(RealmObjectClass::create_instance(ctx, std::move(realm_object))); - list->remove(index); + get_index(ctx, this_object, size - 1, return_value); + list->remove(size - 1); } } template -void ListClass::unshift(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count_at_least(argc, 1); - +void ListClass::unshift(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); - NativeAccessor accessor(ctx, list->get_realm(), list->get_object_schema()); - for (size_t i = 0; i < argc; i++) { - list->insert(accessor, i, arguments[i]); + for (size_t i = 0; i < args.count; i++) { + validate_value(ctx, *list, args[i]); + } + + NativeAccessor accessor(ctx, *list); + for (size_t i = 0; i < args.count; i++) { + list->insert(accessor, i, args[i]); } return_value.set((uint32_t)list->size()); } template -void ListClass::shift(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +void ListClass::shift(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); auto list = get_internal>(this_object); if (list->size() == 0) { @@ -186,151 +207,108 @@ void ListClass::shift(ContextType ctx, FunctionType, ObjectType this_object, return_value.set_undefined(); } else { - auto realm_object = realm::Object(list->get_realm(), list->get_object_schema(), list->get(0)); - - return_value.set(RealmObjectClass::create_instance(ctx, std::move(realm_object))); + get_index(ctx, this_object, 0, return_value); list->remove(0); } } template -void ListClass::splice(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count_at_least(argc, 1); - +void ListClass::splice(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); size_t size = list->size(); - long index = std::min(Value::to_number(ctx, arguments[0]), size); + long index = std::min(Value::to_number(ctx, args[0]), size); if (index < 0) { index = std::max(size + index, 0); } size_t remove; - if (argc < 2) { + if (args.count < 2) { remove = size - index; } else { - remove = std::max(Value::to_number(ctx, arguments[1]), 0); + remove = std::max(Value::to_number(ctx, args[1]), 0); remove = std::min(remove, size - index); } std::vector removed_objects; removed_objects.reserve(remove); - NativeAccessor accessor(ctx, list->get_realm(), list->get_object_schema()); + NativeAccessor accessor(ctx, *list); for (size_t i = 0; i < remove; i++) { - auto realm_object = realm::Object(list->get_realm(), list->get_object_schema(), list->get(index)); - - removed_objects.push_back(RealmObjectClass::create_instance(ctx, std::move(realm_object))); + removed_objects.push_back(list->get(accessor, index)); list->remove(index); } - for (size_t i = 2; i < argc; i++) { - list->insert(accessor, index + i - 2, arguments[i]); + for (size_t i = 2; i < args.count; i++) { + list->insert(accessor, index + i - 2, args[i]); } return_value.set(Object::create_array(ctx, removed_objects)); } template -void ListClass::snapshot(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); - +void ListClass::snapshot(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); auto list = get_internal>(this_object); return_value.set(ResultsClass::create_instance(ctx, list->snapshot())); } template -void ListClass::filtered(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count_at_least(argc, 1); - +void ListClass::filtered(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); - return_value.set(ResultsClass::create_filtered(ctx, *list, argc, arguments)); + return_value.set(ResultsClass::create_filtered(ctx, *list, args)); } template -void ListClass::sorted(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { +void ListClass::sorted(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); - return_value.set(ResultsClass::create_instance(ctx, list->sort(ResultsClass::get_keypaths(ctx, argc, arguments)))); + return_value.set(ResultsClass::create_instance(ctx, list->sort(ResultsClass::get_keypaths(ctx, args)))); } template -void ListClass::is_valid(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { +void ListClass::is_valid(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { return_value.set(get_internal>(this_object)->is_valid()); } template -void ListClass::index_of(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - - ObjectType arg = Value::validated_to_object(ctx, arguments[0]); - if (Object::template is_instance>(ctx, arg)) { - auto object = get_internal>(arg); - if (!object->is_valid()) { - throw std::runtime_error("Object is invalid. Either it has been previously deleted or the Realm it belongs to has been closed."); - } - +void ListClass::index_of(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + auto fn = [&](auto&& row) { auto list = get_internal>(this_object); - size_t ndx = list->find(object->row()); - - if (ndx == realm::not_found) { - return_value.set(-1); - } - else { - return_value.set((uint32_t)ndx); - } - } - else { - return_value.set(-1); - } + NativeAccessor accessor(ctx, *list); + return list->find(accessor, row); + }; + ResultsClass::index_of(ctx, fn, args, return_value); } template -void ListClass::add_listener(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - +void ListClass::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); - auto callback = Value::validated_to_function(ctx, arguments[0]); - Protected protected_callback(ctx, callback); - Protected protected_this(ctx, this_object); - Protected protected_ctx(Context::get_global_context(ctx)); - - auto token = list->add_notification_callback([=](CollectionChangeSet change_set, std::exception_ptr exception) { - HANDLESCOPE - - ValueType arguments[2]; - arguments[0] = static_cast(protected_this); - arguments[1] = CollectionClass::create_collection_change_set(protected_ctx, change_set); - Function::callback(protected_ctx, protected_callback, protected_this, 2, arguments); - }); - list->m_notification_tokens.emplace_back(protected_callback, std::move(token)); + ResultsClass::add_listener(ctx, *list, this_object, args); } template -void ListClass::remove_listener(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - +void ListClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto list = get_internal>(this_object); - auto callback = Value::validated_to_function(ctx, arguments[0]); - auto protected_function = Protected(ctx, callback); - - auto iter = list->m_notification_tokens.begin(); - typename Protected::Comparator compare; - while (iter != list->m_notification_tokens.end()) { - if(compare(iter->first, protected_function)) { - iter = list->m_notification_tokens.erase(iter); - } - else { - iter++; - } - } + ResultsClass::remove_listener(ctx, *list, this_object, args); } - -template -void ListClass::remove_all_listeners(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +template +void ListClass::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); auto list = get_internal>(this_object); list->m_notification_tokens.clear(); } +template +void ListClass::validate_value(ContextType ctx, realm::List& list, ValueType value) { + auto type = list.get_type(); + StringData object_type; + if (type == realm::PropertyType::Object) { + object_type = list.get_object_schema().name; + } + if (!Value::is_valid_for_property_type(ctx, value, type, object_type)) { + throw TypeErrorException("Property", object_type ? object_type : string_for_property_type(type), Value::to_string(ctx, value)); + } +} + } // js } // realm diff --git a/src/js_object_accessor.hpp b/src/js_object_accessor.hpp index 14cbeb1d..4fc116e5 100644 --- a/src/js_object_accessor.hpp +++ b/src/js_object_accessor.hpp @@ -47,12 +47,19 @@ public: using OptionalValue = util::Optional; NativeAccessor(ContextType ctx, std::shared_ptr realm, const ObjectSchema& object_schema) - : m_ctx(ctx), m_realm(std::move(realm)), m_object_schema(object_schema) { } + : m_ctx(ctx), m_realm(std::move(realm)), m_object_schema(&object_schema) { } + + template + NativeAccessor(ContextType ctx, Collection const& collection) + : m_ctx(ctx) + , m_realm(collection.get_realm()) + , m_object_schema(collection.get_type() == realm::PropertyType::Object ? &collection.get_object_schema() : nullptr) + { } NativeAccessor(NativeAccessor& parent, const Property& prop) : m_ctx(parent.m_ctx) , m_realm(parent.m_realm) - , m_object_schema(*m_realm->schema().find(prop.object_type)) + , m_object_schema(&*m_realm->schema().find(prop.object_type)) { } OptionalValue value_for_property(ValueType dict, std::string const& prop_name, size_t prop_index) { @@ -61,11 +68,9 @@ public: return util::none; } ValueType value = Object::get_property(m_ctx, object, prop_name); - const auto& prop = m_object_schema.persisted_properties[prop_index]; + const auto& prop = m_object_schema->persisted_properties[prop_index]; if (!Value::is_valid_for_property(m_ctx, value, prop)) { - throw TypeErrorException(m_object_schema.name, prop.name, - js_type_name_for_property_type(prop.type), - print(value)); + throw TypeErrorException(*this, m_object_schema->name, prop, value); } return value; } @@ -79,6 +84,14 @@ public: template T unbox(ValueType value, bool create = false, bool update = false); + template + util::Optional unbox_optional(ValueType value) { + return is_null(value) ? util::none : util::make_optional(unbox(value)); + } + + template + ValueType box(util::Optional v) { return v ? box(*v) : null_value(); } + ValueType box(bool boolean) { return Value::from_boolean(m_ctx, boolean); } ValueType box(int64_t number) { return Value::from_number(m_ctx, number); } ValueType box(float number) { return Value::from_number(m_ctx, number); } @@ -88,11 +101,20 @@ public: ValueType box(Mixed) { throw std::runtime_error("'Any' type is unsupported"); } ValueType box(Timestamp ts) { + if (ts.is_null()) { + return null_value(); + } return Object::create_date(m_ctx, ts.get_seconds() * 1000 + ts.get_nanoseconds() / 1000000); } ValueType box(realm::Object realm_object) { return RealmObjectClass::create_instance(m_ctx, std::move(realm_object)); } + ValueType box(RowExpr row) { + if (!row.is_attached()) { + return Value::from_null(m_ctx); + } + return RealmObjectClass::create_instance(m_ctx, realm::Object(m_realm, *m_object_schema, row)); + } ValueType box(realm::List list) { return ListClass::create_instance(m_ctx, std::move(list)); } @@ -112,7 +134,7 @@ public: auto obj = Value::validated_to_object(m_ctx, value); uint32_t size = Object::validated_get_length(m_ctx, obj); for (uint32_t i = 0; i < size; ++i) { - func(Object::validated_get_object(m_ctx, obj, i)); + func(Object::get_property(m_ctx, obj, i)); } } @@ -128,12 +150,14 @@ public: void will_change(realm::Object&, realm::Property const&) { } void did_change() { } - std::string print(ValueType const& v) { return Value::to_string(m_ctx, v); } + std::string print(ValueType const&); + void print(std::string&, ValueType const&); + const char *typeof(ValueType const& v) { return Value::typeof(m_ctx, v); } private: ContextType m_ctx; std::shared_ptr m_realm; - const ObjectSchema& m_object_schema; + const ObjectSchema* m_object_schema; std::string m_string_buffer; OwnedBinaryData m_owned_binary_data; @@ -173,34 +197,37 @@ struct Unbox { template struct Unbox> { static util::Optional call(NativeAccessor *ctx, typename JSEngine::Value const& value, bool, bool) { - return js::Value::validated_to_boolean(ctx->m_ctx, value, "Property"); + return ctx->template unbox_optional(value); } }; template struct Unbox> { static util::Optional call(NativeAccessor *ctx, typename JSEngine::Value const& value, bool, bool) { - return js::Value::validated_to_number(ctx->m_ctx, value, "Property"); + return ctx->template unbox_optional(value); } }; template struct Unbox> { static util::Optional call(NativeAccessor *ctx, typename JSEngine::Value const& value, bool, bool) { - return js::Value::validated_to_number(ctx->m_ctx, value, "Property"); + return ctx->template unbox_optional(value); } }; template struct Unbox> { static util::Optional call(NativeAccessor *ctx, typename JSEngine::Value const& value, bool, bool) { - return js::Value::validated_to_number(ctx->m_ctx, value, "Property"); + return ctx->template unbox_optional(value); } }; template struct Unbox { static StringData call(NativeAccessor *ctx, typename JSEngine::Value const& value, bool, bool) { + if (ctx->is_null(value)) { + return StringData(); + } ctx->m_string_buffer = js::Value::validated_to_string(ctx->m_ctx, value, "Property"); return ctx->m_string_buffer; } @@ -209,6 +236,9 @@ struct Unbox { template struct Unbox { static BinaryData call(NativeAccessor *ctx, typename JSEngine::Value value, bool, bool) { + if (ctx->is_null(value)) { + return BinaryData(); + } ctx->m_owned_binary_data = js::Value::validated_to_binary(ctx->m_ctx, value, "Property"); return ctx->m_owned_binary_data.get(); } @@ -224,6 +254,9 @@ struct Unbox { template struct Unbox { static Timestamp call(NativeAccessor *ctx, typename JSEngine::Value const& value, bool, bool) { + if (ctx->is_null(value)) { + return Timestamp(); + } auto date = js::Value::validated_to_date(ctx->m_ctx, value, "Property"); double milliseconds = js::Value::to_number(ctx->m_ctx, date); int64_t seconds = milliseconds / 1000; @@ -248,15 +281,15 @@ struct Unbox { throw std::runtime_error("Realm object is from another Realm"); } } - else if (!create) { - throw std::runtime_error("object is not a Realm Object"); + if (!create) { + throw NonRealmObjectException(); } if (Value::is_array(ctx->m_ctx, object)) { - object = Schema::dict_for_property_array(ctx->m_ctx, ctx->m_object_schema, object); + object = Schema::dict_for_property_array(ctx->m_ctx, *ctx->m_object_schema, object); } - auto child = realm::Object::create(*ctx, ctx->m_realm, ctx->m_object_schema, + auto child = realm::Object::create(*ctx, ctx->m_realm, *ctx->m_object_schema, static_cast(object), try_update); return child.row(); } @@ -269,6 +302,62 @@ U NativeAccessor::unbox(ValueType value, bool create, bool update) { return _impl::Unbox::call(this, std::move(value), create, update); } +template +std::string NativeAccessor::print(ValueType const& value) { + std::string ret; + print(ret, value); + return ret; +} + +template +void NativeAccessor::print(std::string& str, ValueType const& value) { + if (Value::is_null(m_ctx, value)) { + str += "null"; + } + else if (Value::is_undefined(m_ctx, value)) { + str += "undefined"; + } + else if (Value::is_array(m_ctx, value)) { + auto array = Value::to_array(m_ctx, value); + auto length = Object::validated_get_length(m_ctx, array); + + str += "["; + for (uint32_t i = 0; i < length; i++) { + print(str, Object::get_property(m_ctx, array, i)); + if (i + 1 < length) { + str += ", "; + } + } + str += "]"; + } + else if (Value::is_object(m_ctx, value)) { + auto object = Value::to_object(m_ctx, value); + if (Object::template is_instance>(m_ctx, object)) { + auto realm_object = get_internal>(object); + auto& object_schema = realm_object->get_object_schema(); + str += object_schema.name; + str += "{"; + for (size_t i = 0, count = object_schema.persisted_properties.size(); i < count; ++i) { + print(str, realm_object->template get_property_value(*this, object_schema.persisted_properties[i].name)); + if (i + 1 < count) { + str += ", "; + } + } + str += "}"; + } + else { + str += Value::to_string(m_ctx, value); + } + } + else if (Value::is_string(m_ctx, value)) { + str += "'"; + str += Value::to_string(m_ctx, value); + str += "'"; + } + else { + str += Value::to_string(m_ctx, value); + } +} } // js } // realm diff --git a/src/js_realm.cpp b/src/js_realm.cpp index 0b974620..97024c81 100644 --- a/src/js_realm.cpp +++ b/src/js_realm.cpp @@ -18,6 +18,7 @@ #include "platform.hpp" #include "realm_coordinator.hpp" +#include "js_types.hpp" #if REALM_ENABLE_SYNC #include "sync/sync_manager.hpp" @@ -61,6 +62,49 @@ void clear_test_state() { SyncManager::shared().configure_file_system(default_realm_file_directory(), SyncManager::MetadataMode::NoEncryption); #endif } + +std::string TypeErrorException::type_string(Property const& prop) +{ + using realm::PropertyType; + std::string ret; + + switch (prop.type & ~PropertyType::Flags) { + case PropertyType::Int: + case PropertyType::Float: + case PropertyType::Double: + ret = "number"; + break; + case PropertyType::Bool: + ret = "boolean"; + break; + case PropertyType::String: + ret = "string"; + break; + case PropertyType::Date: + ret = "date"; + break; + case PropertyType::Data: + ret = "binary"; + break; + case PropertyType::LinkingObjects: + case PropertyType::Object: + ret = prop.object_type; + break; + case PropertyType::Any: + throw std::runtime_error("'Any' type is not supported"); + default: + REALM_UNREACHABLE(); + } + + if (realm::is_nullable(prop.type)) { + ret += "?"; + } + if (realm::is_array(prop.type)) { + ret += "[]"; + } + return ret; +} + } // js } // realm diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 90981bb9..ad8775e5 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -164,22 +164,22 @@ public: static FunctionType create_constructor(ContextType); // methods - 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 &); + static void objects(ContextType, ObjectType, Arguments, ReturnValue &); + static void object_for_primary_key(ContextType, ObjectType, Arguments, ReturnValue &); + static void create(ContextType, ObjectType, Arguments, ReturnValue &); + static void delete_one(ContextType, ObjectType, Arguments, ReturnValue &); + static void delete_all(ContextType, ObjectType, Arguments, ReturnValue &); + static void write(ContextType, ObjectType, Arguments, ReturnValue &); + static void begin_transaction(ContextType, ObjectType, Arguments, ReturnValue&); + static void commit_transaction(ContextType, ObjectType, Arguments, ReturnValue&); + static void cancel_transaction(ContextType, ObjectType, Arguments, ReturnValue&); + static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void wait_for_download_completion(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); + static void close(ContextType, ObjectType, Arguments, ReturnValue &); + static void compact(ContextType, ObjectType, Arguments, ReturnValue &); + static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -197,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, 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 void schema_version(ContextType, ObjectType, Arguments, ReturnValue &); + static void clear_test_state(ContextType, ObjectType, Arguments, ReturnValue &); + static void copy_bundled_realm_files(ContextType, ObjectType, Arguments, ReturnValue &); + static void delete_file(ContextType, ObjectType, Arguments, ReturnValue &); // static properties static void get_default_path(ContextType, ObjectType, ReturnValue &); @@ -503,7 +503,7 @@ SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Co } template -void RealmClass::schema_version(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::schema_version(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(2); realm::Realm::Config config; @@ -524,19 +524,19 @@ void RealmClass::schema_version(ContextType ctx, FunctionType, ObjectType thi template -void RealmClass::clear_test_state(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::clear_test_state(ContextType ctx, 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, Arguments args, ReturnValue &return_value) { +void RealmClass::copy_bundled_realm_files(ContextType ctx, 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, Arguments args, ReturnValue &return_value) { +void RealmClass::delete_file(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); ValueType value = args[0]; @@ -567,7 +567,7 @@ void RealmClass::delete_file(ContextType ctx, FunctionType, ObjectType this_o } template -void RealmClass::delete_model(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::delete_model(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); ValueType value = args[0]; @@ -641,7 +641,7 @@ void RealmClass::get_sync_session(ContextType ctx, ObjectType object, ReturnV #endif template -void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::wait_for_download_completion(ContextType ctx, 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)]); @@ -743,7 +743,8 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, } ObjectType object = Object::create_empty(protected_ctx); - Object::set_property(protected_ctx, object, "message", Value::from_string(protected_ctx, "Cannot asynchronously open synced Realm, because the associated session previously experienced a fatal error")); + Object::set_property(protected_ctx, object, "message", + Value::from_string(protected_ctx, "Cannot asynchronously open synced Realm because the associated session previously experienced a fatal error")); Object::set_property(protected_ctx, object, "errorCode", Value::from_number(protected_ctx, 1)); ValueType callback_arguments[1]; @@ -760,7 +761,7 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, } template -void RealmClass::objects(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); @@ -771,7 +772,7 @@ void RealmClass::objects(ContextType ctx, FunctionType, ObjectType this_objec } template -void RealmClass::object_for_primary_key(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::object_for_primary_key(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(2); SharedRealm realm = *get_internal>(this_object); @@ -789,7 +790,7 @@ void RealmClass::object_for_primary_key(ContextType ctx, FunctionType, Object } template -void RealmClass::create(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::create(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(3); SharedRealm realm = *get_internal>(this_object); @@ -813,7 +814,7 @@ void RealmClass::create(ContextType ctx, FunctionType, ObjectType this_object } template -void RealmClass::delete_one(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::delete_one(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); @@ -861,7 +862,7 @@ void RealmClass::delete_one(ContextType ctx, FunctionType, ObjectType this_ob } template -void RealmClass::delete_all(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::delete_all(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); @@ -877,7 +878,7 @@ void RealmClass::delete_all(ContextType ctx, FunctionType, ObjectType this_ob } template -void RealmClass::write(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::write(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); SharedRealm realm = *get_internal>(this_object); @@ -897,7 +898,7 @@ void RealmClass::write(ContextType ctx, FunctionType, ObjectType this_object, } template -void RealmClass::begin_transaction(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::begin_transaction(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); @@ -905,7 +906,7 @@ void RealmClass::begin_transaction(ContextType ctx, FunctionType, ObjectType } template -void RealmClass::commit_transaction(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::commit_transaction(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); @@ -913,7 +914,7 @@ void RealmClass::commit_transaction(ContextType ctx, FunctionType, ObjectType } template -void RealmClass::cancel_transaction(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::cancel_transaction(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); @@ -921,7 +922,7 @@ void RealmClass::cancel_transaction(ContextType ctx, FunctionType, ObjectType } template -void RealmClass::add_listener(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(2); validated_notification_name(ctx, args[0]); @@ -933,7 +934,7 @@ void RealmClass::add_listener(ContextType ctx, FunctionType, ObjectType this_ } template -void RealmClass::remove_listener(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(2); validated_notification_name(ctx, args[0]); @@ -945,7 +946,7 @@ void RealmClass::remove_listener(ContextType ctx, FunctionType, ObjectType th } template -void RealmClass::remove_all_listeners(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); if (args.count) { validated_notification_name(ctx, args[0]); @@ -957,7 +958,7 @@ void RealmClass::remove_all_listeners(ContextType ctx, FunctionType, ObjectTy } template -void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::close(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); @@ -965,7 +966,7 @@ void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, } template -void RealmClass::compact(ContextType ctx, FunctionType, ObjectType this_object, Arguments args, ReturnValue &return_value) { +void RealmClass::compact(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(0); SharedRealm realm = *get_internal>(this_object); diff --git a/src/js_realm_object.hpp b/src/js_realm_object.hpp index 78238277..23ed86de 100644 --- a/src/js_realm_object.hpp +++ b/src/js_realm_object.hpp @@ -106,14 +106,12 @@ typename T::Object RealmObjectClass::create_instance(ContextType ctx, realm:: template void RealmObjectClass::get_property(ContextType ctx, ObjectType object, const String &property, ReturnValue &return_value) { - try { - auto realm_object = get_internal>(object); + auto realm_object = get_internal>(object); + std::string name = property; + if (realm_object->get_object_schema().property_for_name(name)) { NativeAccessor accessor(ctx, realm_object->realm(), realm_object->get_object_schema()); - std::string name = property; auto result = realm_object->template get_property_value(accessor, name); return_value.set(result); - } catch (InvalidPropertyException &ex) { - // getters for nonexistent properties in JS should always return undefined } } @@ -129,9 +127,7 @@ bool RealmObjectClass::set_property(ContextType ctx, ObjectType object, const NativeAccessor accessor(ctx, realm_object->realm(), realm_object->get_object_schema()); if (!Value::is_valid_for_property(ctx, value, *prop)) { - throw TypeErrorException(realm_object->get_object_schema().name, property_name, - js_type_name_for_property_type(prop->type), - accessor.print(value)); + throw TypeErrorException(accessor, realm_object->get_object_schema().name, *prop, value); } realm_object->set_property_value(accessor, property_name, value, true); @@ -141,12 +137,15 @@ bool RealmObjectClass::set_property(ContextType ctx, ObjectType object, const template std::vector> RealmObjectClass::get_property_names(ContextType ctx, ObjectType object) { auto realm_object = get_internal>(object); - auto &properties = realm_object->get_object_schema().persisted_properties; + auto &object_schema = realm_object->get_object_schema(); std::vector names; - names.reserve(properties.size()); + names.reserve(object_schema.persisted_properties.size() + object_schema.computed_properties.size()); - for (auto &prop : properties) { + for (auto &prop : object_schema.persisted_properties) { + names.push_back(prop.name); + } + for (auto &prop : object_schema.computed_properties) { names.push_back(prop.name); } diff --git a/src/js_results.hpp b/src/js_results.hpp index 0fb0b58b..afb6e43f 100644 --- a/src/js_results.hpp +++ b/src/js_results.hpp @@ -33,6 +33,10 @@ namespace js { template class NativeAccessor; +struct NonRealmObjectException : public std::logic_error { + NonRealmObjectException() : std::logic_error("Object is not a Realm object") { } +}; + template class Results : public realm::Results { public: @@ -56,29 +60,40 @@ struct ResultsClass : ClassDefinition, CollectionClass< using Object = js::Object; using Value = js::Value; using ReturnValue = js::ReturnValue; + using Arguments = js::Arguments; static ObjectType create_instance(ContextType, realm::Results); static ObjectType create_instance(ContextType, SharedRealm, const std::string &object_type); template - static ObjectType create_filtered(ContextType, const U &, size_t, const ValueType[]); + static ObjectType create_filtered(ContextType, const U &, Arguments); - static std::vector> get_keypaths(ContextType, size_t, const ValueType[]); + static std::vector> get_keypaths(ContextType, Arguments); static void get_length(ContextType, ObjectType, ReturnValue &); + static void get_type(ContextType, ObjectType, ReturnValue &); + static void get_optional(ContextType, ObjectType, ReturnValue &); static void get_index(ContextType, ObjectType, uint32_t, ReturnValue &); - static void snapshot(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void filtered(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void sorted(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void is_valid(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void snapshot(ContextType, ObjectType, Arguments, ReturnValue &); + static void filtered(ContextType, ObjectType, Arguments, ReturnValue &); + static void sorted(ContextType, ObjectType, Arguments, ReturnValue &); + static void is_valid(ContextType, ObjectType, Arguments, ReturnValue &); - static void index_of(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void index_of(ContextType, ObjectType, Arguments, ReturnValue &); + + template + static void index_of(ContextType, Fn&, Arguments, ReturnValue &); // observable - static void add_listener(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 add_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &); + + template + static void add_listener(ContextType, U&, ObjectType, Arguments); + template + static void remove_listener(ContextType, U&, ObjectType, Arguments); std::string const name = "Results"; @@ -95,6 +110,8 @@ struct ResultsClass : ClassDefinition, CollectionClass< PropertyMap const properties = { {"length", {wrap, nullptr}}, + {"type", {wrap, nullptr}}, + {"optional", {wrap, nullptr}}, }; IndexPropertyType const index_accessor = {wrap, nullptr}; @@ -116,15 +133,19 @@ typename T::Object ResultsClass::create_instance(ContextType ctx, SharedRealm template template -typename T::Object ResultsClass::create_filtered(ContextType ctx, const U &collection, size_t argc, const ValueType arguments[]) { - auto query_string = Value::validated_to_string(ctx, arguments[0], "predicate"); +typename T::Object ResultsClass::create_filtered(ContextType ctx, const U &collection, Arguments args) { + if (collection.get_type() != realm::PropertyType::Object) { + throw std::runtime_error("Filtering non-object Lists and Results is not yet implemented."); + } + + auto query_string = Value::validated_to_string(ctx, args[0], "predicate"); auto query = collection.get_query(); auto const &realm = collection.get_realm(); auto const &object_schema = collection.get_object_schema(); parser::Predicate predicate = parser::parse(query_string); NativeAccessor accessor(ctx, realm, object_schema); - query_builder::ArgumentConverter> converter(accessor, &arguments[1], argc - 1); + query_builder::ArgumentConverter> converter(accessor, &args.value[1], args.count - 1); query_builder::apply_predicate(query, predicate, converter, realm->schema(), object_schema.name); return create_instance(ctx, collection.filter(std::move(query))); @@ -132,15 +153,18 @@ typename T::Object ResultsClass::create_filtered(ContextType ctx, const U &co template std::vector> -ResultsClass::get_keypaths(ContextType ctx, size_t argc, const ValueType arguments[]) { - validate_argument_count(argc, 1, 2); +ResultsClass::get_keypaths(ContextType ctx, Arguments args) { + args.validate_maximum(2); std::vector> sort_order; + if (args.count == 0) { + sort_order.emplace_back("self", true); + return sort_order; + } + else if (Value::is_array(ctx, args[0])) { + validate_argument_count(args.count, 1, "Second argument is not allowed if passed an array of sort descriptors"); - if (argc > 0 && Value::is_array(ctx, arguments[0])) { - validate_argument_count(argc, 1, "Second argument is not allowed if passed an array of sort descriptors"); - - ObjectType js_prop_names = Value::validated_to_object(ctx, arguments[0]); + ObjectType js_prop_names = Value::validated_to_object(ctx, args[0]); size_t prop_count = Object::validated_get_length(ctx, js_prop_names); sort_order.reserve(prop_count); @@ -158,8 +182,13 @@ ResultsClass::get_keypaths(ContextType ctx, size_t argc, const ValueType argu } } else { - sort_order.emplace_back(Value::validated_to_string(ctx, arguments[0]), - argc == 1 || !Value::to_boolean(ctx, arguments[1])); + if (Value::is_boolean(ctx, args[0])) { + sort_order.emplace_back("self", !Value::to_boolean(ctx, args[0])); + } + else { + sort_order.emplace_back(Value::validated_to_string(ctx, args[0]), + args.count == 1 || !Value::to_boolean(ctx, args[1])); + } } return sort_order; } @@ -171,124 +200,136 @@ void ResultsClass::get_length(ContextType ctx, ObjectType object, ReturnValue } template -void ResultsClass::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { +void ResultsClass::get_type(ContextType, ObjectType object, ReturnValue &return_value) { auto results = get_internal>(object); - auto row = results->get(index); - - // Return null for deleted objects in a snapshot. - if (!row.is_attached()) { - return_value.set_null(); - return; - } - - auto realm_object = realm::Object(results->get_realm(), results->get_object_schema(), results->get(index)); - return_value.set(RealmObjectClass::create_instance(ctx, std::move(realm_object))); + return_value.set(string_for_property_type(results->get_type() & ~realm::PropertyType::Flags)); } template -void ResultsClass::snapshot(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); +void ResultsClass::get_optional(ContextType, ObjectType object, ReturnValue &return_value) { + auto results = get_internal>(object); + return_value.set(is_nullable(results->get_type())); +} + +template +void ResultsClass::get_index(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { + auto results = get_internal>(object); + NativeAccessor accessor(ctx, *results); + return_value.set(results->get(accessor, index)); +} + +template +void ResultsClass::snapshot(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); auto results = get_internal>(this_object); return_value.set(ResultsClass::create_instance(ctx, results->snapshot())); } template -void ResultsClass::filtered(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count_at_least(argc, 1); - +void ResultsClass::filtered(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto results = get_internal>(this_object); - return_value.set(create_filtered(ctx, *results, argc, arguments)); + return_value.set(create_filtered(ctx, *results, args)); } template -void ResultsClass::sorted(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { +void ResultsClass::sorted(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto results = get_internal>(this_object); - return_value.set(ResultsClass::create_instance(ctx, results->sort(ResultsClass::get_keypaths(ctx, argc, arguments)))); + return_value.set(ResultsClass::create_instance(ctx, results->sort(ResultsClass::get_keypaths(ctx, args)))); } template -void ResultsClass::is_valid(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { +void ResultsClass::is_valid(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { return_value.set(get_internal>(this_object)->is_valid()); } template -void ResultsClass::index_of(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - - ObjectType arg = Value::validated_to_object(ctx, arguments[0]); - if (Object::template is_instance>(ctx, arg)) { - auto object = get_internal>(arg); - if (!object->is_valid()) { - throw std::runtime_error("Object is invalid. Either it has been previously deleted or the Realm it belongs to has been closed."); - } - - size_t ndx; - try { - auto results = get_internal>(this_object); - ndx = results->index_of(object->row()); - } - catch (realm::Results::IncorrectTableException &) { - throw std::runtime_error("Object type does not match the type contained in result"); - } - - if (ndx == realm::not_found) { - return_value.set(-1); - } - else { - return_value.set((uint32_t)ndx); - } +template +void ResultsClass::index_of(ContextType ctx, Fn& fn, Arguments args, ReturnValue &return_value) { + args.validate_maximum(1); + + size_t ndx; + try { + ndx = fn(args[0]); } - else { + catch (realm::Results::IncorrectTableException &) { + throw std::runtime_error("Object type does not match the type contained in result"); + } + catch (NonRealmObjectException&) { + ndx = realm::not_found; + } + + if (ndx == realm::not_found) { return_value.set(-1); } + else { + return_value.set((uint32_t)ndx); + } } - + template -void ResultsClass::add_listener(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - - auto results = get_internal>(this_object); - auto callback = Value::validated_to_function(ctx, arguments[0]); +void ResultsClass::index_of(ContextType ctx, ObjectType this_object, + Arguments args, ReturnValue &return_value) { + auto fn = [&](auto&& row) { + auto results = get_internal>(this_object); + NativeAccessor accessor(ctx, *results); + return results->index_of(accessor, row); + }; + index_of(ctx, fn, args, return_value); +} + +template +template +void ResultsClass::add_listener(ContextType ctx, U& collection, ObjectType this_object, Arguments args) { + args.validate_maximum(1); + + auto callback = Value::validated_to_function(ctx, args[0]); Protected protected_callback(ctx, callback); Protected protected_this(ctx, this_object); Protected protected_ctx(Context::get_global_context(ctx)); - auto token = results->add_notification_callback([=](CollectionChangeSet change_set, std::exception_ptr exception) { + auto token = collection.add_notification_callback([=](CollectionChangeSet const& change_set, std::exception_ptr exception) { HANDLESCOPE - - ValueType arguments[2]; - arguments[0] = static_cast(protected_this); - arguments[1] = CollectionClass::create_collection_change_set(protected_ctx, change_set); + ValueType arguments[] { + static_cast(protected_this), + CollectionClass::create_collection_change_set(protected_ctx, change_set) + }; Function::callback(protected_ctx, protected_callback, protected_this, 2, arguments); }); - results->m_notification_tokens.emplace_back(protected_callback, std::move(token)); + collection.m_notification_tokens.emplace_back(protected_callback, std::move(token)); } template -void ResultsClass::remove_listener(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 1); - +void ResultsClass::add_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { auto results = get_internal>(this_object); - auto callback = Value::validated_to_function(ctx, arguments[0]); - auto protected_function = Protected(ctx, callback); - - auto iter = results->m_notification_tokens.begin(); - typename Protected::Comparator compare; - while (iter != results->m_notification_tokens.end()) { - if(compare(iter->first, protected_function)) { - iter = results->m_notification_tokens.erase(iter); - } - else { - iter++; - } - } + add_listener(ctx, *results, this_object, args); } template -void ResultsClass::remove_all_listeners(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 0); - +template +void ResultsClass::remove_listener(ContextType ctx, U& collection, ObjectType this_object, Arguments args) { + args.validate_maximum(1); + + auto callback = Value::validated_to_function(ctx, args[0]); + auto protected_function = Protected(ctx, callback); + + auto& tokens = collection.m_notification_tokens; + auto compare = [&](auto&& token) { + return typename Protected::Comparator()(token.first, protected_function); + }; + tokens.erase(std::remove_if(tokens.begin(), tokens.end(), compare), tokens.end()); +} + +template +void ResultsClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + auto results = get_internal>(this_object); + remove_listener(ctx, *results, this_object, args); +} + +template +void ResultsClass::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { + args.validate_maximum(0); + auto results = get_internal>(this_object); results->m_notification_tokens.clear(); } diff --git a/src/js_schema.hpp b/src/js_schema.hpp index 51ef3aa5..e3464de9 100644 --- a/src/js_schema.hpp +++ b/src/js_schema.hpp @@ -23,6 +23,8 @@ #include "js_types.hpp" #include "schema.hpp" +#include "util/format.hpp" + namespace realm { namespace js { @@ -41,7 +43,7 @@ struct Schema { using ConstructorMap = std::map>; static ObjectType dict_for_property_array(ContextType, const ObjectSchema &, ObjectType); - static Property parse_property(ContextType, ValueType, std::string, ObjectDefaults &); + static Property parse_property(ContextType, ValueType, StringData, std::string, ObjectDefaults &); static ObjectSchema parse_object_schema(ContextType, ObjectType, ObjectDefaultsMap &, ConstructorMap &); static realm::Schema parse_schema(ContextType, ObjectType, ObjectDefaultsMap &, ConstructorMap &); @@ -68,8 +70,73 @@ typename T::Object Schema::dict_for_property_array(ContextType ctx, const Obj return dict; } +static inline void parse_property_type(StringData object_name, Property& prop, StringData type) +{ + using realm::PropertyType; + if (!type || !type.size()) { + throw std::logic_error(util::format("Property '%1.%2' must have a non-empty type", object_name, prop.name)); + } + if (type.ends_with("[]")) { + prop.type |= PropertyType::Array; + type = type.substr(0, type.size() - 2); + } + if (type.ends_with("?")) { + prop.type |= PropertyType::Nullable; + type = type.substr(0, type.size() - 1); + } + + if (type == "bool") { + prop.type |= PropertyType::Bool; + } + else if (type == "int") { + prop.type |= PropertyType::Int; + } + else if (type == "float") { + prop.type |= PropertyType::Float; + } + else if (type == "double") { + prop.type |= PropertyType::Double; + } + else if (type == "string") { + prop.type |= PropertyType::String; + } + else if (type == "date") { + prop.type |= PropertyType::Date; + } + else if (type == "data") { + prop.type |= PropertyType::Data; + } + else if (type == "list") { + if (is_nullable(prop.type)) { + throw std::logic_error(util::format("List property '%1.%2' cannot be optional", object_name, prop.name)); + } + if (is_array(prop.type)) { + throw std::logic_error(util::format("List property '%1.%2' must have a non-list value type", object_name, prop.name)); + } + prop.type |= PropertyType::Object | PropertyType::Array; + } + else if (type == "linkingObjects") { + prop.type |= PropertyType::LinkingObjects | PropertyType::Array; + } + else if (type == "object") { + prop.type |= PropertyType::Object; + } + else { + // The type could be the name of another object type in the same schema. + prop.type |= PropertyType::Object; + prop.object_type = type; + } + + // Object properties are implicitly optional + if (prop.type == PropertyType::Object && !is_array(prop.type)) { + prop.type |= PropertyType::Nullable; + } +} + + template -Property Schema::parse_property(ContextType ctx, ValueType attributes, std::string property_name, ObjectDefaults &object_defaults) { +Property Schema::parse_property(ContextType ctx, ValueType attributes, StringData object_name, + std::string property_name, ObjectDefaults &object_defaults) { static const String default_string = "default"; static const String indexed_string = "indexed"; static const String type_string = "type"; @@ -78,79 +145,23 @@ Property Schema::parse_property(ContextType ctx, ValueType attributes, std::s static const String property_string = "property"; Property prop; - prop.name = property_name; + prop.name = std::move(property_name); ObjectType property_object = {}; std::string type; using realm::PropertyType; - PropertyType is_optional = PropertyType::Required; if (Value::is_object(ctx, attributes)) { property_object = Value::validated_to_object(ctx, attributes); - type = Object::validated_get_string(ctx, property_object, type_string); + std::string property_type = Object::validated_get_string(ctx, property_object, type_string); + parse_property_type(object_name, prop, property_type); ValueType optional_value = Object::get_property(ctx, property_object, optional_string); if (!Value::is_undefined(ctx, optional_value) && Value::validated_to_boolean(ctx, optional_value, "optional")) { - is_optional = PropertyType::Nullable; + prop.type |= PropertyType::Nullable; } - } - else { - type = Value::validated_to_string(ctx, attributes); - } - if (type == "bool") { - prop.type = PropertyType::Bool | is_optional; - } - else if (type == "int") { - prop.type = PropertyType::Int | is_optional; - } - else if (type == "float") { - prop.type = PropertyType::Float | is_optional; - } - else if (type == "double") { - prop.type = PropertyType::Double | is_optional; - } - else if (type == "string") { - prop.type = PropertyType::String | is_optional; - } - else if (type == "date") { - prop.type = PropertyType::Date | is_optional; - } - else if (type == "data") { - prop.type = PropertyType::Data | is_optional; - } - else if (type == "list") { - if (!Value::is_valid(property_object)) { - throw std::runtime_error("List property must specify 'objectType'"); - } - prop.type = PropertyType::Object | PropertyType::Array; - prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string); - } - else if (type == "linkingObjects") { - prop.type = PropertyType::LinkingObjects | PropertyType::Array; - - if (!Value::is_valid(property_object)) { - throw std::runtime_error("Object property must specify 'objectType'"); - } - prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string); - prop.link_origin_property_name = Object::validated_get_string(ctx, property_object, property_string); - } - else if (type == "object") { - prop.type = PropertyType::Object | PropertyType::Nullable; - - if (!Value::is_valid(property_object)) { - throw std::runtime_error("Object property must specify 'objectType'"); - } - prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string); - } - else { - // The type could be the name of another object type in the same schema. - prop.type = PropertyType::Object | PropertyType::Nullable; - prop.object_type = type; - } - - if (Value::is_valid(property_object)) { ValueType default_value = Object::get_property(ctx, property_object, default_string); if (!Value::is_undefined(ctx, default_value)) { object_defaults.emplace(prop.name, Protected(ctx, default_value)); @@ -161,6 +172,27 @@ Property Schema::parse_property(ContextType ctx, ValueType attributes, std::s prop.is_indexed = Value::validated_to_boolean(ctx, indexed_value); } } + else { + std::string property_type = Value::validated_to_string(ctx, attributes); + parse_property_type(object_name, prop, property_type); + } + + if (prop.type == PropertyType::Object && prop.object_type.empty()) { + if (!Value::is_valid(property_object)) { + throw std::logic_error(util::format("%1 property %2.%3 must specify 'objectType'", + is_array(prop.type) ? "List" : "Object", object_name, prop.name)); + } + prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string); + } + + if (prop.type == PropertyType::LinkingObjects) { + if (!Value::is_valid(property_object)) { + throw std::logic_error(util::format("Linking objects property %1.%2 must specify 'objectType'", + object_name, prop.name)); + } + prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string); + prop.link_origin_property_name = Object::validated_get_string(ctx, property_object, property_string); + } return prop; } @@ -188,7 +220,7 @@ ObjectSchema Schema::parse_object_schema(ContextType ctx, ObjectType object_s for (uint32_t i = 0; i < length; i++) { ObjectType property_object = Object::validated_get_object(ctx, properties_object, i); std::string property_name = Object::validated_get_string(ctx, property_object, name_string); - Property property = parse_property(ctx, property_object, property_name, object_defaults); + Property property = parse_property(ctx, property_object, object_schema.name, std::move(property_name), object_defaults); if (property.type == realm::PropertyType::LinkingObjects) { object_schema.computed_properties.emplace_back(std::move(property)); } @@ -200,9 +232,9 @@ ObjectSchema Schema::parse_object_schema(ContextType ctx, ObjectType object_s } else { auto property_names = Object::get_property_names(ctx, properties_object); - for (auto &property_name : property_names) { + for (auto& property_name : property_names) { ValueType property_value = Object::get_property(ctx, properties_object, property_name); - Property property = parse_property(ctx, property_value, property_name, object_defaults); + Property property = parse_property(ctx, property_value, object_schema.name, property_name, object_defaults); if (property.type == realm::PropertyType::LinkingObjects) { object_schema.computed_properties.emplace_back(std::move(property)); } @@ -291,13 +323,25 @@ typename T::Object Schema::object_for_property(ContextType ctx, const Propert Object::set_property(ctx, object, name_string, Value::from_string(ctx, property.name)); static const String type_string = "type"; - const std::string type = is_array(property.type) ? "list" : string_for_property_type(property.type); - Object::set_property(ctx, object, type_string, Value::from_string(ctx, type)); + if (is_array(property.type)) { + if (property.type == realm::PropertyType::LinkingObjects) { + Object::set_property(ctx, object, type_string, Value::from_string(ctx, "linkingObjects")); + } + else { + Object::set_property(ctx, object, type_string, Value::from_string(ctx, "list")); + } + } + else { + Object::set_property(ctx, object, type_string, Value::from_string(ctx, string_for_property_type(property.type))); + } static const String object_type_string = "objectType"; if (property.object_type.size()) { Object::set_property(ctx, object, object_type_string, Value::from_string(ctx, property.object_type)); } + else if (is_array(property.type)) { + Object::set_property(ctx, object, object_type_string, Value::from_string(ctx, string_for_property_type(property.type & ~realm::PropertyType::Flags))); + } static const String property_string = "property"; if (property.type == realm::PropertyType::LinkingObjects) { @@ -305,14 +349,10 @@ typename T::Object Schema::object_for_property(ContextType ctx, const Propert } static const String indexed_string = "indexed"; - if (property.is_indexed) { - Object::set_property(ctx, object, indexed_string, Value::from_boolean(ctx, true)); - } + Object::set_property(ctx, object, indexed_string, Value::from_boolean(ctx, property.is_indexed)); static const String optional_string = "optional"; - if (is_nullable(property.type)) { - Object::set_property(ctx, object, optional_string, Value::from_boolean(ctx, true)); - } + Object::set_property(ctx, object, optional_string, Value::from_boolean(ctx, is_nullable(property.type))); return object; } diff --git a/src/js_types.hpp b/src/js_types.hpp index 4f20b6cf..24f13c33 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -65,7 +65,7 @@ struct String { String(const char *); String(const StringType &); String(StringType &&); - String(const std::string &); + String(StringData); operator StringType() const; operator std::string() const; @@ -82,16 +82,21 @@ struct Context { class TypeErrorException : public std::invalid_argument { public: - TypeErrorException(StringData object_type, StringData property, - std::string const& type, std::string const& value) - : std::invalid_argument(util::format("%1.%2 must be of type '%3', got (%4)", - object_type, property, type, value)) + template + TypeErrorException(NativeAccessor& accessor, StringData object_type, + Property const& prop, ValueType value) + : std::invalid_argument(util::format("%1.%2 must be of type '%3', got '%4' (%5)", + object_type, prop.name, type_string(prop), + accessor.typeof(value), + accessor.print(value))) {} TypeErrorException(const char *name, std::string const& type, std::string const& value) : std::invalid_argument(util::format("%1 must be of type '%2', got (%3)", name ? name : "JS value", type, value)) {} + + static std::string type_string(Property const& prop); }; template @@ -101,6 +106,8 @@ struct Value { using ObjectType = typename T::Object; using ValueType = typename T::Value; + static const char *typeof(ContextType, const ValueType &); + static bool is_array(ContextType, const ValueType &); static bool is_array_buffer(ContextType, const ValueType &); static bool is_array_buffer_view(ContextType, const ValueType &); @@ -117,12 +124,17 @@ struct Value { static bool is_valid(const ValueType &); static bool is_valid_for_property(ContextType, const ValueType&, const Property&); + static bool is_valid_for_property_type(ContextType, const ValueType&, realm::PropertyType type, StringData object_type); static ValueType from_boolean(ContextType, bool); static ValueType from_null(ContextType); static ValueType from_number(ContextType, double); - static ValueType from_string(ContextType, const String &); - static ValueType from_binary(ContextType, BinaryData); + static ValueType from_string(ContextType ctx, const char *s) { return s ? from_nonnull_string(ctx, s) : from_null(ctx); } + static ValueType from_string(ContextType ctx, StringData s) { return s ? from_nonnull_string(ctx, s) : from_null(ctx); } + static ValueType from_string(ContextType ctx, const std::string& s) { return from_nonnull_string(ctx, s.c_str()); } + static ValueType from_binary(ContextType ctx, BinaryData b) { return b ? from_nonnull_binary(ctx, b) : from_null(ctx); } + static ValueType from_nonnull_string(ContextType, const String&); + static ValueType from_nonnull_binary(ContextType, BinaryData); static ValueType from_undefined(ContextType); static ObjectType to_array(ContextType, const ValueType &); @@ -135,6 +147,7 @@ struct Value { static String to_string(ContextType, const ValueType &); static OwnedBinaryData to_binary(ContextType, ValueType); + #define VALIDATED(return_t, type) \ static return_t validated_to_##type(ContextType ctx, const ValueType &value, const char *name = nullptr) { \ if (!is_##type(ctx, value)) { \ @@ -351,82 +364,76 @@ REALM_JS_INLINE void set_internal(const typename T::Object &object, typename Cla template inline bool Value::is_valid_for_property(ContextType context, const ValueType &value, const Property& prop) { - if (is_nullable(prop.type) && (is_null(context, value) || is_undefined(context, value))) { - return true; - } + return is_valid_for_property_type(context, value, prop.type, prop.object_type); +} +template +inline bool Value::is_valid_for_property_type(ContextType context, const ValueType &value, realm::PropertyType type, StringData object_type) { using realm::PropertyType; - if (realm::is_array(prop.type)) { - if (prop.type != PropertyType::Object) { - return false; - } - // FIXME: Do we need to validate the types of the contained objects? - if (is_array(context, value)) { + auto check_value = [&](auto&& value) { + if (is_nullable(type) && (is_null(context, value) || is_undefined(context, value))) { return true; } - - if (is_object(context, value)) { - auto object = to_object(context, value); - return Object::template is_instance>(context, object) - || Object::template is_instance>(context, object); + switch (type & ~PropertyType::Flags) { + case PropertyType::Int: + case PropertyType::Float: + case PropertyType::Double: + return is_number(context, value); + case PropertyType::Bool: + return is_boolean(context, value); + case PropertyType::String: + return is_string(context, value); + case PropertyType::Data: + return is_binary(context, value); + case PropertyType::Date: + return is_date(context, value); + case PropertyType::Object: + return true; + case PropertyType::Any: + return false; + default: + REALM_UNREACHABLE(); } + }; + auto check_collection_type = [&](auto&& list) { + auto list_type = list->get_type(); + return list_type == type + && is_nullable(list_type) == is_nullable(type) + && (type != PropertyType::Object || list->get_object_schema().name == object_type); + }; + if (!realm::is_array(type)) { + return check_value(value); + } + + if (is_object(context, value)) { + auto object = to_object(context, value); + if (Object::template is_instance>(context, object)) { + return check_collection_type(get_internal>(object)); + } + if (Object::template is_instance>(context, object)) { + return check_collection_type(get_internal>(object)); + } + } + + if (type == PropertyType::Object) { + // FIXME: Do we need to validate the types of the contained objects? + return is_array(context, value); + } + + if (!is_array(context, value)) { return false; } - switch (prop.type & ~PropertyType::Flags) { - case PropertyType::Int: - case PropertyType::Float: - case PropertyType::Double: - return is_number(context, value); - case PropertyType::Bool: - return is_boolean(context, value); - case PropertyType::String: - return is_string(context, value); - case PropertyType::Data: - return is_binary(context, value); - case PropertyType::Date: - return is_date(context, value); - case PropertyType::Object: - return true; - case PropertyType::Any: + auto array = to_array(context, value); + uint32_t size = Object::validated_get_length(context, array); + for (uint32_t i = 0; i < size; ++i) { + if (!check_value(Object::get_property(context, array, i))) { return false; - default: - REALM_UNREACHABLE(); - } -} - -inline std::string js_type_name_for_property_type(realm::PropertyType type) -{ - using realm::PropertyType; - if (realm::is_array(type)) { - if (type == PropertyType::LinkingObjects) { - throw std::runtime_error("LinkingObjects' type is not supported"); } - return "array"; - } - - switch (type & ~PropertyType::Flags) { - case PropertyType::Int: - case PropertyType::Float: - case PropertyType::Double: - return "number"; - case PropertyType::Bool: - return "boolean"; - case PropertyType::String: - return "string"; - case PropertyType::Date: - return "date"; - case PropertyType::Data: - return "binary"; - case PropertyType::Object: - return "object"; - case PropertyType::Any: - throw std::runtime_error("'Any' type is not supported"); - default: - REALM_UNREACHABLE(); } + return true; } } // js diff --git a/src/jsc/jsc_class.hpp b/src/jsc/jsc_class.hpp index 8accc341..031e26dd 100644 --- a/src/jsc/jsc_class.hpp +++ b/src/jsc/jsc_class.hpp @@ -300,20 +300,43 @@ inline void ObjectWrap::get_property_names(JSContextRef ctx, JSObject } } +static inline bool try_get_int(JSStringRef property, int64_t& value) { + value = 0; + auto str = JSStringGetCharactersPtr(property); + auto end = str + JSStringGetLength(property); + while (str != end && iswspace(*str)) { + ++str; + } + bool negative = false; + if (str != end && *str == '-') { + negative = true; + ++str; + } + while (str != end && *str >= '0' && *str <= '9') { + if (int_multiply_with_overflow_detect(value, 10)) { + return false; + } + value += *str - '0'; + ++str; + } + if (negative) { + value *= -1; + } + return str == end; +} + template inline JSValueRef ObjectWrap::get_property(JSContextRef ctx, JSObjectRef object, JSStringRef property, JSValueRef* exception) { if (auto index_getter = s_class.index_accessor.getter) { - try { - uint32_t index = validated_positive_index(jsc::String(property)); + int64_t num; + if (try_get_int(property, num)) { + uint32_t index; + if (num < 0 || util::int_cast_with_overflow_detect(num, index)) { + // Out-of-bounds index getters should just return undefined in JS. + return Value::from_undefined(ctx); + } return index_getter(ctx, object, index, exception); } - catch (std::out_of_range &) { - // Out-of-bounds index getters should just return undefined in JS. - return Value::from_undefined(ctx); - } - catch (std::invalid_argument &) { - // Property is not a number. - } } if (auto string_getter = s_class.string_accessor.getter) { return string_getter(ctx, object, property, exception); @@ -326,24 +349,24 @@ inline bool ObjectWrap::set_property(JSContextRef ctx, JSObjectRef ob auto index_setter = s_class.index_accessor.setter; if (index_setter || s_class.index_accessor.getter) { - try { - uint32_t index = validated_positive_index(jsc::String(property)); - + int64_t num; + if (try_get_int(property, num)) { + if (num < 0) { + *exception = Exception::value(ctx, util::format("Index %1 cannot be less than zero.", num)); + return false; + } + int32_t index; + if (util::int_cast_with_overflow_detect(num, index)) { + *exception = Exception::value(ctx, util::format("Index %1 cannot be greater than %2.", + num, std::numeric_limits::max())); + return false; + } if (index_setter) { return index_setter(ctx, object, index, value, exception); } - else { - *exception = Exception::value(ctx, std::string("Cannot assign to read only index ") + util::to_string(index)); - return false; - } - } - catch (std::out_of_range &e) { - *exception = Exception::value(ctx, e); + *exception = Exception::value(ctx, util::format("Cannot assign to read only index %1", index)); return false; } - catch (std::invalid_argument &) { - // Property is not a number. - } } if (auto string_setter = s_class.string_accessor.setter) { return string_setter(ctx, object, property, value, exception); @@ -372,10 +395,10 @@ 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) { +JSValueRef wrap(JSContextRef ctx, JSObjectRef, 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); + F(ctx, this_object, jsc::Arguments{ctx, argc, arguments}, return_value); return return_value; } catch (std::exception &e) { diff --git a/src/jsc/jsc_return_value.hpp b/src/jsc/jsc_return_value.hpp index 68b2411e..ec69cefc 100644 --- a/src/jsc/jsc_return_value.hpp +++ b/src/jsc/jsc_return_value.hpp @@ -38,6 +38,9 @@ class ReturnValue { void set(const std::string &string) { m_value = JSValueMakeString(m_context, jsc::String(string)); } + void set(const char *string) { + m_value = JSValueMakeString(m_context, jsc::String(string)); + } void set(bool boolean) { m_value = JSValueMakeBoolean(m_context, boolean); } diff --git a/src/jsc/jsc_string.hpp b/src/jsc/jsc_string.hpp index 521b5c70..25a15298 100644 --- a/src/jsc/jsc_string.hpp +++ b/src/jsc/jsc_string.hpp @@ -32,7 +32,8 @@ class String { public: String(const char *s) : m_str(JSStringCreateWithUTF8CString(s)) {} String(const JSStringRef &s) : m_str(JSStringRetain(s)) {} - String(const std::string &str) : String(str.c_str()) {} + String(StringData str) : String(str.data()) {} + String(const std::string& str) : String(str.c_str()) {} String(const StringType &o) : String(o.m_str) {} String(StringType &&o) : m_str(o.m_str) { o.m_str = nullptr; diff --git a/src/jsc/jsc_value.cpp b/src/jsc/jsc_value.cpp index e2288206..142a5bd4 100644 --- a/src/jsc/jsc_value.cpp +++ b/src/jsc/jsc_value.cpp @@ -47,7 +47,7 @@ bool jsc::Value::is_binary(JSContextRef ctx, const JSValueRef &value) } template<> -JSValueRef jsc::Value::from_binary(JSContextRef ctx, BinaryData data) +JSValueRef jsc::Value::from_nonnull_binary(JSContextRef ctx, BinaryData data) { static jsc::String s_buffer = "buffer"; static jsc::String s_uint8_array = "Uint8Array"; diff --git a/src/jsc/jsc_value.hpp b/src/jsc/jsc_value.hpp index e1cfb420..4d4cab89 100644 --- a/src/jsc/jsc_value.hpp +++ b/src/jsc/jsc_value.hpp @@ -41,6 +41,18 @@ static inline bool is_object_of_type(JSContextRef ctx, JSValueRef value, jsc::St return result; } +template<> +inline const char *jsc::Value::typeof(JSContextRef ctx, const JSValueRef &value) { + switch (JSValueGetType(ctx, value)) { + case kJSTypeNull: return "null"; + case kJSTypeNumber: return "number"; + case kJSTypeObject: return "object"; + case kJSTypeString: return "string"; + case kJSTypeBoolean: return "boolean"; + case kJSTypeUndefined: return "undefined"; + } +} + template<> inline bool jsc::Value::is_array(JSContextRef ctx, const JSValueRef &value) { // JSValueIsArray() is not available until iOS 9. @@ -124,7 +136,7 @@ inline JSValueRef jsc::Value::from_number(JSContextRef ctx, double number) { } template<> -inline JSValueRef jsc::Value::from_string(JSContextRef ctx, const jsc::String &string) { +inline JSValueRef jsc::Value::from_nonnull_string(JSContextRef ctx, const jsc::String &string) { return JSValueMakeString(ctx, string); } @@ -134,26 +146,12 @@ inline JSValueRef jsc::Value::from_undefined(JSContextRef ctx) { } template<> -JSValueRef jsc::Value::from_binary(JSContextRef ctx, BinaryData data); +JSValueRef jsc::Value::from_nonnull_binary(JSContextRef ctx, BinaryData data); template<> inline bool jsc::Value::to_boolean(JSContextRef ctx, const JSValueRef &value) { return JSValueToBoolean(ctx, value); } - -template<> -inline double jsc::Value::to_number(JSContextRef ctx, const JSValueRef &value) { - JSValueRef exception = nullptr; - double number = JSValueToNumber(ctx, value, &exception); - if (exception) { - throw jsc::Exception(ctx, exception); - } - if (isnan(number)) { - throw std::invalid_argument("Value not convertible to a number."); - } - return number; -} - template<> inline jsc::String jsc::Value::to_string(JSContextRef ctx, const JSValueRef &value) { JSValueRef exception = nullptr; @@ -168,6 +166,21 @@ inline jsc::String jsc::Value::to_string(JSContextRef ctx, const JSValueRef &val return string; } +template<> +inline double jsc::Value::to_number(JSContextRef ctx, const JSValueRef &value) { + JSValueRef exception = nullptr; + double number = JSValueToNumber(ctx, value, &exception); + if (exception) { + throw jsc::Exception(ctx, exception); + } + if (isnan(number)) { + throw std::invalid_argument(util::format("Value '%1' not convertible to a number.", + (std::string)to_string(ctx, value))); + } + return number; +} + + template<> inline JSObjectRef jsc::Value::to_object(JSContextRef ctx, const JSValueRef &value) { JSValueRef exception = nullptr; diff --git a/src/node/node_class.hpp b/src/node/node_class.hpp index 2b1c7557..e4693b19 100644 --- a/src/node/node_class.hpp +++ b/src/node/node_class.hpp @@ -317,7 +317,7 @@ void wrap(const v8::FunctionCallbackInfo& info) { auto arguments = node::get_arguments(info); try { - F(isolate, info.Callee(), info.This(), node::Arguments{isolate, arguments.size(), arguments.data()}, return_value); + F(isolate, info.This(), node::Arguments{isolate, arguments.size(), arguments.data()}, return_value); } catch (std::exception &e) { Nan::ThrowError(node::Exception::value(isolate, e)); diff --git a/src/node/node_return_value.hpp b/src/node/node_return_value.hpp index a6986f1e..e47ba700 100644 --- a/src/node/node_return_value.hpp +++ b/src/node/node_return_value.hpp @@ -41,6 +41,17 @@ class ReturnValue { m_value.Set(Nan::New(string).ToLocalChecked()); } } + void set(const char *str) { + if (!str) { + m_value.SetNull(); + } + else if (!*str) { + m_value.SetEmptyString(); + } + else { + m_value.Set(Nan::New(str).ToLocalChecked()); + } + } void set(bool boolean) { m_value.Set(boolean); } diff --git a/src/node/node_value.hpp b/src/node/node_value.hpp index 81150e14..20dc7d8e 100644 --- a/src/node/node_value.hpp +++ b/src/node/node_value.hpp @@ -23,6 +23,17 @@ namespace realm { namespace js { +template<> +inline const char *node::Value::typeof(v8::Isolate* isolate, const v8::Local &value) { + if (value->IsNull()) { return "null"; } + if (value->IsNumber()) { return "number"; } + if (value->IsString()) { return "string"; } + if (value->IsBoolean()) { return "boolean"; } + if (value->IsUndefined()) { return "undefined"; } + if (value->IsObject()) { return "object"; } + return "unknown"; +} + template<> inline bool node::Value::is_array(v8::Isolate* isolate, const v8::Local &value) { return value->IsArray(); @@ -110,12 +121,12 @@ inline v8::Local node::Value::from_number(v8::Isolate* isolate, doubl } template<> -inline v8::Local node::Value::from_string(v8::Isolate* isolate, const node::String &string) { +inline v8::Local node::Value::from_nonnull_string(v8::Isolate* isolate, const node::String &string) { return v8::Local(string); } template<> -inline v8::Local node::Value::from_binary(v8::Isolate* isolate, BinaryData data) { +inline v8::Local node::Value::from_nonnull_binary(v8::Isolate* isolate, BinaryData data) { v8::Local buffer = v8::ArrayBuffer::New(isolate, data.size()); v8::ArrayBuffer::Contents contents = buffer->GetContents(); @@ -137,17 +148,18 @@ inline bool node::Value::to_boolean(v8::Isolate* isolate, const v8::Local -inline double node::Value::to_number(v8::Isolate* isolate, const v8::Local &value) { - double number = Nan::To(value).FromMaybe(NAN); - if (std::isnan(number)) { - throw std::invalid_argument("Value not convertible to a number."); - } - return number; +inline node::String node::Value::to_string(v8::Isolate* isolate, const v8::Local &value) { + return value->ToString(); } template<> -inline node::String node::Value::to_string(v8::Isolate* isolate, const v8::Local &value) { - return value->ToString(); +inline double node::Value::to_number(v8::Isolate* isolate, const v8::Local &value) { + double number = Nan::To(value).FromMaybe(NAN); + if (std::isnan(number)) { + throw std::invalid_argument(util::format("Value '%1' not convertible to a number.", + (std::string)to_string(isolate, value))); + } + return number; } template<> diff --git a/src/object-store b/src/object-store index 30b8a785..49705c0f 160000 --- a/src/object-store +++ b/src/object-store @@ -1 +1 @@ -Subproject commit 30b8a7853b17762719aa7671aac6ebea04473e33 +Subproject commit 49705c0fbdbff8536cfa134fc17b837db4385d31 diff --git a/src/rpc.cpp b/src/rpc.cpp index 896fad8a..cb160526 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -34,6 +34,7 @@ using namespace realm::rpc; using Accessor = realm::js::NativeAccessor; +namespace { static const char * const RealmObjectTypesData = "data"; static const char * const RealmObjectTypesDate = "date"; static const char * const RealmObjectTypesDictionary = "dict"; @@ -46,10 +47,40 @@ static const char * const RealmObjectTypesUser = "user"; static const char * const RealmObjectTypesSession = "session"; static const char * const RealmObjectTypesUndefined = "undefined"; -static RPCServer*& get_rpc_server(JSGlobalContextRef ctx) { +json serialize_object_schema(const realm::ObjectSchema &object_schema) { + std::vector properties; + + for (auto &prop : object_schema.persisted_properties) { + properties.push_back(prop.name); + } + + for (auto &prop : object_schema.computed_properties) { + properties.push_back(prop.name); + } + + return { + {"name", object_schema.name}, + {"properties", properties}, + }; +} + +template +json get_type(Container const& c) { + auto type = c.get_type(); + if (type == realm::PropertyType::Object) { + return serialize_object_schema(c.get_object_schema()); + } + return { + {"type", string_for_property_type(type)}, + {"optional", is_nullable(type)} + }; +} + +RPCServer*& get_rpc_server(JSGlobalContextRef ctx) { static std::map s_map; return s_map[ctx]; } +} RPCWorker::RPCWorker() { m_thread = std::thread([this]() { @@ -144,10 +175,31 @@ RPCServer::RPCServer() { if (!realm_constructor) { throw std::runtime_error("Realm constructor not found!"); } - + JSObjectRef sync_constructor = (JSObjectRef)jsc::Object::get_property(m_context, realm_constructor, "Sync"); JSObjectRef user_constructor = (JSObjectRef)jsc::Object::get_property(m_context, sync_constructor, "User"); JSObjectRef create_user_method = (JSObjectRef)jsc::Object::get_property(m_context, user_constructor, "createUser"); + + json::array_t args = dict["arguments"]; + size_t arg_count = args.size(); + JSValueRef arg_values[arg_count]; + + for (size_t i = 0; i < arg_count; i++) { + arg_values[i] = deserialize_json_value(args[i]); + } + + JSObjectRef user_object = (JSObjectRef)jsc::Function::call(m_context, create_user_method, arg_count, arg_values); + return (json){{"result", serialize_json_value(user_object)}}; + }; + m_requests["/_adminUser"] = [this](const json dict) { + JSObjectRef realm_constructor = m_session_id ? JSObjectRef(m_objects[m_session_id]) : NULL; + if (!realm_constructor) { + throw std::runtime_error("Realm constructor not found!"); + } + + JSObjectRef sync_constructor = (JSObjectRef)jsc::Object::get_property(m_context, realm_constructor, "Sync"); + JSObjectRef user_constructor = (JSObjectRef)jsc::Object::get_property(m_context, sync_constructor, "User"); + JSObjectRef create_user_method = (JSObjectRef)jsc::Object::get_property(m_context, user_constructor, "_adminUser"); json::array_t args = dict["arguments"]; size_t arg_count = args.size(); @@ -213,11 +265,11 @@ RPCServer::RPCServer() { if (!realm_constructor) { throw std::runtime_error("Realm constructor not found!"); } - + JSObjectRef sync_constructor = (JSObjectRef)jsc::Object::get_property(m_context, realm_constructor, "Sync"); JSObjectRef user_constructor = (JSObjectRef)jsc::Object::get_property(m_context, sync_constructor, "User"); JSValueRef value = jsc::Object::get_property(m_context, user_constructor, "all"); - + return (json){{"result", serialize_json_value(value)}}; }; m_requests["/clear_test_state"] = [this](const json dict) { @@ -360,7 +412,7 @@ json RPCServer::serialize_json_value(JSValueRef js_value) { {"type", RealmObjectTypesList}, {"id", store_object(js_object)}, {"size", list->size()}, - {"schema", serialize_object_schema(list->get_object_schema())} + {"schema", get_type(*list)}, }; } else if (jsc::Object::is_instance>(m_context, js_object)) { @@ -369,7 +421,7 @@ json RPCServer::serialize_json_value(JSValueRef js_value) { {"type", RealmObjectTypesResults}, {"id", store_object(js_object)}, {"size", results->size()}, - {"schema", serialize_object_schema(results->get_object_schema())} + {"schema", get_type(*results)}, }; } else if (jsc::Object::is_instance>(m_context, js_object)) { @@ -455,23 +507,6 @@ json RPCServer::serialize_json_value(JSValueRef js_value) { assert(0); } -json RPCServer::serialize_object_schema(const realm::ObjectSchema &object_schema) { - std::vector properties; - - for (auto &prop : object_schema.persisted_properties) { - properties.push_back(prop.name); - } - - for (auto &prop : object_schema.computed_properties) { - properties.push_back(prop.name); - } - - return { - {"name", object_schema.name}, - {"properties", properties}, - }; -} - JSValueRef RPCServer::deserialize_json_value(const json dict) { json oid = dict["id"]; if (oid.is_number()) { diff --git a/src/rpc.hpp b/src/rpc.hpp index ca253e0a..ed286d05 100644 --- a/src/rpc.hpp +++ b/src/rpc.hpp @@ -81,8 +81,6 @@ class RPCServer { json serialize_json_value(JSValueRef value); JSValueRef deserialize_json_value(const json dict); - - json serialize_object_schema(const ObjectSchema &objectSchema); }; } // rpc diff --git a/tests/ios/RealmJSCoreTests.m b/tests/ios/RealmJSCoreTests.m index 5fd0cf91..14b46956 100644 --- a/tests/ios/RealmJSCoreTests.m +++ b/tests/ios/RealmJSCoreTests.m @@ -48,6 +48,8 @@ context[@"Promise"] = promiseModule[@"Promise"]; } + context[@"global"] = [JSValue valueWithNewObjectInContext:context]; + // Create Realm constructor in the JS context. RJSInitializeInContext(context.JSGlobalContextRef); diff --git a/tests/js/asserts.js b/tests/js/asserts.js index 95f3378c..1be734bc 100644 --- a/tests/js/asserts.js +++ b/tests/js/asserts.js @@ -19,75 +19,122 @@ 'use strict'; module.exports = { - assertEqual: function(val1, val2, errorMessage) { + assertSimilar: function(type, val1, val2, errorMessage, depth) { + depth = depth || 0; + this.assertDefined(type, depth + 1); + type = type.replace('?', ''); + if (val2 === null) { + this.assertNull(val1, errorMessage, depth + 1); + } + else if (type === 'float' || type === 'double') { + this.assertEqualWithTolerance(val1, val2, 0.000001, errorMessage, depth + 1); + } + else if (type === 'data') { + this.assertArraysEqual(new Uint8Array(val1), val2, errorMessage, depth + 1); + } + else if (type === 'date') { + this.assertEqual(val1 && val1.getTime(), val2.getTime(), errorMessage, depth + 1); + } + else if (type === 'object') { + for (const key of Object.keys(val1)) { + this.assertEqual(val1[key], val2[key], errorMessage, depth + 1); + } + } + else if (type === 'list') { + this.assertArraysEqual(val1, val2, errorMessage, depth + 1); + } + else { + this.assertEqual(val1, val2, errorMessage, depth + 1); + } + }, + + assertEqual: function(val1, val2, errorMessage, depth) { if (val1 !== val2) { - var message = "'" + val1 + "' does not equal expected value '" + val2 + "'"; + let message = `'${val1}' does not equal expected value '${val2}'`; if (errorMessage) { - message = errorMessage + ' - ' + message; + message = `${errorMessage} - ${message}`; } - throw new TestFailureError(message); + throw new TestFailureError(message, depth); } }, - assertNotEqual: function(val1, val2, errorMessage) { + assertNotEqual: function(val1, val2, errorMessage, depth) { if (val1 === val2) { - var message = "'" + val1 + "' equals '" + val2 + "'"; + let message = `'${val1}' equals '${val2}'`; if (errorMessage) { - message = errorMessage + ' - ' + message; + message = `${errorMessage} - ${message}`; } - throw new TestFailureError(message); + throw new TestFailureError(message, depth); } }, - assertEqualWithTolerance: function(val1, val2, tolerance, errorMessage) { + assertEqualWithTolerance: function(val1, val2, tolerance, errorMessage, depth) { if (val1 < val2 - tolerance || val1 > val2 + tolerance) { - var message = "'" + val1 + "' does not equal '" + val2 + "' with tolerance '" + tolerance + "'"; + let message = `'${val1}' does not equal '${val2}' with tolerance '${tolerance}'`; if (errorMessage) { - message = errorMessage + ' - ' + message; + message = `${errorMessage} - ${message}`; } - throw new TestFailureError(message); + throw new TestFailureError(message, depth); } }, - assertArray: function(value, length, errorMessage) { + assertArray: function(value, length, errorMessage, depth) { if (!Array.isArray(value)) { - throw new TestFailureError(errorMessage || `Value ${value} is not an array`); + throw new TestFailureError(errorMessage || `Value ${value} is not an array`, depth); } }, - assertArrayLength: function(value, length, errorMessage) { - this.assertArray(value); + assertArrayLength: function(value, length, errorMessage, depth) { + this.assertArray(value, 1 + depth || 0); if (value.length !== length) { - throw new TestFailureError(errorMessage || `Value ${value} is not an array of length ${length}`); + throw new TestFailureError(errorMessage || `Value ${value} is not an array of length ${length}`, depth); } }, - assertArraysEqual: function(val1, val2, errorMessage) { - var len1 = val1.length; - var len2 = val2.length; - var message; + assertArraysEqual: function(val1, val2, errorMessage, depth) { + this.assertDefined(val1, `val1 should be non-null but is ${val1}`, 1 + (depth || 0)); + this.assertDefined(val2, `val2 should be non-null but is ${val2}`, 1 + (depth || 0)); + const len1 = val1.length; + const len2 = val2.length; if (len1 !== len2) { - message = 'Arrays have different lengths (' + len1 + ' != ' + len2 + ')'; + let message = `Arrays (${val1}) and (${val2}) have different lengths (${len1} != ${len2})`; if (errorMessage) { - message = errorMessage + ' - ' + message; + message = `${errorMessage} - ${message}`; } - throw new TestFailureError(message); + throw new TestFailureError(message, depth); } - for (var i = 0; i < len1; i++) { - if (val1[i] !== val2[i]) { - message = 'Array contents not equal at index ' + i + ' (' + val1[i] + ' != ' + val2[i] + ')'; + let compare; + if (val1.type === "data") { + compare = (i, a, b) => a === b || this.assertArraysEqual(new Uint8Array(a), b, `Data elements at index ${i}`, 1) || true; + } + else if (val1.type === "date") { + compare = (i, a, b) => (a && a.getTime()) === (b && b.getTime()); + } + else if (val1.type === "float" || val1.type === "double") { + compare = (i, a, b) => a >= b - 0.000001 && a <= b + 0.000001; + } + else if (val1.type === 'object') { + compare = (i, a, b) => Object.keys(a).every(key => a[key] === b[key]); + } + else { + compare = (i, a, b) => a === b; + } + + for (let i = 0; i < len1; i++) { + if (!compare(i, val1[i], val2[i])) { + let message = `Array contents not equal at index ${i} (${val1[i]} != ${val2[i]})`; if (errorMessage) { - message = errorMessage + ' - ' + message; + message = `${errorMessage} - ${message}`; } - throw new TestFailureError(message); + throw new TestFailureError(message, depth); } } }, - assertThrows: function(func, errorMessage) { - var caught = false; + assertThrows: function(func, errorMessage, depth) { + let caught = false; try { func(); } @@ -96,22 +143,22 @@ module.exports = { } if (!caught) { - throw new TestFailureError(errorMessage || 'Expected exception not thrown'); + throw new TestFailureError(errorMessage || 'Expected exception not thrown', depth); } }, assertThrowsException: function(func, expectedException) { - var caught = false; + let caught = false; try { func(); } catch (e) { caught = true; if (e.name !== expectedException.name) { - throw new TestFailureError('Expected a ' + expectedException.name + ' exception but caught a ' + e.name + ' instead. Message was: ' + e.message); + throw new TestFailureError(`Expected a ${expectedException.name} exception but caught a ${e.name} instead. Message was: ${e.message}`); } if (e.message != expectedException.message) { - throw new TestFailureError('Expected exception "' + expectedException + '" not thrown - instead caught: "' + e + '"'); + throw new TestFailureError(`Expected exception "${expectedException}" not thrown - instead caught: "${e}"`); } } @@ -120,48 +167,60 @@ module.exports = { } }, - assertThrowsContaining: function(func, expectedMessage) { - var caught = false; + assertThrowsContaining: function(func, expectedMessage, depth) { + let caught = false; try { func(); } catch (e) { caught = true; if (!e.message.includes(expectedMessage)) { - throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown - instead caught: "${e}"`); + throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown - instead caught: "${e}"`, depth); } } if (!caught) { - throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown`); + throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown`, depth); } }, - assertTrue: function(condition, errorMessage) { + assertTrue: function(condition, errorMessage, depth) { if (!condition) { - throw new TestFailureError(errorMessage || `Condition ${condition} expected to be true`); + throw new TestFailureError(errorMessage || `Condition ${condition} expected to be true`, depth); } }, - assertInstanceOf: function(object, type, errorMessage) { + assertFalse: function(condition, errorMessage, depth) { + if (condition) { + throw new TestFailureError(errorMessage || `Condition ${condition} expected to be false`, depth); + } + }, + + assertInstanceOf: function(object, type, errorMessage, depth) { if (!(object instanceof type)) { - throw new TestFailureError(errorMessage || `Object ${object} expected to be of type ${type}`); + throw new TestFailureError(errorMessage || `Object ${object} expected to be of type ${type}`, depth); } }, - assertType: function(value, type) { - this.assertEqual(typeof value, type, `Value ${value} expected to be of type ${type}`); + assertType: function(value, type, depth) { + this.assertEqual(typeof value, type, `Value ${value} expected to be of type ${type}`, 1 + depth || 0); }, - assertUndefined: function(value, errorMessage) { + assertDefined: function(value, errorMessage, depth) { + if (value === undefined || value === null) { + throw new TestFailureError(errorMessage || `Value ${value} expected to be non-null`, depth); + } + }, + + assertUndefined: function(value, errorMessage, depth) { if (value !== undefined) { - throw new TestFailureError(errorMessage || `Value ${value} expected to be undefined`); + throw new TestFailureError(errorMessage || `Value ${value} expected to be undefined`, depth); } }, - assertNull: function(value, errorMessage) { + assertNull: function(value, errorMessage, depth) { if (value !== null) { - throw new TestFailureError(errorMessage || `Value ${value} expected to be null`); + throw new TestFailureError(errorMessage || `Value ${value} expected to be null`, depth); } }, @@ -176,26 +235,28 @@ module.exports = { }, }; -function TestFailureError(message) { - var error; +function TestFailureError(message, depth) { + let error; try { throw new Error(message); } catch (e) { error = e; } + depth = 2 + (depth || 0); + // This regular expression will match stack trace lines provided by JavaScriptCore. // Example: someMethod@file:///path/to/file.js:10:24 - var regex = /^(?:.*?@)?([^\[\(].+?):(\d+)(?::(\d+))?\s*$/; + const regex = /^(?:.*?@)?([^\[\(].+?):(\d+)(?::(\d+))?\s*$/; // Remove the top two stack frames and use information from the third, if possible. - var stack = error.stack && error.stack.split('\n'); - var match = stack[2] && stack[2].match(regex); + const stack = error.stack && error.stack.split('\n'); + const match = stack[depth] && stack[depth].match(regex); if (match) { this.sourceURL = match[1]; this.line = +match[2]; this.column = +match[3]; - this.stack = stack.slice(2).join('\n'); + this.stack = stack.slice(depth).join('\n'); } this.__proto__ = error; diff --git a/tests/js/list-tests.js b/tests/js/list-tests.js index aae0b60c..e7a84420 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -18,34 +18,78 @@ 'use strict'; -var Realm = require('realm'); -var TestCase = require('./asserts'); -var schemas = require('./schemas'); +const Realm = require('realm'); +let TestCase = require('./asserts'); +let schemas = require('./schemas'); + +const DATA1 = new Uint8Array([0x01]); +const DATA2 = new Uint8Array([0x02]); +const DATA3 = new Uint8Array([0x03]); +const DATE1 = new Date(1); +const DATE2 = new Date(2); +const DATE3 = new Date(3); module.exports = { testListConstructor: function() { - var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); + const realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); - realm.write(function() { - var obj = realm.create('PersonList', {list: []}); - TestCase.assertTrue(obj.list instanceof Realm.List); - TestCase.assertTrue(obj.list instanceof Realm.Collection); + realm.write(() => { + let obj = realm.create('PersonList', {list: []}); + TestCase.assertInstanceOf(obj.list, Realm.List); + TestCase.assertInstanceOf(obj.list, Realm.Collection); }); - TestCase.assertThrows(function() { - new Realm.List(); + TestCase.assertThrowsContaining(() => new Realm.List(), 'constructor'); + + TestCase.assertType(Realm.List, 'function'); + TestCase.assertInstanceOf(Realm.List, Function); + }, + + testListType: function() { + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject, schemas.PrimitiveArrays]}); + + let obj, prim; + realm.write(() => { + obj = realm.create('LinkTypesObject', {}); + prim = realm.create('PrimitiveArrays', {}); }); - TestCase.assertEqual(typeof Realm.List, 'function'); - TestCase.assertTrue(Realm.List instanceof Function); + TestCase.assertEqual(obj.arrayCol.type, 'object'); + TestCase.assertEqual(obj.arrayCol1.type, 'object'); + + TestCase.assertEqual(prim.bool.type, 'bool'); + TestCase.assertEqual(prim.int.type, 'int'); + TestCase.assertEqual(prim.float.type, 'float'); + TestCase.assertEqual(prim.double.type, 'double'); + TestCase.assertEqual(prim.string.type, 'string'); + TestCase.assertEqual(prim.date.type, 'date'); + TestCase.assertEqual(prim.optBool.type, 'bool'); + TestCase.assertEqual(prim.optInt.type, 'int'); + TestCase.assertEqual(prim.optFloat.type, 'float'); + TestCase.assertEqual(prim.optDouble.type, 'double'); + TestCase.assertEqual(prim.optString.type, 'string'); + TestCase.assertEqual(prim.optDate.type, 'date'); + + TestCase.assertFalse(prim.bool.optional); + TestCase.assertFalse(prim.int.optional); + TestCase.assertFalse(prim.float.optional); + TestCase.assertFalse(prim.double.optional); + TestCase.assertFalse(prim.string.optional); + TestCase.assertFalse(prim.date.optional); + TestCase.assertTrue(prim.optBool.optional); + TestCase.assertTrue(prim.optInt.optional); + TestCase.assertTrue(prim.optFloat.optional); + TestCase.assertTrue(prim.optDouble.optional); + TestCase.assertTrue(prim.optString.optional); + TestCase.assertTrue(prim.optDate.optional); }, testListLength: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}], @@ -60,94 +104,286 @@ module.exports = { obj.arrayCol = [{doubleCol: 1}, {doubleCol: 2}]; TestCase.assertEqual(array.length, 2); - TestCase.assertThrows(function() { - array.length = 0; - }, 'cannot set length property on lists'); + TestCase.assertThrowsContaining(() => array.length = 0, + "Cannot assign to read only property 'length'"); }); TestCase.assertEqual(array.length, 2); }, testListSubscriptGetters: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject, schemas.PrimitiveArrays]}); + let obj, prim; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}, {doubleCol: 4}], + arrayCol1: [{doubleCol: 5}, {doubleCol: 6}], }); + prim = realm.create('PrimitiveArrays', { + bool: [true, false], + int: [1, 2], + float: [1.1, 2.2], + double: [1.11, 2.22], + string: ['a', 'b'], + date: [new Date(1), new Date(2)], + data: [DATA1, DATA2], - array = obj.arrayCol; + optBool: [true, null], + optInt: [1, null], + optFloat: [1.1, null], + optDouble: [1.11, null], + optString: ['a', null], + optDate: [new Date(1), null], + optData: [DATA1, null], + }); }); - TestCase.assertEqual(array[0].doubleCol, 3); - TestCase.assertEqual(array[1].doubleCol, 4); - TestCase.assertEqual(array[2], undefined); - TestCase.assertEqual(array[-1], undefined); + TestCase.assertEqual(obj.arrayCol[0].doubleCol, 3); + TestCase.assertEqual(obj.arrayCol[1].doubleCol, 4); + TestCase.assertEqual(obj.arrayCol[2], undefined); + TestCase.assertEqual(obj.arrayCol[-1], undefined); + TestCase.assertEqual(obj.arrayCol['foo'], undefined); + + TestCase.assertEqual(obj.arrayCol1[0].doubleCol, 5); + TestCase.assertEqual(obj.arrayCol1[1].doubleCol, 6); + TestCase.assertEqual(obj.arrayCol1[2], undefined); + TestCase.assertEqual(obj.arrayCol1[-1], undefined); + TestCase.assertEqual(obj.arrayCol1['foo'], undefined); + + for (let field of Object.keys(prim)) { + TestCase.assertEqual(prim[field][2], undefined); + TestCase.assertEqual(prim[field][-1], undefined); + TestCase.assertEqual(prim[field]['foo'], undefined); + if (field.includes('opt')) { + TestCase.assertEqual(prim[field][1], null); + } + } + + TestCase.assertSimilar('bool', prim.bool[0], true); + TestCase.assertSimilar('bool', prim.bool[1], false); + TestCase.assertSimilar('int', prim.int[0], 1); + TestCase.assertSimilar('int', prim.int[1], 2); + TestCase.assertSimilar('float', prim.float[0], 1.1); + TestCase.assertSimilar('float', prim.float[1], 2.2); + TestCase.assertSimilar('double', prim.double[0], 1.11); + TestCase.assertSimilar('double', prim.double[1], 2.22); + TestCase.assertSimilar('string', prim.string[0], 'a'); + TestCase.assertSimilar('string', prim.string[1], 'b'); + TestCase.assertSimilar('data', prim.data[0], DATA1); + TestCase.assertSimilar('data', prim.data[1], DATA2); + TestCase.assertSimilar('date', prim.date[0], new Date(1)); + TestCase.assertSimilar('date', prim.date[1], new Date(2)); + + TestCase.assertSimilar('bool', prim.optBool[0], true); + TestCase.assertSimilar('int', prim.optInt[0], 1); + TestCase.assertSimilar('float', prim.optFloat[0], 1.1); + TestCase.assertSimilar('double', prim.optDouble[0], 1.11); + TestCase.assertSimilar('string', prim.optString[0], 'a'); + TestCase.assertSimilar('data', prim.optData[0], DATA1); + TestCase.assertSimilar('date', prim.optDate[0], new Date(1)); }, testListSubscriptSetters: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject, + schemas.PrimitiveArrays]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}, {doubleCol: 4}], }); - + let prim = realm.create('PrimitiveArrays', {}); array = obj.arrayCol; + array[0] = {doubleCol: 5}; array[1] = {doubleCol: 6}; - TestCase.assertEqual(array[0].doubleCol, 5); TestCase.assertEqual(array[1].doubleCol, 6); array[0] = obj.objectCol; array[1] = obj.objectCol1; - TestCase.assertEqual(array[0].doubleCol, 1); TestCase.assertEqual(array[1].doubleCol, 2); - TestCase.assertThrows(function() { - array[2] = {doubleCol: 1}; - }, 'cannot set list item beyond its bounds'); + TestCase.assertThrowsContaining(() => array[0] = null, + "JS value must be of type 'object', got (null)"); + TestCase.assertThrowsContaining(() => array[0] = {}, + "Missing value for property 'TestObject.doubleCol'"); + TestCase.assertThrowsContaining(() => array[0] = {foo: 'bar'}, + "Missing value for property 'TestObject.doubleCol'"); + TestCase.assertThrowsContaining(() => array[0] = prim, + "Object of type (PrimitiveArrays) does not match List type (TestObject)"); + TestCase.assertThrowsContaining(() => array[0] = array, + "Missing value for property 'TestObject.doubleCol'"); + TestCase.assertThrowsContaining(() => array[2] = {doubleCol: 1}, + "Requested index 2 greater than max 1"); + TestCase.assertThrowsContaining(() => array[-1] = {doubleCol: 1}, + "Index -1 cannot be less than zero."); - TestCase.assertThrows(function() { - array[-1] = {doubleCol: 1}; - }, 'cannot set list item with negative index'); + array['foo'] = 'bar'; + TestCase.assertEqual(array.foo, 'bar'); + + function testAssign(name, v1, v2) { + prim[name].push(v1); + TestCase.assertSimilar(prim[name].type, prim[name][0], v1, undefined, 1); + prim[name][0] = v2; + TestCase.assertSimilar(prim[name].type, prim[name][0], v2, undefined, 1); + } + + testAssign('bool', true, false); + testAssign('int', 1, 2); + testAssign('float', 1.1, 2.2); + testAssign('double', 1.1, 2.2); + testAssign('string', 'a', 'b'); + testAssign('data', DATA1, DATA2); + testAssign('date', DATE1, DATE2); + + function testAssignNull(name, expected) { + TestCase.assertThrowsContaining(() => prim[name][0] = null, + `Property must be of type '${expected}', got (null)`, + undefined, 1); + } + + testAssignNull('bool', 'bool'); + testAssignNull('int', 'int'); + testAssignNull('float', 'float'); + testAssignNull('double', 'double'); + testAssignNull('string', 'string'); + testAssignNull('data', 'data'); + testAssignNull('date', 'date'); + + testAssign('optBool', true, null); + testAssign('optInt', 1, null); + testAssign('optFloat', 1.1, null); + testAssign('optDouble', 1.1, null); + testAssign('optString', 'a', null); + testAssign('optData', DATA1, null); + testAssign('optDate', DATE1, null); }); - TestCase.assertThrows(function() { - array[0] = {doubleCol: 1}; - }, 'cannot set list item outside write transaction'); + TestCase.assertThrowsContaining(() => array[0] = {doubleCol: 1}, + "Cannot modify managed objects outside of a write transaction."); + + array['foo'] = 'baz'; + TestCase.assertEqual(array.foo, 'baz'); }, - testListInvalidProperty: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + testListAssignment: function() { + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject, + schemas.PersonList, schemas.PersonObject, + schemas.PrimitiveArrays]}); - realm.write(function() { - var obj = realm.create('LinkTypesObject', { - objectCol: {doubleCol: 1}, - objectCol1: {doubleCol: 2}, - arrayCol: [{doubleCol: 3}, {doubleCol: 4}], - }); + let obj, prim; + realm.write(() => { + obj = realm.create('LinkTypesObject', {}); + prim = realm.create('PrimitiveArrays', {}); + let person = realm.create('PersonObject', {name: 'a', age: 2.0}); + let personList = realm.create('PersonList', {list: [person]}).list; - array = obj.arrayCol; + TestCase.assertThrowsContaining(() => obj.arrayCol = [0], + "JS value must be of type 'object', got (0)"); + TestCase.assertThrowsContaining(() => obj.arrayCol = [null], + "JS value must be of type 'object', got (null)"); + TestCase.assertThrowsContaining(() => obj.arrayCol = [person], + "Object of type (PersonObject) does not match List type (TestObject)"); + TestCase.assertThrowsContaining(() => obj.arrayCol = personList, + "LinkTypesObject.arrayCol must be of type 'TestObject[]', got 'object' ("); + obj.arrayCol = [realm.create('TestObject', {doubleCol: 1.0})] + TestCase.assertEqual(obj.arrayCol[0].doubleCol, 1.0); + obj.arrayCol = obj.arrayCol; + TestCase.assertEqual(obj.arrayCol[0].doubleCol, 1.0); + + TestCase.assertThrowsContaining(() => prim.bool = [person], + "PrimitiveArrays.bool must be of type 'boolean[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.int = [person], + "PrimitiveArrays.int must be of type 'number[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.float = [person], + "PrimitiveArrays.float must be of type 'number[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.double = [person], + "PrimitiveArrays.double must be of type 'number[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.string = [person], + "PrimitiveArrays.string must be of type 'string[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.data = [person], + "PrimitiveArrays.data must be of type 'binary[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.date = [person], + "PrimitiveArrays.date must be of type 'date[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optBool = [person], + "PrimitiveArrays.optBool must be of type 'boolean?[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optInt = [person], + "PrimitiveArrays.optInt must be of type 'number?[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optFloat = [person], + "PrimitiveArrays.optFloat must be of type 'number?[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optDouble = [person], + "PrimitiveArrays.optDouble must be of type 'number?[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optString = [person], + "PrimitiveArrays.optString must be of type 'string?[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optData = [person], + "PrimitiveArrays.optData must be of type 'binary?[]', got 'object' ([PersonObject{"); + TestCase.assertThrowsContaining(() => prim.optDate = [person], + "PrimitiveArrays.optDate must be of type 'date?[]', got 'object' ([PersonObject{"); + + function testAssign(name, value) { + prim[name] = [value]; + TestCase.assertSimilar(prim[name].type, prim[name][0], value, undefined, 1); + } + + testAssign('bool', true); + testAssign('int', 1); + testAssign('float', 1.1); + testAssign('double', 1.1); + testAssign('string', 'a'); + testAssign('data', DATA1); + testAssign('date', DATE1); + + function testAssignNull(name, expected) { + TestCase.assertThrowsContaining(() => prim[name] = [null], + `PrimitiveArrays.${name} must be of type '${expected}[]', got 'object' ([null])`, + undefined, 1); + TestCase.assertEqual(prim[name].length, 1, + "List should not have been cleared by invalid assignment", 1); + } + + testAssignNull('bool', 'boolean'); + testAssignNull('int', 'number'); + testAssignNull('float', 'number'); + testAssignNull('double', 'number'); + testAssignNull('string', 'string'); + testAssignNull('data', 'binary'); + testAssignNull('date', 'date'); + + testAssign('optBool', true); + testAssign('optInt', 1); + testAssign('optFloat', 1.1); + testAssign('optDouble', 1.1); + testAssign('optString', 'a'); + testAssign('optData', DATA1); + testAssign('optDate', DATE1); + + testAssign('optBool', null); + testAssign('optInt', null); + testAssign('optFloat', null); + testAssign('optDouble', null); + testAssign('optString', null); + testAssign('optData', null); + testAssign('optDate', null); }); - TestCase.assertEqual(undefined, array.ablasdf); + TestCase.assertThrowsContaining(() => obj.arrayCol = [], + "Cannot modify managed objects outside of a write transaction."); + TestCase.assertThrowsContaining(() => prim.bool = [], + "Cannot modify managed objects outside of a write transaction."); }, testListEnumerate: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var obj; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let obj; - realm.write(function() { + realm.write(() => { obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, @@ -155,19 +391,18 @@ module.exports = { }); }); - var index; - for (index in obj.arrayCol) { + for (const index in obj.arrayCol) { TestCase.assertTrue(false, "No objects should have been enumerated: " + index); } - realm.write(function() { + realm.write(() => { obj.arrayCol = [{doubleCol: 0}, {doubleCol: 1}]; - TestCase.assertEqual(obj.arrayCol.length, 2); }); + TestCase.assertEqual(obj.arrayCol.length, 2); - var count = 0; - var keys = Object.keys(obj.arrayCol); - for (index in obj.arrayCol) { + let count = 0; + let keys = Object.keys(obj.arrayCol); + for (const index in obj.arrayCol) { TestCase.assertEqual(count++, +index); TestCase.assertEqual(keys[index], index); } @@ -177,11 +412,11 @@ module.exports = { }, testListPush: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}], @@ -199,23 +434,22 @@ module.exports = { TestCase.assertEqual(array[2].doubleCol, 1); TestCase.assertEqual(array[3].doubleCol, 2); - TestCase.assertThrows(function() { - array.push(); - }); + TestCase.assertEqual(array.push(), 4); + TestCase.assertEqual(array.length, 4); }); TestCase.assertEqual(array.length, 4); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { array.push([1]); - }, 'can only push in a write transaction'); + }, "Cannot modify managed objects outside of a write transaction."); }, testListPop: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}, {doubleCol: 4}], @@ -228,22 +462,19 @@ module.exports = { TestCase.assertEqual(array.pop(), undefined); - TestCase.assertThrows(function() { - array.pop(1); - }); + TestCase.assertThrowsContaining(() => array.pop(1), 'Invalid argument'); }); - TestCase.assertThrows(function() { - array.pop(); - }, 'can only pop in a write transaction'); + TestCase.assertThrowsContaining(() => array.pop(), + "Cannot modify managed objects outside of a write transaction."); }, testListUnshift: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}], @@ -260,20 +491,22 @@ module.exports = { TestCase.assertEqual(array.length, 4); TestCase.assertEqual(array[0].doubleCol, 1); TestCase.assertEqual(array[1].doubleCol, 2); + + TestCase.assertEqual(array.unshift(), 4); + TestCase.assertEqual(array.length, 4); }); TestCase.assertEqual(array.length, 4); - TestCase.assertThrows(function() { - array.unshift({doubleCol: 1}); - }, 'can only unshift in a write transaction'); + TestCase.assertThrowsContaining(() => array.unshift({doubleCol: 1}), + 'Cannot modify managed objects outside of a write transaction.'); }, testListShift: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}, {doubleCol: 4}], @@ -286,29 +519,27 @@ module.exports = { TestCase.assertEqual(array.shift(), undefined); - TestCase.assertThrows(function() { - array.shift(1); - }); + TestCase.assertThrowsContaining(() => array.shift(1), 'Invalid argument'); }); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { array.shift(); - }, 'can only shift in a write transaction'); + }, "Cannot modify managed objects outside of a write transaction."); }, testListSplice: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}, {doubleCol: 4}], }); array = obj.arrayCol; - var removed; + let removed; removed = array.splice(0, 0, obj.objectCol, obj.objectCol1); TestCase.assertEqual(removed.length, 0); @@ -355,26 +586,26 @@ module.exports = { TestCase.assertEqual(removed.length, 1); TestCase.assertEqual(array.length, 0); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { array.splice('cat', 1); - }); + }, "Value 'cat' not convertible to a number"); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { array.splice(0, 0, 0); - }); + }, "JS value must be of type 'object', got (0)"); }); - TestCase.assertThrows(function() { + TestCase.assertThrowsContaining(() => { array.splice(0, 0, {doubleCol: 1}); - }, 'can only splice in a write transaction'); + }, "Cannot modify managed objects outside of a write transaction"); }, testListDeletions: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var object; - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let object; + let array; - realm.write(function() { + realm.write(() => { object = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, @@ -385,7 +616,7 @@ module.exports = { }); try { - realm.write(function() { + realm.write(() => { realm.delete(array[0]); TestCase.assertEqual(array.length, 1); TestCase.assertEqual(array[0].doubleCol, 4); @@ -398,22 +629,20 @@ module.exports = { TestCase.assertEqual(array.length, 2); TestCase.assertEqual(array[0].doubleCol, 3); - realm.write(function() { + realm.write(() => { realm.delete(object); }); - TestCase.assertThrows(function() { - array[0]; - }); + TestCase.assertThrowsContaining(() => array[0], 'invalidated'); }, testLiveUpdatingResults: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var objects = realm.objects('TestObject'); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let objects = realm.objects('TestObject'); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', { + realm.write(() => { + let obj = realm.create('LinkTypesObject', { objectCol: {doubleCol: 1}, objectCol1: {doubleCol: 2}, arrayCol: [{doubleCol: 3}, {doubleCol: 4}], @@ -426,7 +655,7 @@ module.exports = { TestCase.assertEqual(objects.length, 4); try { - realm.write(function() { + realm.write(() => { array.push({doubleCol: 5}); TestCase.assertEqual(objects.length, 5); @@ -449,50 +678,50 @@ module.exports = { }, testListSnapshot: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); - var objects = realm.objects('TestObject'); - var array; + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + let objects = realm.objects('TestObject'); + let array; - realm.write(function() { - var obj = realm.create('LinkTypesObject', [[1], [2], [[3], [4]]]); + realm.write(() => { + let obj = realm.create('LinkTypesObject', [[1], [2], [[3], [4]], [[5], [6]]]); array = obj.arrayCol; }); - var objectsCopy = objects.snapshot(); - var arrayCopy = array.snapshot(); + let objectsCopy = objects.snapshot(); + let arrayCopy = array.snapshot(); - TestCase.assertEqual(objectsCopy.length, 4); + TestCase.assertEqual(objectsCopy.length, 6); TestCase.assertEqual(arrayCopy.length, 2); - realm.write(function() { + realm.write(() => { array.push([5]); - TestCase.assertEqual(objectsCopy.length, 4); + TestCase.assertEqual(objectsCopy.length, 6); TestCase.assertEqual(arrayCopy.length, 2); - TestCase.assertEqual(objectsCopy.snapshot().length, 4); + TestCase.assertEqual(objectsCopy.snapshot().length, 6); TestCase.assertEqual(arrayCopy.snapshot().length, 2); - TestCase.assertEqual(objects.snapshot().length, 5); + TestCase.assertEqual(objects.snapshot().length, 7); TestCase.assertEqual(array.snapshot().length, 3); realm.delete(array[0]); - TestCase.assertEqual(objectsCopy.length, 4); + TestCase.assertEqual(objectsCopy.length, 6); TestCase.assertEqual(arrayCopy.length, 2); TestCase.assertEqual(arrayCopy[0], null); realm.deleteAll(); - TestCase.assertEqual(objectsCopy.length, 4); + TestCase.assertEqual(objectsCopy.length, 6); TestCase.assertEqual(arrayCopy.length, 2); TestCase.assertEqual(arrayCopy[1], null); }); }, testListFiltered: function() { - var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); - var list; + const realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); + let list; - realm.write(function() { - var object = realm.create('PersonList', {list: [ + realm.write(() => { + let object = realm.create('PersonList', {list: [ {name: 'Ari', age: 10}, {name: 'Tim', age: 11}, {name: 'Bjarne', age: 12}, @@ -515,10 +744,11 @@ module.exports = { {name: 'Target', properties: {value: 'int'}}, {name: 'Mid', properties: {value: 'int', link: 'Target'}}, {name: 'List', properties: {list: {type: 'list', objectType: 'Mid'}}}, + schemas.PrimitiveArrays ]; const realm = new Realm({schema: schema}); - let list; + let list, prim; realm.write(() => { list = realm.create('List', {list: [ {value: 3, link: {value: 1}}, @@ -528,13 +758,31 @@ module.exports = { realm.create('List', {list: [ {value: 4, link: {value: 4}}, ]}); + prim = realm.create('PrimitiveArrays', { + bool: [true, false], + int: [3, 1, 2], + float: [3, 1, 2], + double: [3, 1, 2], + string: ['c', 'a', 'b'], + data: [DATA3, DATA1, DATA2], + date: [DATE3, DATE1, DATE2], + optBool: [true, false, null], + optInt: [3, 1, 2, null], + optFloat: [3, 1, 2, null], + optDouble: [3, 1, 2, null], + optString: ['c', 'a', 'b', null], + optData: [DATA3, DATA1, DATA2, null], + optDate: [DATE3, DATE1, DATE2, null], + }); }); const values = (results) => results.map((o) => o.value); - TestCase.assertThrows(() => list.sorted()); - TestCase.assertThrows(() => list.sorted('nonexistent property')); - TestCase.assertThrows(() => list.sorted('link')); + // TestCase.assertThrowsContaining(() => list.sorted()); + TestCase.assertThrowsContaining(() => list.sorted('nonexistent property'), + "Cannot sort on key path 'nonexistent property': property 'Mid.nonexistent property' does not exist."); + TestCase.assertThrowsContaining(() => list.sorted('link'), + "Cannot sort on key path 'link': property 'Mid.link' of type 'object' cannot be the final property in the key path."); TestCase.assertArraysEqual(values(list.sorted([])), [3, 1, 2]); @@ -551,24 +799,50 @@ module.exports = { TestCase.assertArraysEqual(values(list.sorted(['link.value'])), [3, 2, 1]); TestCase.assertArraysEqual(values(list.sorted([['link.value', false]])), [3, 2, 1]); TestCase.assertArraysEqual(values(list.sorted([['link.value', true]])), [1, 2, 3]); + + TestCase.assertThrowsContaining(() => prim.int.sorted('value', true), + "Cannot sort on key path 'value': arrays of 'int' can only be sorted on 'self'"); + TestCase.assertThrowsContaining(() => prim.int.sorted('!ARRAY_VALUE', true), + "Cannot sort on key path '!ARRAY_VALUE': arrays of 'int' can only be sorted on 'self'"); + + TestCase.assertArraysEqual(prim.int.sorted([]), [3, 1, 2]); + TestCase.assertArraysEqual(prim.int.sorted(), [1, 2, 3]); + TestCase.assertArraysEqual(prim.int.sorted(false), [1, 2, 3]); + TestCase.assertArraysEqual(prim.int.sorted(true), [3, 2, 1]); + + TestCase.assertArraysEqual(prim.optInt.sorted([]), [3, 1, 2, null]); + TestCase.assertArraysEqual(prim.optInt.sorted(), [null, 1, 2, 3]); + TestCase.assertArraysEqual(prim.optInt.sorted(false), [null, 1, 2, 3]); + TestCase.assertArraysEqual(prim.optInt.sorted(true), [3, 2, 1, null]); + + TestCase.assertArraysEqual(prim.bool.sorted(), [false, true]); + TestCase.assertArraysEqual(prim.float.sorted(), [1, 2, 3]); + TestCase.assertArraysEqual(prim.double.sorted(), [1, 2, 3]); + TestCase.assertArraysEqual(prim.string.sorted(), ['a', 'b', 'c']); + TestCase.assertArraysEqual(prim.data.sorted(), [DATA1, DATA2, DATA3]); + TestCase.assertArraysEqual(prim.date.sorted(), [DATE1, DATE2, DATE3]); + TestCase.assertArraysEqual(prim.optBool.sorted(), [null, false, true]); + TestCase.assertArraysEqual(prim.optFloat.sorted(), [null, 1, 2, 3]); + TestCase.assertArraysEqual(prim.optDouble.sorted(), [null, 1, 2, 3]); + TestCase.assertArraysEqual(prim.optString.sorted(), [null, 'a', 'b', 'c']); + TestCase.assertArraysEqual(prim.optData.sorted(), [null, DATA1, DATA2, DATA3]); + TestCase.assertArraysEqual(prim.optDate.sorted(), [null, DATE1, DATE2, DATE3]); }, testArrayMethods: function() { - var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); - var object; + const realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList, schemas.PrimitiveArrays]}); + let object, prim; - realm.write(function() { + realm.write(() => { object = realm.create('PersonList', {list: [ {name: 'Ari', age: 10}, {name: 'Tim', age: 11}, {name: 'Bjarne', age: 12}, ]}); + prim = realm.create('PrimitiveArrays', {int: [10, 11, 12]}); }); - [ - object.list, - realm.objects('PersonObject'), - ].forEach(function(list) { + for (const list of [object.list, realm.objects('PersonObject')]) { TestCase.assertEqual(list.slice().length, 3); TestCase.assertEqual(list.slice(-1).length, 1); TestCase.assertEqual(list.slice(-1)[0].age, 12); @@ -581,43 +855,43 @@ module.exports = { TestCase.assertEqual(list.join(' '), 'Ari Tim Bjarne'); } - var count = 0; - list.forEach(function(p, i) { + let count = 0; + list.forEach((p, i) => { TestCase.assertEqual(p.name, list[i].name); count++; }); TestCase.assertEqual(count, list.length); - TestCase.assertArraysEqual(list.map(function(p) {return p.age}), [10, 11, 12]); - TestCase.assertTrue(list.some(function(p) {return p.age > 10})); - TestCase.assertTrue(list.every(function(p) {return p.age > 0})); + TestCase.assertArraysEqual(list.map(p => p.age), [10, 11, 12]); + TestCase.assertTrue(list.some(p => p.age > 10)); + TestCase.assertTrue(list.every(p => p.age > 0)); - var person = list.find(function(p) {return p.name == 'Tim'}); + let person = list.find(p => p.name == 'Tim'); TestCase.assertEqual(person.name, 'Tim'); - var index = list.findIndex(function(p) {return p.name == 'Tim'}); + let index = list.findIndex(p => p.name == 'Tim'); TestCase.assertEqual(index, 1); TestCase.assertEqual(list.indexOf(list[index]), index); - TestCase.assertEqual(list.reduce(function(n, p) {return n + p.age}, 0), 33); - TestCase.assertEqual(list.reduceRight(function(n, p) {return n + p.age}, 0), 33); + TestCase.assertEqual(list.reduce((n, p) => n + p.age, 0), 33); + TestCase.assertEqual(list.reduceRight((n, p) => n + p.age, 0), 33); // eslint-disable-next-line no-undef - var iteratorMethodNames = ['entries', 'keys', 'values']; + let iteratorMethodNames = ['entries', 'keys', 'values']; iteratorMethodNames.push(Symbol.iterator); - iteratorMethodNames.forEach(function(methodName) { - var iterator = list[methodName](); - var count = 0; - var result; + iteratorMethodNames.forEach(methodName => { + let iterator = list[methodName](); + let count = 0; + let result; // This iterator should itself be iterable. // TestCase.assertEqual(iterator[iteratorSymbol](), iterator); TestCase.assertEqual(iterator[Symbol.iterator](), iterator); while ((result = iterator.next()) && !result.done) { - var value = result.value; + let value = result.value; switch (methodName) { case 'entries': @@ -640,14 +914,83 @@ module.exports = { TestCase.assertEqual(result.value, undefined); TestCase.assertEqual(count, list.length); }); + } + + const list = prim.int; + TestCase.assertEqual(list.slice().length, 3); + TestCase.assertEqual(list.slice(-1).length, 1); + TestCase.assertEqual(list.slice(-1)[0], 12); + TestCase.assertEqual(list.slice(1, 3).length, 2); + TestCase.assertEqual(list.slice(1, 3)[1], 12); + + TestCase.assertEqual(list.join(' '), '10 11 12'); + + let count = 0; + list.forEach((v, i) => { + TestCase.assertEqual(v, i + 10); + count++; + }); + TestCase.assertEqual(count, list.length); + + TestCase.assertArraysEqual(list.map(p => p + 1), [11, 12, 13]); + TestCase.assertTrue(list.some(p => p > 10)); + TestCase.assertTrue(list.every(p => p > 0)); + + let value = list.find(p => p == 11); + TestCase.assertEqual(value, 11) + + let index = list.findIndex(p => p == 11); + TestCase.assertEqual(index, 1); + TestCase.assertEqual(list.indexOf(list[index]), index); + + TestCase.assertEqual(list.reduce((n, p) => n + p, 0), 33); + TestCase.assertEqual(list.reduceRight((n, p) => n + p, 0), 33); + + // eslint-disable-next-line no-undef + let iteratorMethodNames = ['entries', 'keys', 'values']; + + iteratorMethodNames.push(Symbol.iterator); + + iteratorMethodNames.forEach(methodName => { + let iterator = list[methodName](); + let count = 0; + let result; + + // This iterator should itself be iterable. + // TestCase.assertEqual(iterator[iteratorSymbol](), iterator); + TestCase.assertEqual(iterator[Symbol.iterator](), iterator); + + while ((result = iterator.next()) && !result.done) { + let value = result.value; + + switch (methodName) { + case 'entries': + TestCase.assertEqual(value.length, 2); + TestCase.assertEqual(value[0], count); + TestCase.assertEqual(value[1], list[count]); + break; + case 'keys': + TestCase.assertEqual(value, count); + break; + default: + TestCase.assertEqual(value.name, list[count].name); + break; + } + + count++; + } + + TestCase.assertEqual(result.done, true); + TestCase.assertEqual(result.value, undefined); + TestCase.assertEqual(count, list.length); }); }, testIsValid: function() { - var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); - var object; - var list; - realm.write(function() { + const realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); + let object; + let list; + realm.write(() => { object = realm.create('PersonList', {list: [ {name: 'Ari', age: 10}, {name: 'Tim', age: 11}, @@ -659,8 +1002,6 @@ module.exports = { }); TestCase.assertEqual(list.isValid(), false); - TestCase.assertThrows(function() { - list.length; - }); + TestCase.assertThrowsContaining(() => list.length, 'invalidated'); }, }; diff --git a/tests/js/migration-tests.js b/tests/js/migration-tests.js index de5cd271..b9ac1048 100644 --- a/tests/js/migration-tests.js +++ b/tests/js/migration-tests.js @@ -88,8 +88,8 @@ module.exports = { renamed: 'string', prop1: 'int', } - }], - schemaVersion: 1, + }], + schemaVersion: 1, migration: function(oldRealm, newRealm) { var oldObjects = oldRealm.objects('TestObject'); var newObjects = newRealm.objects('TestObject'); @@ -136,8 +136,8 @@ module.exports = { renamed: 'string', prop1: 'int', } - }], - schemaVersion: 1, + }], + schemaVersion: 1, migration: function(oldRealm, newRealm) { var oldSchema = oldRealm.schema; var newSchema = newRealm.schema; @@ -324,4 +324,44 @@ module.exports = { realm.close(); }, + + testMigrateToListOfInts: function() { + let realm = new Realm({schema: [{name: 'TestObject', properties: {values: 'IntObject[]'}}, + {name: 'IntObject', properties: {value: 'int'}}]}); + realm.write(function() { + realm.create('TestObject', {values: [{value: 1}, {value: 2}, {value: 3}]}); + realm.create('TestObject', {values: [{value: 1}, {value: 4}, {value: 5}]}); + }); + realm.close(); + + realm = new Realm({ + schema: [{name: 'TestObject', properties: {values: 'int[]'}}], + schemaVersion: 1, + migration: function(oldRealm, newRealm) { + const oldObjects = oldRealm.objects('TestObject'); + const newObjects = newRealm.objects('TestObject'); + TestCase.assertEqual(oldObjects.length, 2); + TestCase.assertEqual(newObjects.length, 2); + + for (let i = 0; i < oldObjects.length; ++i) { + TestCase.assertEqual(oldObjects[i].values.length, 3); + TestCase.assertEqual(newObjects[i].values.length, 0); + newObjects[i].values = oldObjects[i].values.map(o => o.value); + TestCase.assertEqual(newObjects[i].values.length, 3); + } + newRealm.deleteModel('IntObject'); + } + }); + + const objects = realm.objects('TestObject'); + TestCase.assertEqual(objects.length, 2); + TestCase.assertEqual(objects[0].values.length, 3); + TestCase.assertEqual(objects[1].values.length, 3); + TestCase.assertEqual(objects[0].values[0], 1); + TestCase.assertEqual(objects[0].values[1], 2); + TestCase.assertEqual(objects[0].values[2], 3); + TestCase.assertEqual(objects[1].values[0], 1); + TestCase.assertEqual(objects[1].values[1], 4); + TestCase.assertEqual(objects[1].values[2], 5); + }, }; diff --git a/tests/js/object-tests.js b/tests/js/object-tests.js index 64c84890..0b19e20e 100644 --- a/tests/js/object-tests.js +++ b/tests/js/object-tests.js @@ -18,238 +18,158 @@ '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'); -var RANDOM_DATA = new Uint8Array([ +const RANDOM_DATA = new Uint8Array([ 0xd8, 0x21, 0xd6, 0xe8, 0x00, 0x57, 0xbc, 0xb2, 0x6a, 0x15, 0x77, 0x30, 0xac, 0x77, 0x96, 0xd9, 0x67, 0x1e, 0x40, 0xa7, 0x6d, 0x52, 0x83, 0xda, 0x07, 0x29, 0x9c, 0x70, 0x38, 0x48, 0x4e, 0xff, ]); -module.exports = { - testBasicTypesPropertyGetters: function() { - var realm = new Realm({schema: [schemas.BasicTypes]}); - var object; +const allTypesValues = { + boolCol: true, + intCol: 1, + floatCol: 1.1, + doubleCol: 1.11, + stringCol: 'string', + dateCol: new Date(1), + dataCol: RANDOM_DATA, + objectCol: {doubleCol: 2.2}, - var basicTypesValues = { - boolCol: true, - intCol: 1, - floatCol: 1.1, - doubleCol: 1.11, - stringCol: 'string', - dateCol: new Date(1), - dataCol: RANDOM_DATA, - }; + optBoolCol: true, + optIntCol: 1, + optFloatCol: 1.1, + optDoubleCol: 1.11, + optStringCol: 'string', + optDateCol: new Date(1), + optDataCol: RANDOM_DATA, + + boolArrayCol: [true], + intArrayCol: [1], + floatArrayCol: [1.1], + doubleArrayCol: [1.11], + stringArrayCol: ['string'], + dateArrayCol: [new Date(1)], + dataArrayCol: [RANDOM_DATA], + objectArrayCol: [{doubleCol: 2.2}], + + optBoolArrayCol: [true], + optIntArrayCol: [1], + optFloatArrayCol: [1.1], + optDoubleArrayCol: [1.11], + optStringArrayCol: ['string'], + optDateArrayCol: [new Date(1)], + optDataArrayCol: [RANDOM_DATA], +}; +const nullPropertyValues = (() => { + let values = {} + for (let name in allTypesValues) { + if (name.includes('opt')) { + values[name] = name.includes('Array') ? [null] : null; + } + else { + values[name] = allTypesValues[name]; + } + } + return values; +})(); + +module.exports = { + testAllPropertyGetters: function() { + const realm = new Realm({schema: [schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); + let object, nullObject; realm.write(function() { - object = realm.create('BasicTypesObject', basicTypesValues); + object = realm.create('AllTypesObject', allTypesValues); + nullObject = realm.create('AllTypesObject', nullPropertyValues); }); - for (var name in schemas.BasicTypes.properties) { - var prop = schemas.BasicTypes.properties[name]; - var type = typeof prop == 'object' ? prop.type : prop; + const objectSchema = realm.schema[0]; + for (const name of Object.keys(objectSchema.properties)) { + const type = objectSchema.properties[name].type; + if (type === 'linkingObjects') { + TestCase.assertEqual(object[name].length, 0); + TestCase.assertEqual(nullObject[name].length, 0); + continue; + } - if (type == 'float' || type == 'double') { - TestCase.assertEqualWithTolerance(object[name], basicTypesValues[name], 0.000001); - } - else if (type == 'data') { - TestCase.assertArraysEqual(new Uint8Array(object[name]), RANDOM_DATA); - } - else if (type == 'date') { - TestCase.assertEqual(object[name].getTime(), basicTypesValues[name].getTime()); - } - else { - TestCase.assertEqual(object[name], basicTypesValues[name]); - } + TestCase.assertSimilar(type, object[name], allTypesValues[name]); + TestCase.assertSimilar(type, nullObject[name], nullPropertyValues[name]); } TestCase.assertEqual(object.nonexistent, undefined); }, - testNullableBasicTypesPropertyGetters: function() { - var realm = new Realm({schema: [schemas.NullableBasicTypes]}); - var object, nullObject; - var basicTypesValues = { - boolCol: true, - intCol: 1, - floatCol: 1.1, - doubleCol: 1.11, - stringCol: 'string', - dateCol: new Date(1), - dataCol: RANDOM_DATA, - }; + testAllTypesPropertySetters: function() { + const realm = new Realm({schema: [schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); + let obj; realm.write(function() { - object = realm.create('NullableBasicTypesObject', basicTypesValues); - - nullObject = realm.create('NullableBasicTypesObject', { - boolCol: null, - intCol: null, - floatCol: null, - doubleCol: null, - stringCol: null, - dateCol: null, - dataCol: null, - }); + obj = realm.create('AllTypesObject', allTypesValues); }); - for (var name in schemas.BasicTypes.properties) { - var prop = schemas.BasicTypes.properties[name]; - var type = typeof prop == 'object' ? prop.type : prop; - - TestCase.assertEqual(nullObject[name], null); - - if (type == 'float' || type == 'double') { - TestCase.assertEqualWithTolerance(object[name], basicTypesValues[name], 0.000001); - } - else if (type == 'data') { - TestCase.assertArraysEqual(new Uint8Array(object[name]), RANDOM_DATA); - } - else if (type == 'date') { - TestCase.assertEqual(object[name].getTime(), basicTypesValues[name].getTime()); - } - else { - TestCase.assertEqual(object[name], basicTypesValues[name]); - } - } - - }, - testBasicTypesPropertySetters: function() { - var realm = new Realm({schema: [schemas.BasicTypes]}); - var obj; - - var basicTypesValues = { - boolCol: true, - intCol: 1, - floatCol: 1.1, - doubleCol: 1.11, - stringCol: 'string', - dateCol: new Date(1), - dataCol: new ArrayBuffer(), - }; - - realm.write(function() { - obj = realm.create('BasicTypesObject', basicTypesValues); + TestCase.assertThrows(function() { obj.boolCol = false; - obj.intCol = 2; - obj.floatCol = 2.2; - obj.doubleCol = 2.22; - obj.stringCol = 'STRING'; - obj.dateCol = new Date(2); - obj.dataCol = RANDOM_DATA; - }); - - TestCase.assertEqual(obj.boolCol, false, 'wrong bool value'); - TestCase.assertEqual(obj.intCol, 2, 'wrong int value'); - TestCase.assertEqualWithTolerance(obj.floatCol, 2.2, 0.000001, 'wrong float value'); - TestCase.assertEqualWithTolerance(obj.doubleCol, 2.22, 0.000001, 'wrong double value'); - TestCase.assertEqual(obj.stringCol, 'STRING', 'wrong string value'); - TestCase.assertEqual(obj.dateCol.getTime(), 2, 'wrong date value'); - TestCase.assertArraysEqual(new Uint8Array(obj.dataCol), RANDOM_DATA, 'wrong data value'); - - realm.write(function() { - TestCase.assertThrows(function() { - obj.boolCol = 'cat'; - }); - TestCase.assertThrows(function() { - obj.intCol = 'dog'; - }); - - TestCase.assertThrows(function() { - obj.boolCol = null; - }); - TestCase.assertThrows(function() { - obj.boolCol = undefined; - }); - TestCase.assertThrows(function() { - obj.intCol = null; - }); - TestCase.assertThrows(function() { - obj.intCol = undefined; - }); - TestCase.assertThrows(function() { - obj.floatCol = null; - }); - TestCase.assertThrows(function() { - obj.floatCol = undefined; - }); - TestCase.assertThrows(function() { - obj.doubleCol = null; - }); - TestCase.assertThrows(function() { - obj.doubleCol = undefined; - }); - TestCase.assertThrows(function() { - obj.stringCol = null; - }); - TestCase.assertThrows(function() { - obj.stringCol = undefined; - }); - TestCase.assertThrows(function() { - obj.dateCol = null; - }); - TestCase.assertThrows(function() { - obj.dateCol = undefined; - }); - TestCase.assertThrows(function() { - obj.dataCol = null; - }); - TestCase.assertThrows(function() { - obj.dataCol = undefined; - }); - }); - - TestCase.assertThrows(function() { - obj.boolCol = true; }, 'can only set property values in a write transaction'); - TestCase.assertEqual(obj.boolCol, false, 'bool value changed outside transaction'); - }, - testNullableBasicTypesPropertySetters: function() { - var realm = new Realm({schema: [schemas.NullableBasicTypes]}); - var obj, obj1; - - var basicTypesValues = { - boolCol: true, - intCol: 1, - floatCol: 1.1, - doubleCol: 1.11, - stringCol: 'string', - dateCol: new Date(1), - dataCol: RANDOM_DATA, - }; + TestCase.assertEqual(obj.boolCol, true, 'bool value changed outside transaction'); realm.write(function() { - obj = realm.create('NullableBasicTypesObject', basicTypesValues); - obj1 = realm.create('NullableBasicTypesObject', basicTypesValues); + TestCase.assertThrows(() => obj.boolCol = 'cat'); + TestCase.assertThrows(() => obj.intCol = 'dog'); - for (var name in schemas.NullableBasicTypes.properties) { - obj[name] = null; - obj1[name] = undefined; + // Non-optional properties should complain about null + for (const name of ['boolCol', 'intCol', 'floatCol', 'doubleCol', 'stringCol', 'dataCol', 'dateCol']) { + TestCase.assertThrows(() => obj[name] = null, `Setting ${name} to null should throw`); + TestCase.assertThrows(() => obj[name] = undefined, `Setting ${name} to undefined should throw`); } + + // Optional properties should allow it + for (const name of ['optBoolCol', 'optIntCol', 'optFloatCol', 'optDoubleCol', + 'optStringCol', 'optDataCol', 'optDateCol', 'objectCol']) { + obj[name] = null; + TestCase.assertEqual(obj[name], null); + obj[name] = undefined; + TestCase.assertEqual(obj[name], null); + } + + function tryAssign(name, value) { + var prop = schemas.AllTypes.properties[name]; + var type = typeof prop == 'object' ? prop.type : prop; + obj[name] = value; + TestCase.assertSimilar(type, obj[name], value, undefined, 1); + } + + tryAssign('boolCol', false); + tryAssign('intCol', 10); + tryAssign('floatCol', 2.2); + tryAssign('doubleCol', 3.3); + tryAssign('stringCol', 'new str'); + tryAssign('dateCol', new Date(2)); + tryAssign('dataCol', RANDOM_DATA); + + tryAssign('optBoolCol', null); + tryAssign('optIntCol', null); + tryAssign('optFloatCol', null); + tryAssign('optDoubleCol', null); + tryAssign('optStringCol', null); + tryAssign('optDateCol', null); + tryAssign('optDataCol', null); + + tryAssign('optBoolCol', false); + tryAssign('optIntCol', 10); + tryAssign('optFloatCol', 2.2); + tryAssign('optDoubleCol', 3.3); + tryAssign('optStringCol', 'new str'); + tryAssign('optDateCol', new Date(2)); + tryAssign('optDataCol', RANDOM_DATA); + }); - - for (var name in schemas.NullableBasicTypes.properties) { - TestCase.assertEqual(obj[name], null); - TestCase.assertEqual(obj1[name], null); - } - - realm.write(function() { - TestCase.assertThrows(function() { - obj.boolCol = 'cat'; - }); - TestCase.assertThrows(function() { - obj.intCol = 'dog'; - }); - }); - - TestCase.assertThrows(function() { - obj.boolCol = null; - }, 'can only set property values in a write transaction'); }, + testLinkTypesPropertyGetters: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); var obj = null; realm.write(function() { @@ -273,8 +193,9 @@ module.exports = { TestCase.assertEqual(arrayVal.length, 1); TestCase.assertEqual(arrayVal[0].doubleCol, 3); }, + testLinkTypesPropertySetters: function() { - var realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); + const realm = new Realm({schema: [schemas.LinkTypes, schemas.TestObject]}); var objects = realm.objects('TestObject'); var obj; @@ -340,33 +261,25 @@ module.exports = { }); TestCase.assertEqual(obj.objectCol.doubleCol, 3); }, + testEnumerablePropertyNames: function() { - var realm = new Realm({schema: [schemas.BasicTypes]}); - var object; + const realm = new Realm({schema: [schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); + let object; - realm.write(function() { - object = realm.create('BasicTypesObject', { - boolCol: true, - intCol: 1, - floatCol: 1.1, - doubleCol: 1.11, - stringCol: 'string', - dateCol: new Date(1), - dataCol: RANDOM_DATA, - }); - }); + realm.write(() => object = realm.create('AllTypesObject', allTypesValues)); - var propNames = Object.keys(schemas.BasicTypes.properties); + const propNames = Object.keys(schemas.AllTypes.properties); TestCase.assertArraysEqual(Object.keys(object), propNames, 'Object.keys'); - for (var key in object) { + for (let key in object) { TestCase.assertEqual(key, propNames.shift()); } TestCase.assertEqual(propNames.length, 0); }, + testDataProperties: function() { - var realm = new Realm({schema: [schemas.DefaultValues, schemas.TestObject]}); + const realm = new Realm({schema: [schemas.DefaultValues, schemas.TestObject]}); var object; // Should be be able to set a data property with a typed array. @@ -444,7 +357,7 @@ module.exports = { }, testObjectConstructor: function() { - var realm = new Realm({schema: [schemas.TestObject]}); + const realm = new Realm({schema: [schemas.TestObject]}); realm.write(function() { var obj = realm.create('TestObject', {doubleCol: 1}); @@ -456,7 +369,7 @@ module.exports = { }, testIsValid: function() { - var realm = new Realm({schema: [schemas.TestObject]}); + const realm = new Realm({schema: [schemas.TestObject]}); var obj; realm.write(function() { obj = realm.create('TestObject', {doubleCol: 1}); @@ -470,9 +383,9 @@ module.exports = { obj.doubleCol; }); }, - + testObjectSchema: function() { - var realm = new Realm({schema: [schemas.TestObject]}); + const realm = new Realm({schema: [schemas.TestObject]}); var obj; realm.write(function() { obj = realm.create('TestObject', {doubleCol: 1}); @@ -515,7 +428,7 @@ module.exports = { TestCase.assertEqual(realm_v3.objects('Date')[0].nullDate.getTime(), 1462500087955); TestCase.assertEqual(realm_v3.objects('Date')[1].currentDate.getTime(), -10000); TestCase.assertEqual(realm_v3.objects('Date')[1].nullDate, null); - + // test different dates var realm = new Realm({schema: [schemas.DateObject]}); realm.write(function() { diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 251e8e8d..9762ccfb 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -95,7 +95,8 @@ module.exports = { }, testRealmConstructorSchemaValidation: function() { - TestCase.assertThrowsContaining(() => new Realm({schema: schemas.AllTypes}), "schema must be of type 'array', got"); + 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: [{}]}), @@ -105,26 +106,21 @@ module.exports = { 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.assertThrowsContaining(() => { - new Realm({schema: [{ - name: 'InvalidObject', - properties: { - linkingObjects: {type:'linkingObjects', objectType: 'InvalidObject', property: 'nosuchproperty'} - } - }]}); - }, "Property 'InvalidObject.nosuchproperty' declared as origin of linking objects property 'InvalidObject.linkingObjects' does not exist"); + function assertPropertyInvalid(prop, message) { + TestCase.assertThrowsContaining(() => { + new Realm({schema: [{name: 'InvalidObject', properties: { int: 'int', bad: prop }}]}); + }, message, 1); + } - // linkingObjects property where the source property is not a link - TestCase.assertThrowsContaining(() => { - new Realm({schema: [{ - name: 'InvalidObject', - properties: { - integer: 'int', - linkingObjects: {type:'linkingObjects', objectType: 'InvalidObject', property: 'integer'} - } - }]}); - }, "Property 'InvalidObject.integer' declared as origin of linking objects property 'InvalidObject.linkingObjects' is not a link") + assertPropertyInvalid({type:'list[]', objectType: 'InvalidObject'}, + "List property 'InvalidObject.bad' must have a non-list value type"); + assertPropertyInvalid({type:'list?', objectType: 'InvalidObject'}, + "List property 'InvalidObject.bad' cannot be optional"); + assertPropertyInvalid('', "Property 'InvalidObject.bad' must have a non-empty type"); + assertPropertyInvalid({type:'linkingObjects', objectType: 'InvalidObject', property: 'nosuchproperty'}, + "Property 'InvalidObject.nosuchproperty' declared as origin of linking objects property 'InvalidObject.bad' does not exist"); + assertPropertyInvalid({type:'linkingObjects', objectType: 'InvalidObject', property: 'int'}, + "Property 'InvalidObject.int' declared as origin of linking objects property 'InvalidObject.bad' is not a link"); // linkingObjects property where the source property links elsewhere TestCase.assertThrowsContaining(() => { @@ -141,8 +137,18 @@ module.exports = { } }]}); }, "Property 'InvalidObject.link' declared as origin of linking objects property 'InvalidObject.linkingObjects' links to type 'IntObject'") + + { + new Realm({schema: [{ + name: 'Object', + properties: { + // weird but valid + objectList: {type:'object[]', objectType: 'Object'} + } + }]}); + } }, - + testRealmConstructorInMemory: function() { // open in-memory realm instance const realm1 = new Realm({inMemory: true, schema: [schemas.TestObject]}); @@ -166,7 +172,7 @@ module.exports = { // Open the same in-memory realm again and verify that it is now empty const realm3 = new Realm({inMemory: true}); TestCase.assertEqual(realm3.schema.length, 0); - + // try to open the same realm in persistent mode (should fail as you cannot mix modes) TestCase.assertThrowsContaining(() => new Realm({}), 'already opened with different inMemory settings.'); }, @@ -296,23 +302,10 @@ module.exports = { }); }, - testRealmCreateOptionals: 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 (const name in schemas.NullableBasicTypes.properties) { - TestCase.assertEqual(basic[name], null); - } - TestCase.assertEqual(links.objectCol, null); - TestCase.assertEqual(links.arrayCol.length, 0); - }, - testRealmCreateUpsert: function() { - const realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]}); - realm.write(() => { + const realm = new Realm({schema: [schemas.AllPrimaryTypes, schemas.TestObject, + schemas.StringPrimary]}); + realm.write(function() { const values = { primaryCol: '0', boolCol: true, @@ -326,12 +319,12 @@ module.exports = { arrayCol: [], }; - const obj0 = realm.create('AllTypesObject', values); + const obj0 = realm.create('AllPrimaryTypesObject', values); - TestCase.assertThrowsContaining(() => realm.create('AllTypesObject', values), - "Attempting to create an object of type 'AllTypesObject' with an existing primary key value '0'."); + TestCase.assertThrowsContaining(() => realm.create('AllPrimaryTypesObject', values), + "Attempting to create an object of type 'AllPrimaryTypesObject' with an existing primary key value ''0''."); - const obj1 = realm.create('AllTypesObject', { + const obj1 = realm.create('AllPrimaryTypesObject', { primaryCol: '1', boolCol: false, intCol: 2, @@ -344,10 +337,10 @@ module.exports = { arrayCol: [{doubleCol: 2}], }, true); - const objects = realm.objects('AllTypesObject'); + const objects = realm.objects('AllPrimaryTypesObject'); TestCase.assertEqual(objects.length, 2); - realm.create('AllTypesObject', { + realm.create('AllPrimaryTypesObject', { primaryCol: '0', boolCol: false, intCol: 2, @@ -371,13 +364,13 @@ module.exports = { TestCase.assertEqual(obj0.objectCol, null); TestCase.assertEqual(obj0.arrayCol.length, 1); - realm.create('AllTypesObject', {primaryCol: '0'}, true); - realm.create('AllTypesObject', {primaryCol: '1'}, true); + realm.create('AllPrimaryTypesObject', {primaryCol: '0'}, true); + realm.create('AllPrimaryTypesObject', {primaryCol: '1'}, true); TestCase.assertEqual(obj0.stringCol, '2'); TestCase.assertEqual(obj0.objectCol, null); TestCase.assertEqual(obj1.objectCol.doubleCol, 0); - realm.create('AllTypesObject', { + realm.create('AllPrimaryTypesObject', { primaryCol: '0', stringCol: '3', objectCol: {doubleCol: 0}, @@ -393,13 +386,13 @@ module.exports = { TestCase.assertEqual(obj0.objectCol.doubleCol, 0); TestCase.assertEqual(obj0.arrayCol.length, 1); - realm.create('AllTypesObject', {primaryCol: '0', objectCol: undefined}, true); - realm.create('AllTypesObject', {primaryCol: '1', objectCol: null}, true); + realm.create('AllPrimaryTypesObject', {primaryCol: '0', objectCol: undefined}, true); + realm.create('AllPrimaryTypesObject', {primaryCol: '1', objectCol: null}, true); TestCase.assertEqual(obj0.objectCol, null); TestCase.assertEqual(obj1.objectCol, null); // test with string primaries - const obj =realm.create('StringPrimaryObject', { + const obj = realm.create('StringPrimaryObject', { primaryCol: '0', valueCol: 0 }); @@ -784,8 +777,9 @@ module.exports = { }, testSchema: function() { - const originalSchema = [schemas.TestObject, schemas.BasicTypes, schemas.NullableBasicTypes, schemas.IndexedTypes, schemas.IntPrimary, - schemas.PersonObject, schemas.LinkTypes, schemas.LinkingObjectsObject]; + const originalSchema = [schemas.TestObject, schemas.AllTypes, schemas.LinkToAllTypes, + schemas.IndexedTypes, schemas.IntPrimary, schemas.PersonObject, + schemas.LinkTypes, schemas.LinkingObjectsObject]; const schemaMap = {}; originalSchema.forEach(objectSchema => { @@ -801,45 +795,67 @@ module.exports = { const schema = realm.schema; TestCase.assertEqual(schema.length, originalSchema.length); - function isString(val) { - return typeof val === 'string' || val instanceof String; - } + const normalizeProperty = (val) => { + let prop; + if (typeof val !== 'string' && !(val instanceof String)) { + prop = val; + prop.optional = val.optional || false; + prop.indexed = val.indexed || false; + } + else { + prop = {type: val, indexed: false, optional: false}; + } + if (prop.type.includes('?')) { + prop.optional = true; + prop.type = prop.type.replace('?', ''); + } + if (prop.type.includes('[]')) { + prop.objectType = prop.type.replace('[]', ''); + prop.type = 'list'; + } + return prop; + }; - function verifyObjectSchema(returned) { - let original = schemaMap[returned.name]; + for (const objectSchema of schema) { + let original = schemaMap[objectSchema.name]; if (original.schema) { original = original.schema; } - TestCase.assertEqual(returned.primaryKey, original.primaryKey); - 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); + TestCase.assertEqual(objectSchema.primaryKey, original.primaryKey); + for (const propName in objectSchema.properties) { + TestCase.assertDefined(original.properties[propName], `schema has unexpected property ${propName}`); + + const actual = objectSchema.properties[propName]; + const expected = normalizeProperty(original.properties[propName]); + TestCase.assertEqual(actual.name, propName); + TestCase.assertEqual(actual.indexed, expected.indexed); + + if (actual.type == 'object') { + TestCase.assertEqual(actual.objectType, expected.type === 'object' ? expected.objectType : expected.type); + TestCase.assertEqual(actual.optional, true); + TestCase.assertUndefined(actual.property); } - else if (prop1.type == 'list') { - TestCase.assertEqual(prop1.objectType, prop2.objectType); - TestCase.assertEqual(prop1.optional, undefined); + else if (actual.type == 'list') { + TestCase.assertEqual(actual.type, expected.type); + TestCase.assertEqual(actual.objectType, expected.objectType); + TestCase.assertEqual(actual.optional, expected.optional); + TestCase.assertUndefined(actual.property); } - else if (prop1.type == 'linking objects') { - TestCase.assertEqual(prop1.objectType, prop2.objectType); - TestCase.assertEqual(prop1.property, prop2.property); - TestCase.assertEqual(prop1.optional, undefined); + else if (actual.type == 'linkingObjects') { + TestCase.assertEqual(actual.type, expected.type); + TestCase.assertEqual(actual.objectType, expected.objectType); + TestCase.assertEqual(actual.property, expected.property); + TestCase.assertEqual(actual.optional, false); } else { - TestCase.assertEqual(prop1.type, isString(prop2) ? prop2 : prop2.type); - TestCase.assertEqual(prop1.optional, prop2.optional || undefined); + TestCase.assertEqual(actual.type, expected.type); + TestCase.assertEqual(actual.optional, expected.optional); + TestCase.assertUndefined(actual.property); + TestCase.assertUndefined(actual.objectType); } - - TestCase.assertEqual(prop1.indexed, prop2.indexed || undefined); } } - - for (let i = 0; i < originalSchema.length; i++) { - verifyObjectSchema(schema[i]); - } }, testCopyBundledRealmFiles: function() { @@ -869,7 +885,7 @@ module.exports = { const p1 = realm.create('PersonObject', { name: 'Ari', age: 10 }); p1.age = "Ten"; }); - }, new Error("PersonObject.age must be of type 'number', got (Ten)")); + }, new Error("PersonObject.age must be of type 'number', got 'string' ('Ten')")); }, testErrorMessageFromInvalidCreate: function() { @@ -879,7 +895,7 @@ module.exports = { realm.write(() => { const p1 = realm.create('PersonObject', { name: 'Ari', age: 'Ten' }); }); - }, new Error("PersonObject.age must be of type 'number', got (Ten)")); + }, new Error("PersonObject.age must be of type 'number', got 'string' ('Ten')")); }, testValidTypesForListProperties: function() { @@ -934,7 +950,7 @@ module.exports = { realm.cancelTransaction(); TestCase.assertTrue(!realm.isInTransaction); }, - + testCompact: function() { let wasCalled = false; const count = 1000; diff --git a/tests/js/results-tests.js b/tests/js/results-tests.js index ad7395e2..b8458870 100644 --- a/tests/js/results-tests.js +++ b/tests/js/results-tests.js @@ -299,7 +299,7 @@ module.exports = { }, testResultsInvalidation: function() { - var realm = new Realm({schema: [schemas.TestObject]}); + let realm = new Realm({schema: [schemas.TestObject]}); realm.write(function() { for (var i = 10; i > 0; i--) { realm.create('TestObject', [i]); @@ -322,7 +322,7 @@ module.exports = { realm.close(); realm = new Realm({ schemaVersion: 1, - schema: [schemas.TestObject, schemas.BasicTypes] + schema: [schemas.TestObject, schemas.DateObject] }); resultsVariants.forEach(function(objects) { diff --git a/tests/js/schemas.js b/tests/js/schemas.js index 2dde4649..6a67bd31 100644 --- a/tests/js/schemas.js +++ b/tests/js/schemas.js @@ -33,7 +33,7 @@ PersonObject.schema = { properties: { name: 'string', age: 'double', - married: {type: 'bool', default: false}, + married: {type: 'bool', default: false}, children: {type: 'list', objectType: 'PersonObject'}, parents: {type: 'linkingObjects', objectType: 'PersonObject', property: 'children'}, } @@ -51,7 +51,7 @@ exports.PersonObject = PersonObject; exports.PersonList = { name: 'PersonList', properties: { - list: {type: 'list', objectType: 'PersonObject'}, + list: 'PersonObject[]', } }; @@ -68,26 +68,82 @@ exports.BasicTypes = { } }; -exports.NullableBasicTypes = { - name: 'NullableBasicTypesObject', +exports.AllTypes = { + name: 'AllTypesObject', properties: { - boolCol: {type: 'bool', optional: true}, - intCol: {type: 'int', optional: true}, - floatCol: {type: 'float', optional: true}, - doubleCol: {type: 'double', optional: true}, - stringCol: {type: 'string', optional: true}, - dateCol: {type: 'date', optional: true}, - dataCol: {type: 'data', optional: true}, + boolCol: 'bool', + intCol: 'int', + floatCol: 'float', + doubleCol: 'double', + stringCol: 'string', + dateCol: 'date', + dataCol: 'data', + objectCol: 'TestObject', + + optBoolCol: 'bool?', + optIntCol: 'int?', + optFloatCol: 'float?', + optDoubleCol: 'double?', + optStringCol: 'string?', + optDateCol: 'date?', + optDataCol: 'data?', + + boolArrayCol: 'bool[]', + intArrayCol: 'int[]', + floatArrayCol: 'float[]', + doubleArrayCol: 'double[]', + stringArrayCol: 'string[]', + dateArrayCol: 'date[]', + dataArrayCol: 'data[]', + objectArrayCol: 'TestObject[]', + + optBoolArrayCol: 'bool?[]', + optIntArrayCol: 'int?[]', + optFloatArrayCol: 'float?[]', + optDoubleArrayCol: 'double?[]', + optStringArrayCol: 'string?[]', + optDateArrayCol: 'date?[]', + optDataArrayCol: 'data?[]', + + linkingObjectsCol: {type: 'linkingObjects', objectType: 'LinkToAllTypesObject', property: 'allTypesCol'}, } }; +exports.AllPrimaryTypes = { + name: 'AllPrimaryTypesObject', + primaryKey: 'primaryCol', + properties: { + primaryCol: 'string', + boolCol: 'bool', + intCol: 'int', + floatCol: 'float', + doubleCol: 'double', + stringCol: 'string', + dateCol: 'date', + dataCol: 'data', + objectCol: 'TestObject', + arrayCol: {type: 'list', objectType: 'TestObject'}, + } +}; + +exports.LinkToAllTypes = { + name: 'LinkToAllTypesObject', + properties: { + allTypesCol: 'AllTypesObject', + } +} + exports.IndexedTypes = { name: 'IndexedTypesObject', properties: { - boolCol: {type: 'bool', indexed: true}, - intCol: {type: 'int', indexed: true}, - stringCol: {type: 'string', indexed: true}, - dateCol: {type: 'date', indexed: true}, + boolCol: {type: 'bool', indexed: true}, + intCol: {type: 'int', indexed: true}, + stringCol: {type: 'string', indexed: true}, + dateCol: {type: 'date', indexed: true}, + optBoolCol: {type: 'bool?', indexed: true}, + optIntCol: {type: 'int?', indexed: true}, + optStringCol: {type: 'string?', indexed: true}, + optDateCol: {type: 'date?', indexed: true}, } }; @@ -95,9 +151,31 @@ exports.IndexedTypes = { exports.LinkTypes = { name: 'LinkTypesObject', properties: { - objectCol: 'TestObject', + objectCol: 'TestObject', objectCol1: {type: 'object', objectType: 'TestObject'}, - arrayCol: {type: 'list', objectType: 'TestObject'}, + arrayCol: 'TestObject[]', + arrayCol1: {type: 'list', objectType: 'TestObject'}, + } +}; + +exports.PrimitiveArrays = { + name: 'PrimitiveArrays', + properties: { + bool: 'bool[]', + int: 'int[]', + float: 'float[]', + double: 'double[]', + string: 'string[]', + date: 'date[]', + data: 'data[]', + + optBool: 'bool?[]', + optInt: 'int?[]', + optFloat: 'float?[]', + optDouble: 'double?[]', + optString: 'string?[]', + optDate: 'date?[]', + optData: 'data?[]', } }; @@ -126,44 +204,19 @@ exports.StringOnly = { } }; -exports.AllTypes = { - name: 'AllTypesObject', - primaryKey: 'primaryCol', - properties: { - primaryCol: 'string', - boolCol: 'bool', - intCol: 'int', - floatCol: 'float', - doubleCol: 'double', - stringCol: 'string', - dateCol: 'date', - dataCol: 'data', - objectCol: 'TestObject', - arrayCol: {type: 'list', objectType: 'TestObject'}, - linkingObjectsCol: {type: 'linkingObjects', objectType: 'LinkToAllTypesObject', property: 'allTypesCol'}, - } -}; - -exports.LinkToAllTypes = { - name: 'LinkToAllTypesObject', - properties: { - allTypesCol: 'AllTypesObject', - } -} - exports.DefaultValues = { name: 'DefaultValuesObject', properties: { - boolCol: {type: 'bool', default: true}, - intCol: {type: 'int', default: -1}, - floatCol: {type: 'float', default: -1.1}, - doubleCol: {type: 'double', default: -1.11}, - stringCol: {type: 'string', default: 'defaultString'}, - dateCol: {type: 'date', default: new Date(1.111)}, - dataCol: {type: 'data', default: new ArrayBuffer(1)}, - objectCol: {type: 'TestObject', default: {doubleCol: 1}}, - nullObjectCol: {type: 'TestObject', default: null}, - arrayCol: {type: 'list', objectType: 'TestObject', default: [{doubleCol: 2}]}, + boolCol: {type: 'bool', default: true}, + intCol: {type: 'int', default: -1}, + floatCol: {type: 'float', default: -1.1}, + doubleCol: {type: 'double', default: -1.11}, + stringCol: {type: 'string', default: 'defaultString'}, + dateCol: {type: 'date', default: new Date(1.111)}, + dataCol: {type: 'data', default: new ArrayBuffer(1)}, + objectCol: {type: 'TestObject', default: {doubleCol: 1}}, + nullObjectCol: {type: 'TestObject', default: null}, + arrayCol: {type: 'TestObject[]', default: [{doubleCol: 2}]}, } }; @@ -203,7 +256,7 @@ exports.DateObject = { name: 'Date', properties: { currentDate: 'date', - nullDate: { type: 'date', optional: true } + nullDate: 'date?' } }; @@ -211,7 +264,7 @@ exports.LinkingObjectsObject = { name: 'LinkingObjectsObject', properties: { value: 'int', - links: {type: 'list', objectType: 'LinkingObjectsObject'}, + links: 'LinkingObjectsObject[]', linkingObjects: {type: 'linkingObjects', objectType: 'LinkingObjectsObject', property: 'links'} } }