realm
Version:
Realm by MongoDB is an offline-first mobile database: an alternative to SQLite and key-value stores
482 lines • 22.1 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.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