UNPKG

acebase-core

Version:

Shared AceBase core components, no need to install manually

321 lines 13.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeMappings = void 0; const utils_1 = require("./utils"); const path_info_1 = require("./path-info"); const data_reference_1 = require("./data-reference"); const data_snapshot_1 = require("./data-snapshot"); /** * (for internal use) - gets the mapping set for a specific path */ function get(mappings, path) { // path points to the mapped (object container) location path = path.replace(/^\/|\/$/g, ''); // trim slashes const keys = path_info_1.PathInfo.getPathKeys(path); const mappedPath = Object.keys(mappings).find(mpath => { const mkeys = path_info_1.PathInfo.getPathKeys(mpath); if (mkeys.length !== keys.length) { return false; // Can't be a match } return mkeys.every((mkey, index) => { if (mkey === '*' || (typeof mkey === 'string' && mkey[0] === '$')) { return true; // wildcard } return mkey === keys[index]; }); }); const mapping = mappings[mappedPath]; return mapping; } /** * (for internal use) - gets the mapping set for a specific path's parent */ function map(mappings, path) { // path points to the object location, its parent should have the mapping const targetPath = path_info_1.PathInfo.get(path).parentPath; if (targetPath === null) { return; } return get(mappings, targetPath); } /** * (for internal use) - gets all mappings set for a specific path and all subnodes * @returns returns array of all matched mappings in path */ function mapDeep(mappings, entryPath) { // returns mapping for this node, and all mappings for nested nodes // entryPath: "users/ewout" // mappingPath: "users" // mappingPath: "users/*/posts" entryPath = entryPath.replace(/^\/|\/$/g, ''); // trim slashes // Start with current path's parent node const pathInfo = path_info_1.PathInfo.get(entryPath); const startPath = pathInfo.parentPath; const keys = startPath ? path_info_1.PathInfo.getPathKeys(startPath) : []; // Every path that starts with startPath, is a match // TODO: refactor to return Object.keys(mappings),filter(...) const matches = Object.keys(mappings).reduce((m, mpath) => { //const mkeys = mpath.length > 0 ? mpath.split("/") : []; const mkeys = path_info_1.PathInfo.getPathKeys(mpath); if (mkeys.length < keys.length) { return m; // Can't be a match } let isMatch = true; if (keys.length === 0 && startPath !== null) { // Only match first node's children if mapping pattern is "*" or "$variable" isMatch = mkeys.length === 1 && (mkeys[0] === '*' || (typeof mkeys[0] === 'string' && mkeys[0][0] === '$')); } else { mkeys.every((mkey, index) => { if (index >= keys.length) { return false; // stop .every loop } else if ((mkey === '*' || (typeof mkey === 'string' && mkey[0] === '$')) || mkey === keys[index]) { return true; // continue .every loop } else { isMatch = false; return false; // stop .every loop } }); } if (isMatch) { const mapping = mappings[mpath]; m.push({ path: mpath, type: mapping }); } return m; }, []); return matches; } /** * (for internal use) - serializes or deserializes an object using type mappings * @returns returns the (de)serialized value */ function process(db, mappings, path, obj, action) { if (obj === null || typeof obj !== 'object') { return obj; } const keys = path_info_1.PathInfo.getPathKeys(path); // path.length > 0 ? path.split("/") : []; const m = mapDeep(mappings, path); const changes = []; m.sort((a, b) => path_info_1.PathInfo.getPathKeys(a.path).length > path_info_1.PathInfo.getPathKeys(b.path).length ? -1 : 1); // Deepest paths first m.forEach(mapping => { const mkeys = path_info_1.PathInfo.getPathKeys(mapping.path); //mapping.path.length > 0 ? mapping.path.split("/") : []; mkeys.push('*'); const mTrailKeys = mkeys.slice(keys.length); if (mTrailKeys.length === 0) { const vars = path_info_1.PathInfo.extractVariables(mapping.path, path); const ref = new data_reference_1.DataReference(db, path, vars); if (action === 'serialize') { // serialize this object obj = mapping.type.serialize(obj, ref); } else if (action === 'deserialize') { // deserialize this object const snap = new data_snapshot_1.DataSnapshot(ref, obj); obj = mapping.type.deserialize(snap); } return; } // Find all nested objects at this trail path const process = (parentPath, parent, keys) => { if (obj === null || typeof obj !== 'object') { return obj; } const key = keys[0]; let children = []; if (key === '*' || (typeof key === 'string' && key[0] === '$')) { // Include all children if (parent instanceof Array) { children = parent.map((val, index) => ({ key: index, val })); } else { children = Object.keys(parent).map(k => ({ key: k, val: parent[k] })); } } else { // Get the 1 child const child = parent[key]; if (typeof child === 'object') { children.push({ key, val: child }); } } children.forEach(child => { const childPath = path_info_1.PathInfo.getChildPath(parentPath, child.key); const vars = path_info_1.PathInfo.extractVariables(mapping.path, childPath); const ref = new data_reference_1.DataReference(db, childPath, vars); if (keys.length === 1) { // TODO: this alters the existing object, we must build our own copy! if (action === 'serialize') { // serialize this object changes.push({ parent, key: child.key, original: parent[child.key] }); parent[child.key] = mapping.type.serialize(child.val, ref); } else if (action === 'deserialize') { // deserialize this object const snap = new data_snapshot_1.DataSnapshot(ref, child.val); parent[child.key] = mapping.type.deserialize(snap); } } else { // Dig deeper process(childPath, child.val, keys.slice(1)); } }); }; process(path, obj, mTrailKeys); }); if (action === 'serialize') { // Clone this serialized object so any types that remained // will become plain objects without functions, and we can restore // the original object's values if any mappings were processed. // This will also prevent circular references obj = (0, utils_1.cloneObject)(obj); if (changes.length > 0) { // Restore the changes made to the original object changes.forEach(change => { change.parent[change.key] = change.original; }); } } return obj; } const _mappings = Symbol('mappings'); class TypeMappings { constructor(db) { this.db = db; this[_mappings] = {}; } /** (for internal use) */ get mappings() { return this[_mappings]; } /** (for internal use) */ map(path) { return map(this[_mappings], path); } /** * Maps objects that are stored in a specific path to a class, so they can automatically be * serialized when stored to, and deserialized (instantiated) when loaded from the database. * @param path path to an object container, eg "users" or "users/*\/posts" * @param type class to bind all child objects of path to * Best practice is to implement 2 methods for instantiation and serializing of your objects: * 1) `static create(snap: DataSnapshot)` and 2) `serialize(ref: DataReference)`. See example * @param options (optional) You can specify the functions to use to * serialize and/or instantiate your class. If you do not specificy a creator (constructor) method, * AceBase will call `YourClass.create(snapshot)` method if it exists, or create an instance of * YourClass with `new YourClass(snapshot)`. * If you do not specifiy a serializer method, AceBase will call `YourClass.prototype.serialize(ref)` * if it exists, or tries storing your object's fields unaltered. NOTE: `this` in your creator * function will point to `YourClass`, and `this` in your serializer function will point to the * `instance` of `YourClass`. * @example * class User { * static create(snap: DataSnapshot): User { * // Deserialize (instantiate) User from plain database object * let user = new User(); * Object.assign(user, snap.val()); // Copy all properties to user * user.id = snap.ref.key; // Add the key as id * return user; * } * serialize(ref: DataReference) { * // Serialize user for database storage * return { * name: this.name * email: this.email * }; * } * } * db.types.bind('users', User); // Automatically uses serialize and static create methods */ bind(path, type, options = {}) { // Maps objects that are stored in a specific path to a constructor method, // so they are automatically deserialized if (typeof path !== 'string') { throw new TypeError('path must be a string'); } if (typeof type !== 'function') { throw new TypeError('constructor must be a function'); } if (typeof options.serializer === 'undefined') { // if (typeof type.prototype.serialize === 'function') { // // Use .serialize instance method // options.serializer = type.prototype.serialize; // } // Use object's serialize method upon serialization (if available) } else if (typeof options.serializer === 'string') { if (typeof type.prototype[options.serializer] === 'function') { options.serializer = type.prototype[options.serializer]; } else { throw new TypeError(`${type.name}.prototype.${options.serializer} is not a function, cannot use it as serializer`); } } else if (typeof options.serializer !== 'function') { throw new TypeError(`serializer for class ${type.name} must be a function, or the name of a prototype method`); } if (typeof options.creator === 'undefined') { if (typeof type.create === 'function') { // Use static .create as creator method options.creator = type.create; } } else if (typeof options.creator === 'string') { if (typeof type[options.creator] === 'function') { options.creator = type[options.creator]; } else { throw new TypeError(`${type.name}.${options.creator} is not a function, cannot use it as creator`); } } else if (typeof options.creator !== 'function') { throw new TypeError(`creator for class ${type.name} must be a function, or the name of a static method`); } path = path.replace(/^\/|\/$/g, ''); // trim slashes this[_mappings][path] = { db: this.db, type, creator: options.creator, serializer: options.serializer, deserialize(snap) { // run constructor method let obj; if (this.creator) { obj = this.creator.call(this.type, snap); } else { obj = new this.type(snap); } return obj; }, serialize(obj, ref) { if (this.serializer) { obj = this.serializer.call(obj, ref, obj); } else if (obj && typeof obj.serialize === 'function') { obj = obj.serialize(ref, obj); } return obj; }, }; } /** * @internal (for internal use) * Serializes any child in given object that has a type mapping * @param path | path to the object's location * @param obj object to serialize */ serialize(path, obj) { return process(this.db, this[_mappings], path, obj, 'serialize'); } /** * @internal (for internal use) * Deserialzes any child in given object that has a type mapping * @param path path to the object's location * @param obj object to deserialize */ deserialize(path, obj) { return process(this.db, this[_mappings], path, obj, 'deserialize'); } } exports.TypeMappings = TypeMappings; //# sourceMappingURL=type-mappings.js.map