UNPKG

@small-tech/jsdb

Version:

A zero-dependency, transparent, in-memory, streaming write-on-update JavaScript database for Small Web applications that persists to a JavaScript transaction log.

170 lines (141 loc) 6.56 kB
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // DataProxy class. // // Copyright ⓒ 2020-2021 Aral Balkan. Licensed under AGPLv3 or later. // Shared with ♥ by the Small Technology Foundation. // // Like this? Fund us! // https://small-tech.org/fund-us // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// import JSDF from './JSDF.js' import { needsToBeProxified, quoteKeyIfNotSafeInteger } from './Util.js' import IncompleteQueryProxy from './IncompleteQueryProxy.js' import QueryProxy from './QueryProxy.js' const variableReference = (id, property) => `${id}[${quoteKeyIfNotSafeInteger(property)}]` export default class DataProxy { // // Class. // // Factory method. Use this to instantiate a deep (non-lazy) proxified structure. static createDeepProxy (table, object, id) { Object.keys(object).forEach(key => { // We treat keys that begin with an underscore as private so // we don’t proxify them. if (key.startsWith('_')) return const value = object[key] if (needsToBeProxified(value)) { object[key] = this.createDeepProxy(table, value, variableReference(id, key)) } }) // Proxify the original object itself. return new this(table, object, id) } // // Instance. // constructor (table, data, id) { this.table = table this.data = data this.id = id // Note: we return a proxy instance; not an instance of DataProxy. Use accordingly. this.proxy = new Proxy(this.data, { get: this.getHandler.bind(this), set: this.setHandler.bind(this), defineProperty: this.definePropertyHandler.bind(this), deleteProperty: this.deletePropertyHandler.bind(this) }) return this.proxy } getHandler (target, property, receiver) { // This is mainly for internal use. Exposes the table instance itself from the data proxy. if (property === '__table__') return this.table // If a proxy is being asked for toJSON, it must return the toJSON of its data. // We encounter this if we’re coping data from one JSDB table to another, for example. if (property === 'toJSON' && this.data.toJSON != undefined) { return this.data.toJSON.bind(this.data) } // Bind methods on Date instances to the Date object itself as they will not // work if accessed on the Proxy instance. // (See https://codeberg.org/small-tech/jsdb/issues/5) if (target.constructor !== undefined && target.constructor.name === 'Date' && property !== 'constructor') { return Reflect.get(this.data, property, receiver).bind(this.data) } // The reserved word “where” starts a query. We return a function // that executes and captures the passed property that we want to query // on and returns a QueryProxy instance that has references to both the // table and that data. // // Note that queries as well as data set operations execute synchronously // so you will not encounter race conditions when using them in web routes. if (property === 'where') { if (Array.isArray(this.data)) { return (function (property) { return new IncompleteQueryProxy(this.table, this.data, `valueOf.${property}`) }).bind(this) } else { throw new TypeError('Queries can only be applied to arrays.') } } // For more complicated queries (e.g., those involving parentheticals, etc.), // you can pass the query string directly. // // Note that when you do this, you will have to prefix your property names with valueOf. // e.g., The query string for where('age').isGreaterThanOrEqualTo(21).and('name').startsWith('A') would be // 'valueOf.age >= 21 && valueOf.name.startsWith("A")' if (property === 'whereIsTrue') { if (Array.isArray(this.data)) { return (function (predicate) { return new QueryProxy(this.table, this.data, predicate) }).bind(this) } else { throw new TypeError('Queries can only be applied to arrays.') } } return Reflect.get(this.data, property, receiver) } setHandler (target, property, value, receiver) { // If this is a superfluous call to set the length following a push() statement, // do not pollute the transaction log with it. if (Array.isArray(target) && property === 'length' && target.length === value) { // Update the in-memory object graph but don’t persist. Reflect.set(this.data, property, value, receiver) return true } // Ignore symbols (as we can’t differentiate between registered and unregistered // symbols and so cannot reliably recreate them) and properties that start with // underscores as we treat them as private/internal. // // We also ignore dynamic function assignments so they don’t throw (in case custom // objects or the base classes they extend implement monkey-patching/mixins at). // (Any such exceptions added here must also be reflected in the JSDB class // so they’re applied in its serialisation method.) if (typeof property === 'symbol' || (typeof property === 'string' && property.startsWith('_')) || typeof value === 'function') { Reflect.set(this.data, property, value, receiver) return true } const keyPath = variableReference(this.id, property) // Serialise the value update into a JSDF transaction. const change = JSDF.serialise(value, keyPath) // Update the in-memory store. if (value === undefined) { // Delete property if undefined value assigned to property. this.data[property] = undefined // This check is necessary since, surprisingly, null values have type `object` in JavaScript. } else if (value !== null && typeof value === 'object') { this.data[property] = DataProxy.createDeepProxy(this.table, value, keyPath) } else { Reflect.set(this.data, property, value, receiver) } // Persist the change. this.table.persistChange(change) return true } definePropertyHandler (target, key, descriptor) { // Note: we do not trigger a save here as one will be triggered by the setHandler. return Reflect.defineProperty(this.data, key, descriptor) } deletePropertyHandler (target, property) { const change = `delete ${variableReference(this.id, property)};\n` Reflect.deleteProperty(this.data, property) this.table.persistChange(change) return true } }