UNPKG

@react-native-ohos/realm

Version:

Realm by MongoDB is an offline-first mobile database: an alternative to SQLite and key-value stores

637 lines 30.2 kB
"use strict"; //////////////////////////////////////////////////////////////////////////// // // Copyright 2022 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////// Object.defineProperty(exports, "__esModule", { value: true }); exports.OrderedCollection = void 0; const binding_1 = require("./binding"); const assert_1 = require("./assert"); const errors_1 = require("./errors"); const indirect_1 = require("./indirect"); const Collection_1 = require("./Collection"); const JSONCacheMap_1 = require("./JSONCacheMap"); const Object_1 = require("./Object"); const TypeHelpers_1 = require("./TypeHelpers"); const schema_1 = require("./schema"); const ranges_1 = require("./ranges"); const Mixed_1 = require("./type-helpers/Mixed"); const symbols_1 = require("./symbols"); const Results_1 = require("./collection-accessors/Results"); const INDEX_NOT_FOUND = -1; const DEFAULT_COLUMN_KEY = binding_1.binding.Int64.numToInt(0); const DEFAULT_PROPERTY_DESCRIPTOR = { configurable: true, enumerable: true, writable: true }; const PROXY_HANDLER = { // TODO: Consider executing the `parseInt` first to optimize for index access over accessing a member on the list get(target, prop) { if (Reflect.has(target, prop)) { return Reflect.get(target, prop); } else if (typeof prop === "string") { const index = Number.parseInt(prop, 10); // TODO: Consider catching an error from access out of bounds, instead of checking the length, to optimize for the hot path if (!Number.isNaN(index) && index >= 0 && index < target.length) { return target.get(index); } } }, set(target, prop, value, receiver) { if (typeof prop === "string") { const index = Number.parseInt(prop, 10); if (Number.isInteger(index)) { // Optimize for the hot-path by catching a potential out of bounds access from Core, rather // than checking the length upfront. Thus, our List differs from the behavior of a JS array. try { target.set(index, value); } catch (err) { // Let the custom errors from Results take precedence over out of bounds errors. This will // let users know that they cannot modify Results, rather than erroring on incorrect index. if (index < 0 && !(target instanceof indirect_1.indirect.Results)) { throw new Error(`Cannot set item at negative index ${index}.`); } throw err; } return true; } } return Reflect.set(target, prop, value, receiver); }, ownKeys(target) { return Reflect.ownKeys(target).concat([...target.keys()].map(String)); }, getOwnPropertyDescriptor(target, prop) { if (Reflect.has(target, prop)) { return Reflect.getOwnPropertyDescriptor(target, prop); } else if (typeof prop === "string") { const index = Number.parseInt(prop, 10); if (index < target.length) { return DEFAULT_PROPERTY_DESCRIPTOR; } } }, }; /** * An {@link OrderedCollection} is a homogenous sequence of values of any of the types * that can be stored as properties of Realm objects. It can be * accessed in any of the ways that a normal JavaScript Array can, including * subscripting, enumerating with `for-of` and so on. * @see {@link https://mdn.io/Array | Array} */ class OrderedCollection extends Collection_1.Collection { /** @internal */ constructor(realm, results, accessor, typeHelpers) { if (arguments.length === 0) { throw new errors_1.IllegalConstructorError("OrderedCollection"); } super(accessor, typeHelpers, (callback, keyPaths) => { return results.addNotificationCallback((changes) => { try { callback(proxied, { deletions: (0, ranges_1.unwind)(changes.deletions), insertions: (0, ranges_1.unwind)(changes.insertions), oldModifications: (0, ranges_1.unwind)(changes.modifications), newModifications: (0, ranges_1.unwind)(changes.modificationsNew), }); } catch (err) { // Scheduling a throw on the event loop, // since throwing synchronously here would result in an abort in the calling C++ setImmediate(() => { throw err; }); } }, keyPaths ? this.mapKeyPaths(keyPaths) : keyPaths); }); // Wrap in a proxy to trap ownKeys and get, enabling the spread operator const proxied = new Proxy(this, PROXY_HANDLER); // Get the class helpers for later use, if available const { objectType } = results; const classHelpers = typeof objectType === "string" && objectType !== "" ? realm.getClassHelpers(objectType) : null; // Make the internal properties non-enumerable Object.defineProperty(this, "realm", { enumerable: false, configurable: false, writable: false, value: realm, }); Object.defineProperty(this, "results", { enumerable: false, configurable: false, writable: false, value: results, }); Object.defineProperty(this, "classHelpers", { enumerable: false, configurable: false, writable: false, value: classHelpers, }); Object.defineProperty(this, "mixedToBinding", { enumerable: false, configurable: false, writable: false, value: Mixed_1.mixedToBinding.bind(undefined, realm.internal), }); // See https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype-@@unscopables Object.defineProperty(this, Symbol.unscopables, { enumerable: false, configurable: true, writable: false, }); return proxied; } /** @internal */ toJSON(_, cache = new JSONCacheMap_1.JSONCacheMap()) { return this.map((item, index) => { if (item instanceof Object_1.RealmObject) { return item.toJSON(index.toString(), cache); } else { return item; } }); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/keys | Array.prototype.keys()} * @returns An iterator with all keys in the collection. */ *keys() { const size = this.results.size(); for (let i = 0; i < size; i++) { yield i; } } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values} | Array.prototype.values()} * @returns An iterator with all values in the collection. */ *values() { const snapshot = this.snapshot(); for (const i of this.keys()) { yield snapshot[i]; } } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/entries | Array.prototype.entries()} * @returns An iterator with all key/value pairs in the collection. */ *entries() { const snapshot = this.snapshot(); const size = snapshot.length; for (let i = 0; i < size; i++) { yield [i, snapshot[i]]; } } /** * @returns The number of values. */ get length() { return this.results.size(); } /** * @throws An {@link Error} as the length property cannot be assigned. */ set length(value) { throw new Error("Cannot assign to read only property 'length'"); } /** * Name of the type of items. * @returns The name of the type of values. */ get type() { return (0, schema_1.getTypeName)((0, TypeHelpers_1.toItemType)(this.results.type), undefined); } /** * Whether `null` is a valid value for the collection. * @returns Whether `null` is a valid value for the collection. * @readonly */ get optional() { return !!(this.results.type & 64 /* binding.PropertyType.Nullable */); } /* eslint-disable @typescript-eslint/no-explicit-any -- We've copied these from the lib types */ /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString | Array.prototype.toString()} * @returns A string representation of the collection. */ toString() { return [...this].toString(); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toLocaleString | Array.prototype.toLocaleString()} * @returns A localized string representation of the collection. */ toLocaleString() { return [...this].toLocaleString(); } concat(...items) { return [...this].concat(...items); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join | Array.prototype.join()} * @params separator - A string used to separate one element of the collection from the next in the resulting String. * @returns A string representing the elements of the collection. */ join(separator) { return [...this].join(separator); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice | Array.prototype.slice()} * @params start - Zero-based index at which to begin extraction. * @params end - Zero-based index at which to end extraction. It extracts up to but not including `end`. * @returns A new array containing the elements between the start and end indices. */ slice(start, end) { return [...this].slice(start, end); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf | Array.prototype.indexOf()} * @params searchElement - Element to locate in the collection. * @params fromIndex - The collection index at which to begin the search. If omitted, the search starts at index 0. * @note `fromIndex` is currently not supported. So all searches start at index 0. * @returns The first index at which a given element can be found in the collection, or -1 if it is not present. */ indexOf(searchElement, fromIndex) { (0, assert_1.assert)(typeof fromIndex === "undefined", "The second fromIndex argument is not yet supported"); if (this.type === "object") { assert_1.assert.instanceOf(searchElement, Object_1.RealmObject); return this.results.indexOfObj(searchElement[symbols_1.OBJECT_INTERNAL]); } else { try { return this.results.indexOf(this[Collection_1.COLLECTION_TYPE_HELPERS].toBinding(searchElement)); } catch { // Inability to convert to the binding representation means we won't be able to find it. return INDEX_NOT_FOUND; } } } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf | Array.prototype.lastIndexOf()} * @params searchElement - Element to locate in the collection. * @params fromIndex - The collection index at which to begin the search. If omitted, the search starts at the last index. * @returns The last index at which a given element can be found in the collection, or -1 if it is not present. The collection is searched backwards, starting at `fromIndex`. */ lastIndexOf(searchElement, fromIndex) { return [...this].lastIndexOf(searchElement, fromIndex); } every(predicate, thisArg) { return [...this].every(predicate, thisArg); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some | Array.prototype.some()} * @params predicate - A function to test for each element. * @params predicate.value - The current element being processed in the collection. * @params predicate.index - The index of the current element being processed in the collection. * @params predicate.array - The collection `every` was called upon. * @params thisArg - An object to which the `this` keyword can refer in the predicate function. If `thisArg` is omitted, `undefined` is used as the `this` value. * @returns `true` if the callback function returns a truthy value for any collection element; otherwise, `false`. */ some(predicate, thisArg) { return [...this].some(predicate, thisArg); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach | Array.prototype.forEach()} * @params callbackfn - A function that accepts up to three arguments. `forEach` calls the callbackfn function one time for each element in the collection. * @params callbackfn.value - The current element being processed in the collection. * @params callbackfn.index - The index of the current element being processed in the collection. * @params callbackfn.array - The collection `forEach` was called upon. * @params thisArg - An object to which the `this` keyword can refer in the `callbackfn` function. If `thisArg` is omitted, `undefined` is used as the `this` value. */ forEach(callbackfn, thisArg) { return [...this].forEach(callbackfn, thisArg); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map | Array.prototype.map()} * @params callbackfn - A function that accepts up to three arguments. The `map` method calls the `callbackfn` function one time for each element in the collection. * @params callbackfn.value - The current element being processed in the collection. * @params callbackfn.index - The index of the current element being processed in the collection. * @params callbackfn.array - The collection `map` was called upon. * @params thisArg - An object to which the `this` keyword can refer in the `callbackfn` function. If `thisArg` is omitted, `undefined` is used as the `this` value. * @returns A new array containing the results of calling the `callbackfn` function on each element in the collection. */ map(callbackfn, thisArg) { return [...this].map(callbackfn, thisArg); } filter(predicate, thisArg) { return [...this].filter(predicate, thisArg); } reduce(callbackfn, initialValue) { return [...this].reduce(callbackfn, initialValue); } reduceRight(callbackfn, initialValue) { return [...this].reduceRight(callbackfn, initialValue); } find(predicate, thisArg) { return [...this].find(predicate, thisArg); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex | Array.prototype.findIndex()} * @params predicate - A function that accepts up to three arguments. The `findIndex` method calls the `predicate` function one time for each element in the collection. * @params predicate.value - The value of the element. * @params predicate.index - The index of the element. * @params predicate.obj - The object being traversed. * @params thisArg - An object to which the `this` keyword can refer in the `predicate` function. If `thisArg` is omitted, `undefined` is used as the `this` value. * @returns The index of the first element in the array that satisfies the provided testing function. Otherwise, -1 is returned. */ findIndex(predicate, thisArg) { return [...this].findIndex(predicate, thisArg); } // TODO: Implement support for RealmObjects, by comparing their #objectKey values /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes | Array.prototype.includes()} * @params searchElement - The element to search for. * @params fromIndex - The position in this array at which to begin searching for `searchElement`. A negative value searches from the index of array.length + fromIndex by asc. * @note `fromIndex` is currently not supported. So all searches start at index 0. * @returns `true` if the `searchElement` is found in the array; otherwise, `false`. */ includes(searchElement, fromIndex) { return this.indexOf(searchElement, fromIndex) !== -1; } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap | Array.prototype.flatMap()} * @params callback - Function that produces an element of the new Array, taking three arguments: * @params callback.currentValue - The current element being processed in the array. * @params callback.index - The index of the current element being processed in the array. * @params callback.array - The array `flatMap` was called upon. * @params thisArg - Value to use as this when executing callback. * @returns A new array with each element being the result of the callback function and flattened to a depth of 1. */ flatMap(callback, thisArg) { return [...this].flatMap(callback, thisArg); } flat() { throw new Error("Method not implemented."); } /** * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at | Array.prototype.at()} * @params index - The index of the element to return from the array. If the index is a negative number, the element at `array.length + index` is returned. * @returns The element at the given index in the array; `undefined` if there is no element at the given index. */ at(index) { return [...this].at(index); } /* eslint-enable @typescript-eslint/no-explicit-any */ /** * @returns An iterator that iterates over all the values in the collection. */ [Symbol.iterator]() { return this.values(); } /** * An Object whose truthy properties are properties that are excluded from the 'with' * environment bindings of the associated objects. */ [Symbol.unscopables] = Array.prototype[Symbol.unscopables]; // Other methods // TODO: Implement this method /** * @returns A string describing the filters applied to this collection. */ description() { throw new Error("Method not implemented."); } /** * Checks if this collection is empty. * @returns `true` if the collection is empty, `false` if not. */ isEmpty() { return this.results.size() === 0; } /** * Returns the minimum value of the values in the collection or of the * given property among all the objects in the collection, or `undefined` * if the collection is empty. * * Only supported for int, float, double and date properties. `null` values * are ignored entirely by this method and will not be returned. * @param property - For a collection of objects, the property to take the minimum of. * @throws A {@link TypeAssertionError} if no property with the name exists or if property is not numeric/date. * @returns The minimum value. */ min(property) { const columnKey = this.getPropertyColumnKey(property); const result = this.results.min(columnKey); if (result instanceof Date || typeof result === "number" || typeof result === "undefined") { return result; } else if (binding_1.binding.Int64.isInt(result)) { return binding_1.binding.Int64.intToNum(result); } else if (result instanceof binding_1.binding.Float) { return result.value; } else if (result instanceof binding_1.binding.Timestamp) { return result.toDate(); } else { throw new errors_1.TypeAssertionError("Timestamp, number, bigint, Float or null", result, "result"); } } /** * Returns the maximum value of the values in the collection or of the * given property among all the objects in the collection, or `undefined` * if the collection is empty. * * Only supported for int, float, double and date properties. `null` values * are ignored entirely by this method and will not be returned. * @param property - For a collection of objects, the property to take the maximum of. * @throws An {@link Error} if no property with the name exists or if property is not numeric/date. * @returns The maximum value. */ max(property) { const columnKey = this.getPropertyColumnKey(property); const result = this.results.max(columnKey); if (result instanceof Date || typeof result === "number" || typeof result === "undefined") { return result; } else if (binding_1.binding.Int64.isInt(result)) { return binding_1.binding.Int64.intToNum(result); } else if (result instanceof binding_1.binding.Float) { return result.value; } else if (result instanceof binding_1.binding.Timestamp) { return result.toDate(); } else { throw new errors_1.TypeAssertionError("Timestamp, number, bigint, Float or undefined", result, "result"); } } /** * Computes the sum of the values in the collection or of the given * property among all the objects in the collection, or 0 if the collection * is empty. * * Only supported for int, float and double properties. `null` values are * ignored entirely by this method. * @param property - For a collection of objects, the property to take the sum of. * @throws An {@link Error} if no property with the name exists or if property is not numeric. * @returns The sum. */ sum(property) { const columnKey = this.getPropertyColumnKey(property); const result = this.results.sum(columnKey); if (typeof result === "number") { return result; } else if (binding_1.binding.Int64.isInt(result)) { return binding_1.binding.Int64.intToNum(result); } else if (result instanceof binding_1.binding.Float) { return result.value; } else { throw new errors_1.TypeAssertionError("number, bigint or Float", result, "result"); } } /** * Computes the average of the values in the collection or of the given * property among all the objects in the collection, or `undefined` if the collection * is empty. * * Only supported for int, float and double properties. `null` values are * ignored entirely by this method and will not be factored into the average. * @param property - For a collection of objects, the property to take the average of. * @throws An {@link Error} if no property with the name exists or if property is not numeric. * @returns The sum. */ avg(property) { const columnKey = this.getPropertyColumnKey(property); const result = this.results.average(columnKey); if (typeof result === "number" || typeof result === "undefined") { return result; } else if (binding_1.binding.Int64.isInt(result)) { return binding_1.binding.Int64.intToNum(result); } else if (result instanceof binding_1.binding.Float) { return result.value; } else { throw new errors_1.TypeAssertionError("number, Float, bigint or undefined", result, "result"); } } /** * Returns new {@link Results} that represent this collection being filtered by the provided query. * @param queryString - Query used to filter objects from the collection. * @param args - Each subsequent argument is used by the placeholders * (e.g. `$0`, `$1`, `$2`, …) in the query. * @throws An {@link Error} if the query or any other argument passed into this method is invalid. * @returns Results filtered according to the provided query. * @note This is currently only supported for collections of Realm Objects. * @example * let merlots = wines.filtered('variety == "Merlot" && vintage <= $0', maxYear); */ filtered(queryString, ...args) { const { results: parent, realm } = this; const kpMapping = binding_1.binding.Helpers.getKeypathMapping(realm.internal); const bindingArgs = args.map((arg) => this.queryArgToBinding(arg)); const newQuery = parent.query.table.query(queryString, bindingArgs, kpMapping); const results = binding_1.binding.Helpers.resultsAppendQuery(parent, newQuery); const itemType = (0, TypeHelpers_1.toItemType)(results.type); const typeHelpers = this[Collection_1.COLLECTION_TYPE_HELPERS]; const accessor = (0, Results_1.createResultsAccessor)({ realm, typeHelpers, itemType }); return new indirect_1.indirect.Results(realm, results, accessor, typeHelpers); } /** @internal */ queryArgToBinding(arg) { return Array.isArray(arg) ? arg.map((innerArg) => this.mixedToBinding(innerArg, { isQueryArg: true })) : this.mixedToBinding(arg, { isQueryArg: true }); } sorted(arg0 = "self", arg1) { if (Array.isArray(arg0)) { assert_1.assert.undefined(arg1, "second 'argument'"); const { results: parent, realm } = this; // Map optional "reversed" to "ascending" (expected by the binding) const descriptors = arg0.map((arg, i) => { if (typeof arg === "string") { return [arg, true]; } else if (Array.isArray(arg)) { const [property, direction] = arg; assert_1.assert.string(property, "property"); assert_1.assert.boolean(direction, "direction"); return [property, !direction]; } else { throw new errors_1.TypeAssertionError("string or array with two elements [string, boolean]", arg, `descriptor[${i}]`); } }); // TODO: Call `parent.sort`, avoiding property name to column key conversion to speed up performance here. const results = parent.sortByNames(descriptors); const itemType = (0, TypeHelpers_1.toItemType)(results.type); const typeHelpers = this[Collection_1.COLLECTION_TYPE_HELPERS]; const accessor = (0, Results_1.createResultsAccessor)({ realm, typeHelpers, itemType }); return new indirect_1.indirect.Results(realm, results, accessor, typeHelpers); } else if (typeof arg0 === "string") { return this.sorted([[arg0, arg1 === true]]); } else if (typeof arg0 === "boolean") { return this.sorted([["self", arg0]]); } else { throw new errors_1.TypeAssertionError("property name and optional bool or an array of descriptors", arg0, "argument"); } } /** * 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 Results which will **not** live update. */ snapshot() { const { realm, internal } = this; const snapshot = internal.snapshot(); const itemType = (0, TypeHelpers_1.toItemType)(snapshot.type); const typeHelpers = this[Collection_1.COLLECTION_TYPE_HELPERS]; const accessor = (0, Results_1.createResultsAccessor)({ realm, typeHelpers, itemType }); return new indirect_1.indirect.Results(realm, snapshot, accessor, typeHelpers); } /** @internal */ getPropertyColumnKey(name) { if (this.classHelpers) { assert_1.assert.string(name, "name"); return this.classHelpers.properties.get(name).columnKey; } else if (name) { throw new Error(`Cannot get property named '${name}' on a list of primitives`); } else { return DEFAULT_COLUMN_KEY; } } /** @internal */ mapKeyPaths(keyPaths) { return this.realm.internal.createKeyPathArray(this.results.objectType, keyPaths); } } exports.OrderedCollection = OrderedCollection; (0, indirect_1.injectIndirect)("OrderedCollection", OrderedCollection); //# sourceMappingURL=OrderedCollection.js.map