@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
JavaScript
"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