UNPKG

realm

Version:

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

482 lines 22.1 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.RealmObject = exports.KEY_SET = exports.KEY_ARRAY = exports.UpdateMode = void 0; const binding_1 = require("./binding"); const assert_1 = require("./assert"); const indirect_1 = require("./indirect"); const bson_1 = require("./bson"); const schema_1 = require("./schema"); const JSONCacheMap_1 = require("./JSONCacheMap"); const ObjectListeners_1 = require("./ObjectListeners"); const flags_1 = require("./flags"); const symbols_1 = require("./symbols"); const Results_1 = require("./collection-accessors/Results"); /** * The update mode to use when creating an object that already exists, * which is determined by a matching primary key. */ var UpdateMode; (function (UpdateMode) { /** * Objects are only created. If an existing object exists (determined by a * matching primary key), an exception is thrown. */ UpdateMode["Never"] = "never"; /** * If an existing object exists (determined by a matching primary key), only * properties where the value has actually changed will be updated. This improves * notifications and server side performance but also have implications for how * changes across devices are merged. For most use cases, the behavior will match * the intuitive behavior of how changes should be merged, but if updating an * entire object is considered an atomic operation, this mode should not be used. */ UpdateMode["Modified"] = "modified"; /** * If an existing object exists (determined by a matching primary key), all * properties provided will be updated, any other properties will remain unchanged. */ UpdateMode["All"] = "all"; })(UpdateMode = exports.UpdateMode || (exports.UpdateMode = {})); exports.KEY_ARRAY = Symbol("Object#keys"); exports.KEY_SET = Symbol("Object#keySet"); const INTERNAL_LISTENERS = Symbol("Object#listeners"); const DEFAULT_PROPERTY_DESCRIPTOR = { configurable: true, enumerable: true, writable: true }; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const PROXY_HANDLER = { ownKeys(target) { return Reflect.ownKeys(target).concat(target[exports.KEY_ARRAY]); }, getOwnPropertyDescriptor(target, prop) { if (typeof prop === "string" && target[exports.KEY_SET].has(prop)) { return DEFAULT_PROPERTY_DESCRIPTOR; } const result = Reflect.getOwnPropertyDescriptor(target, prop); if (result && typeof prop === "symbol") { if (prop === symbols_1.OBJECT_INTERNAL) { result.enumerable = false; result.writable = false; } else if (prop === INTERNAL_LISTENERS) { result.enumerable = false; } } return result; }, }; /** * Base class for a Realm Object. * @example * To define a class `Person` with required `name` and `age` * properties, define a `static schema`: * ``` * class Person extends Realm.Object<Person> { * _id!: Realm.BSON.ObjectId; * name!: string; * age!: number; * static schema: Realm.ObjectSchema = { * name: "Person", * primaryKey: "_id", * properties: { * _id: "objectId", * name: "string", * age: "int", * }, * }; * } * ``` * @example * If using the [@realm/babel-plugin](https://www.npmjs.com/package/@realm/babel-plugin): * To define a class `Person` with required `name` and `age` properties, they would * need to be specified in the type argument when it is being constructed to allow * Typescript-only model definitions: * ``` * class Person extends Realm.Object<Person, "name" | "age"> { * _id = new Realm.Types.ObjectId(); * name: Realm.Types.String; * age: Realm.Types.Int; * static primaryKey = "_id"; * } * ``` * @see {@link ObjectSchema} * @typeParam `T` - The type of this class (e.g. if your class is `Person`, * `T` should also be `Person` - this duplication is required due to how * TypeScript works) * @typeParam `RequiredProperties` - The names of any properties of this * class which are required when an instance is constructed with `new`. Any * properties not specified will be optional, and will default to a sensible * null value if no default is specified elsewhere. */ class RealmObject { /** * This property is stored on the per class prototype when transforming the schema. * @internal */ static [symbols_1.OBJECT_HELPERS]; static allowValuesArrays = false; /** * Optionally specify the primary key of the schema when using [@realm/babel-plugin](https://www.npmjs.com/package/@realm/babel-plugin). */ static primaryKey; /** * Optionally specify that the schema is an embedded schema when using [@realm/babel-plugin](https://www.npmjs.com/package/@realm/babel-plugin). */ static embedded; /** * Optionally specify that the schema should sync unidirectionally if using flexible sync when using [@realm/babel-plugin](https://www.npmjs.com/package/@realm/babel-plugin). */ static asymmetric; /** * Create an object in the database and set values on it * @internal */ static create(realm, values, mode, context) { assert_1.assert.inTransaction(realm); if (Array.isArray(values)) { if (flags_1.flags.ALLOW_VALUES_ARRAYS) { const { persistedProperties } = context.helpers.objectSchema; return RealmObject.create(realm, Object.fromEntries(values.map((value, index) => { const property = persistedProperties[index]; const propertyName = property.publicName || property.name; return [propertyName, value]; })), mode, context); } else { throw new Error("Array values on object creation is no longer supported"); } } const { helpers: { properties, wrapObject, objectSchema: { persistedProperties }, }, createObj, } = context; // Create the underlying object const [obj, created] = createObj ? createObj() : this.createObj(realm, values, mode, context); const result = wrapObject(obj); (0, assert_1.assert)(result); // Persist any values provided for (const property of persistedProperties) { const propertyName = property.publicName || property.name; const { default: defaultValue, get: getProperty, set: setProperty } = properties.get(propertyName); if (property.isPrimary) { continue; // Skip setting this, as we already provided it on object creation } const propertyValue = values[propertyName]; if (typeof propertyValue !== "undefined") { if (mode !== UpdateMode.Modified || getProperty(obj) !== propertyValue) { // Calling `set`/`setProperty` (or `result[propertyName] = propertyValue`) // will call into the property setter in PropertyHelpers.ts. // (E.g. the setter for [binding.PropertyType.Array] in the case of lists.) setProperty(obj, propertyValue, created); } } else { // If the user has omitted a value for the `property`, and the underlying // object was just created, set the `property` to the default if provided. if (created) { if (typeof defaultValue !== "undefined") { const extractedValue = typeof defaultValue === "function" ? defaultValue() : defaultValue; setProperty(obj, extractedValue, created); } else if (!(property.type & 896 /* binding.PropertyType.Collection */) && !(property.type & 64 /* binding.PropertyType.Nullable */)) { throw new Error(`Missing value for property '${propertyName}'`); } } } } return result; } /** * Create an object in the database and populate its primary key value, if required * @internal */ static createObj(realm, values, mode, context) { const { helpers: { objectSchema: { name, tableKey, primaryKey }, properties, }, } = context; // Create the underlying object const table = binding_1.binding.Helpers.getTable(realm.internal, tableKey); if (primaryKey) { const primaryKeyHelpers = properties.get(primaryKey); let primaryKeyValue = values[primaryKey]; // If the value for the primary key was not set, use the default value if (primaryKeyValue === undefined) { const defaultValue = primaryKeyHelpers.default; primaryKeyValue = typeof defaultValue === "function" ? defaultValue() : defaultValue; } const pk = primaryKeyHelpers.toBinding( // Fallback to default value if the provided value is undefined or null typeof primaryKeyValue !== "undefined" && primaryKeyValue !== null ? primaryKeyValue : primaryKeyHelpers.default); const result = binding_1.binding.Helpers.getOrCreateObjectWithPrimaryKey(table, pk); const [, created] = result; if (mode === UpdateMode.Never && !created) { throw new Error(`Attempting to create an object of type '${name}' with an existing primary key value '${primaryKeyValue}'.`); } return result; } else { return [table.createObject(), true]; } } /** * Create a wrapper for accessing an object from the database * @internal */ static createWrapper(internal, constructor) { const result = Object.create(constructor.prototype); result[symbols_1.OBJECT_INTERNAL] = internal; // Initializing INTERNAL_LISTENERS here rather than letting it just be implicitly undefined since JS engines // prefer adding all fields to objects upfront. Adding optional fields later can sometimes trigger deoptimizations. result[INTERNAL_LISTENERS] = null; // Wrap in a proxy to trap keys, enabling the spread operator, and hiding our internal fields. return new Proxy(result, PROXY_HANDLER); } /** * Create a `RealmObject` wrapping an `Obj` from the binding. * @param realm - The Realm managing the object. * @param values - The values of the object's properties at creation. */ constructor(realm, values) { return realm.create(this.constructor, values); } /** * @returns An array of the names of the object's properties. * @deprecated Please use {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys | Object.keys()} */ keys() { // copying to prevent caller from modifying the static array. return [...this[exports.KEY_ARRAY]]; } /** * @returns An array of key/value pairs of the object's properties. * @deprecated Please use {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries | Object.entries()} */ entries() { return Object.entries(this); } /** @internal */ toJSON(_, cache = new JSONCacheMap_1.JSONCacheMap()) { // Construct a reference-id of table-name & primaryKey if it exists, or fall back to objectId. // Check if current objectId has already processed, to keep object references the same. const existing = cache.find(this); if (existing) { return existing; } const result = {}; cache.add(this, result); // Move all enumerable keys to result, triggering any specific toJSON implementation in the process. for (const key in this) { const value = this[key]; if (typeof value == "function") { continue; } if (value instanceof indirect_1.indirect.Object || value instanceof indirect_1.indirect.OrderedCollection || value instanceof indirect_1.indirect.Dictionary) { // recursively trigger `toJSON` for Realm instances with the same cache. result[key] = value.toJSON(key, cache); } else { // Other cases, including null and undefined. result[key] = value; } } return result; } /** * Checks if this object has not been deleted and is part of a valid Realm. * @returns `true` if the object can be safely accessed, `false` if not. */ isValid() { return this[symbols_1.OBJECT_INTERNAL] && this[symbols_1.OBJECT_INTERNAL].isValid; } /** * The schema for the type this object belongs to. * @returns The {@link CanonicalObjectSchema} that describes this object. */ objectSchema() { return this[symbols_1.OBJECT_REALM].getClassHelpers(this).canonicalObjectSchema; } linkingObjects(objectType, propertyName) { const realm = this[symbols_1.OBJECT_REALM]; const targetClassHelpers = realm.getClassHelpers(objectType); const { objectSchema: targetObjectSchema, properties, wrapObject } = targetClassHelpers; const targetProperty = properties.get(propertyName); const originObjectSchema = this.objectSchema(); (0, assert_1.assert)(originObjectSchema.name === targetProperty.objectType, () => `'${targetObjectSchema.name}#${propertyName}' is not a relationship to '${originObjectSchema.name}'`); const typeHelpers = { // See `[binding.PropertyType.LinkingObjects]` in `TypeHelpers.ts`. toBinding(value) { return value; }, fromBinding(value) { assert_1.assert.instanceOf(value, binding_1.binding.Obj); return wrapObject(value); }, }; const accessor = (0, Results_1.createResultsAccessor)({ realm, typeHelpers, itemType: 7 /* binding.PropertyType.Object */ }); // Create the Result for the backlink view. const tableRef = binding_1.binding.Helpers.getTable(realm.internal, targetObjectSchema.tableKey); const tableView = this[symbols_1.OBJECT_INTERNAL].getBacklinkView(tableRef, targetProperty.columnKey); const results = binding_1.binding.Results.fromTableView(realm.internal, tableView); return new indirect_1.indirect.Results(realm, results, accessor, typeHelpers); } /** * Returns the total count of incoming links to this object * @returns The number of links to this object. */ linkingObjectsCount() { return this[symbols_1.OBJECT_INTERNAL].getBacklinkCount(); } /** * @deprecated * TODO: Remove completely once the type tests are abandoned. */ _objectId() { throw new Error("This is now removed!"); } /** * A string uniquely identifying the object across all objects of the same type. */ _objectKey() { return this[symbols_1.OBJECT_INTERNAL].key.toString(); } /** * Add a listener `callback` which will be called when a **live** object instance changes. * @param callback - A function to be called when changes occur. * @param keyPaths - Indicates a lower bound on the changes relevant for the listener. This is a lower bound, since if multiple listeners are added (each with their own `keyPaths`) the union of these key-paths will determine the changes that are considered relevant for all listeners registered on the object. In other words: A listener might fire more than the key-paths specify, if other listeners with different key-paths are present. * @throws A {@link TypeAssertionError} if `callback` is not a function. * @note * Adding the listener is an asynchronous operation, so the callback is invoked the first time to notify the caller when the listener has been added. * Thus, when the callback is invoked the first time it will contain an empty array for {@link Realm.ObjectChangeSet.changedProperties | changes.changedProperties}. * * Unlike {@link Collection.addListener}, changes on properties containing other objects (both standalone and embedded) will not trigger this listener. * @example * wine.addListener((obj, changes) => { * // obj === wine * console.log(`object is deleted: ${changes.deleted}`); * console.log(`${changes.changedProperties.length} properties have been changed:`); * changes.changedProperties.forEach(prop => { * console.log(` ${prop}`); * }); * }) * @example * wine.addListener((obj, changes) => { * console.log("The wine got deleted or its brand might have changed"); * }, ["brand"]) */ addListener(callback, keyPaths) { assert_1.assert.function(callback); if (!this[INTERNAL_LISTENERS]) { this[INTERNAL_LISTENERS] = new ObjectListeners_1.ObjectListeners(this[symbols_1.OBJECT_REALM].internal, this); } this[INTERNAL_LISTENERS].addListener(callback, typeof keyPaths === "string" ? [keyPaths] : keyPaths); } /** * Remove the listener `callback` from this object. * @throws A {@link TypeAssertionError} if `callback` is not a function. * @param callback A function previously added as listener */ removeListener(callback) { assert_1.assert.function(callback); // Note: if the INTERNAL_LISTENERS field hasn't been initialized, then we have no listeners to remove. this[INTERNAL_LISTENERS]?.removeListener(callback); } /** * Remove all listeners from this object. */ removeAllListeners() { // Note: if the INTERNAL_LISTENERS field hasn't been initialized, then we have no listeners to remove. this[INTERNAL_LISTENERS]?.removeAllListeners(); } /** * Get underlying type of a property value. * @param propertyName - The name of the property to retrieve the type of. * @throws An {@link Error} if property does not exist. * @returns Underlying type of the property value. */ getPropertyType(propertyName) { const { properties } = this[symbols_1.OBJECT_REALM].getClassHelpers(this); const { type, objectType, columnKey } = properties.get(propertyName); const typeName = (0, schema_1.getTypeName)(type, objectType); if (typeName === "mixed") { // This requires actually getting the object and inferring its type const value = this[symbols_1.OBJECT_INTERNAL].getAny(columnKey); if (value === null) { return "null"; } else if (binding_1.binding.Int64.isInt(value)) { return "int"; } else if (value instanceof binding_1.binding.Float) { return "float"; } else if (value instanceof binding_1.binding.Timestamp) { return "date"; } else if (value instanceof binding_1.binding.Obj) { const { objectSchema } = this[symbols_1.OBJECT_REALM].getClassHelpers(value.table.key); return `<${objectSchema.name}>`; } else if (value instanceof binding_1.binding.ObjLink) { const { objectSchema } = this[symbols_1.OBJECT_REALM].getClassHelpers(value.tableKey); return `<${objectSchema.name}>`; } else if (value instanceof ArrayBuffer) { return "data"; } else if (typeof value === "number") { return "double"; } else if (typeof value === "string") { return "string"; } else if (typeof value === "boolean") { return "bool"; } else if (value instanceof bson_1.BSON.ObjectId) { return "objectId"; } else if (value instanceof bson_1.BSON.Decimal128) { return "decimal128"; } else if (value instanceof bson_1.BSON.UUID) { return "uuid"; } else if (value === binding_1.binding.ListSentinel) { return "list"; } else if (value === binding_1.binding.DictionarySentinel) { return "dictionary"; } else if (typeof value === "symbol") { throw new Error(`Unexpected Symbol: ${value.toString()}`); } else { assert_1.assert.never(value, "value"); } } else { return typeName; } } } exports.RealmObject = RealmObject; // We like to refer to this as "Realm.Object" // TODO: Determine if we want to revisit this if we're going away from a namespaced API Object.defineProperty(RealmObject, "name", { value: "Realm.Object" }); (0, indirect_1.injectIndirect)("Object", RealmObject); //# sourceMappingURL=Object.js.map