UNPKG

orange-orm

Version:

Object Relational Mapper

1,001 lines (800 loc) 22.1 kB
'use strict'; const PATH_SEPARATOR = '.'; const TARGET = Symbol('target'); const UNSUBSCRIBE = Symbol('unsubscribe'); function isBuiltinWithMutableMethods(value) { return value instanceof Date || value instanceof Set || value instanceof Map || value instanceof WeakSet || value instanceof WeakMap || ArrayBuffer.isView(value); } function isBuiltinWithoutMutableMethods(value) { return (typeof value === 'object' ? value === null : typeof value !== 'function') || value instanceof RegExp; } var isArray = Array.isArray; function isSymbol(value) { return typeof value === 'symbol'; } const path = { after: (path, subPath) => { if (isArray(path)) { return path.slice(subPath.length); } if (subPath === '') { return path; } return path.slice(subPath.length + 1); }, concat: (path, key) => { if (isArray(path)) { path = [...path]; if (key) { path.push(key); } return path; } if (key && key.toString !== undefined) { if (path !== '') { path += PATH_SEPARATOR; } if (isSymbol(key)) { return path + key.toString(); } return path + key; } return path; }, initial: path => { if (isArray(path)) { return path.slice(0, -1); } if (path === '') { return path; } const index = path.lastIndexOf(PATH_SEPARATOR); if (index === -1) { return ''; } return path.slice(0, index); }, last: path => { if (isArray(path)) { return path[path.length - 1] || ''; } if (path === '') { return path; } const index = path.lastIndexOf(PATH_SEPARATOR); if (index === -1) { return path; } return path.slice(index + 1); }, walk: (path, callback) => { if (isArray(path)) { for (const key of path) { callback(key); } } else if (path !== '') { let position = 0; let index = path.indexOf(PATH_SEPARATOR); if (index === -1) { callback(path); } else { while (position < path.length) { if (index === -1) { index = path.length; } callback(path.slice(position, index)); position = index + 1; index = path.indexOf(PATH_SEPARATOR, position); } } } }, get(object, path) { this.walk(path, key => { if (object) { object = object[key]; } }); return object; }, }; function isIterator(value) { return typeof value === 'object' && typeof value.next === 'function'; } // eslint-disable-next-line max-params function wrapIterator(iterator, target, thisArg, applyPath, prepareValue) { const originalNext = iterator.next; if (target.name === 'entries') { iterator.next = function () { const result = originalNext.call(this); if (result.done === false) { result.value[0] = prepareValue( result.value[0], target, result.value[0], applyPath, ); result.value[1] = prepareValue( result.value[1], target, result.value[0], applyPath, ); } return result; }; } else if (target.name === 'values') { const keyIterator = thisArg[TARGET].keys(); iterator.next = function () { const result = originalNext.call(this); if (result.done === false) { result.value = prepareValue( result.value, target, keyIterator.next().value, applyPath, ); } return result; }; } else { iterator.next = function () { const result = originalNext.call(this); if (result.done === false) { result.value = prepareValue( result.value, target, result.value, applyPath, ); } return result; }; } return iterator; } function ignoreProperty(cache, options, property) { return cache.isUnsubscribed || (options.ignoreSymbols && isSymbol(property)) || (options.ignoreUnderscores && property.charAt(0) === '_') || ('ignoreKeys' in options && options.ignoreKeys.includes(property)); } /** @class Cache @private */ class Cache { constructor(equals) { this._equals = equals; this._proxyCache = new WeakMap(); this._pathCache = new WeakMap(); this.isUnsubscribed = false; } _getDescriptorCache() { if (this._descriptorCache === undefined) { this._descriptorCache = new WeakMap(); } return this._descriptorCache; } _getProperties(target) { const descriptorCache = this._getDescriptorCache(); let properties = descriptorCache.get(target); if (properties === undefined) { properties = {}; descriptorCache.set(target, properties); } return properties; } _getOwnPropertyDescriptor(target, property) { if (this.isUnsubscribed) { return Reflect.getOwnPropertyDescriptor(target, property); } const properties = this._getProperties(target); let descriptor = properties[property]; if (descriptor === undefined) { descriptor = Reflect.getOwnPropertyDescriptor(target, property); properties[property] = descriptor; } return descriptor; } getProxy(target, path, handler, proxyTarget) { if (this.isUnsubscribed) { return target; } const reflectTarget = target[proxyTarget]; const source = reflectTarget || target; this._pathCache.set(source, path); let proxy = this._proxyCache.get(source); if (proxy === undefined) { proxy = reflectTarget === undefined ? new Proxy(target, handler) : target; this._proxyCache.set(source, proxy); } return proxy; } getPath(target) { return this.isUnsubscribed ? undefined : this._pathCache.get(target); } isDetached(target, object) { return !Object.is(target, path.get(object, this.getPath(target))); } defineProperty(target, property, descriptor) { if (!Reflect.defineProperty(target, property, descriptor)) { return false; } if (!this.isUnsubscribed) { this._getProperties(target)[property] = descriptor; } return true; } setProperty(target, property, value, receiver, previous) { // eslint-disable-line max-params if (!this._equals(previous, value) || !(property in target)) { const descriptor = this._getOwnPropertyDescriptor(target, property); if (descriptor !== undefined && 'set' in descriptor) { return Reflect.set(target, property, value, receiver); } return Reflect.set(target, property, value); } return true; } deleteProperty(target, property, previous) { if (Reflect.deleteProperty(target, property)) { if (!this.isUnsubscribed) { const properties = this._getDescriptorCache().get(target); if (properties) { delete properties[property]; this._pathCache.delete(previous); } } return true; } return false; } isSameDescriptor(a, target, property) { const b = this._getOwnPropertyDescriptor(target, property); return a !== undefined && b !== undefined && Object.is(a.value, b.value) && (a.writable || false) === (b.writable || false) && (a.enumerable || false) === (b.enumerable || false) && (a.configurable || false) === (b.configurable || false) && a.get === b.get && a.set === b.set; } isGetInvariant(target, property) { const descriptor = this._getOwnPropertyDescriptor(target, property); return descriptor !== undefined && descriptor.configurable !== true && descriptor.writable !== true; } unsubscribe() { this._descriptorCache = null; this._pathCache = null; this._proxyCache = null; this.isUnsubscribed = true; } } function isObject(value) { return toString.call(value) === '[object Object]'; } function isDiffCertain() { return true; } function isDiffArrays(clone, value) { return clone.length !== value.length || clone.some((item, index) => value[index] !== item); } const IMMUTABLE_OBJECT_METHODS = new Set([ 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf', ]); const IMMUTABLE_ARRAY_METHODS = new Set([ 'concat', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', ]); const MUTABLE_ARRAY_METHODS = { push: isDiffCertain, pop: isDiffCertain, shift: isDiffCertain, unshift: isDiffCertain, copyWithin: isDiffArrays, reverse: isDiffArrays, sort: isDiffArrays, splice: isDiffArrays, flat: isDiffArrays, fill: isDiffArrays, }; const HANDLED_ARRAY_METHODS = new Set([ ...IMMUTABLE_OBJECT_METHODS, ...IMMUTABLE_ARRAY_METHODS, ...Object.keys(MUTABLE_ARRAY_METHODS), ]); function isDiffSets(clone, value) { if (clone.size !== value.size) { return true; } for (const element of clone) { if (!value.has(element)) { return true; } } return false; } const COLLECTION_ITERATOR_METHODS = [ 'keys', 'values', 'entries', ]; const IMMUTABLE_SET_METHODS = new Set([ 'has', 'toString', ]); const MUTABLE_SET_METHODS = { add: isDiffSets, clear: isDiffSets, delete: isDiffSets, forEach: isDiffSets, }; const HANDLED_SET_METHODS = new Set([ ...IMMUTABLE_SET_METHODS, ...Object.keys(MUTABLE_SET_METHODS), ...COLLECTION_ITERATOR_METHODS, ]); function isDiffMaps(clone, value) { if (clone.size !== value.size) { return true; } let bValue; for (const [key, aValue] of clone) { bValue = value.get(key); if (bValue !== aValue || (bValue === undefined && !value.has(key))) { return true; } } return false; } const IMMUTABLE_MAP_METHODS = new Set([...IMMUTABLE_SET_METHODS, 'get']); const MUTABLE_MAP_METHODS = { set: isDiffMaps, clear: isDiffMaps, delete: isDiffMaps, forEach: isDiffMaps, }; const HANDLED_MAP_METHODS = new Set([ ...IMMUTABLE_MAP_METHODS, ...Object.keys(MUTABLE_MAP_METHODS), ...COLLECTION_ITERATOR_METHODS, ]); class CloneObject { constructor(value, path, argumentsList, hasOnValidate) { this._path = path; this._isChanged = false; this._clonedCache = new Set(); this._hasOnValidate = hasOnValidate; this._changes = hasOnValidate ? [] : null; this.clone = path === undefined ? value : this._shallowClone(value); } static isHandledMethod(name) { return IMMUTABLE_OBJECT_METHODS.has(name); } _shallowClone(value) { let clone = value; if (isObject(value)) { clone = {...value}; } else if (isArray(value) || ArrayBuffer.isView(value)) { clone = [...value]; } else if (value instanceof Date) { clone = new Date(value); } else if (value instanceof Set) { clone = new Set([...value].map(item => this._shallowClone(item))); } else if (value instanceof Map) { clone = new Map(); for (const [key, item] of value.entries()) { clone.set(key, this._shallowClone(item)); } } this._clonedCache.add(clone); return clone; } preferredThisArg(isHandledMethod, name, thisArg, thisProxyTarget) { if (isHandledMethod) { if (isArray(thisProxyTarget)) { this._onIsChanged = MUTABLE_ARRAY_METHODS[name]; } else if (thisProxyTarget instanceof Set) { this._onIsChanged = MUTABLE_SET_METHODS[name]; } else if (thisProxyTarget instanceof Map) { this._onIsChanged = MUTABLE_MAP_METHODS[name]; } return thisProxyTarget; } return thisArg; } update(fullPath, property, value) { const changePath = path.after(fullPath, this._path); if (property !== 'length') { let object = this.clone; path.walk(changePath, key => { if (object && object[key]) { if (!this._clonedCache.has(object[key])) { object[key] = this._shallowClone(object[key]); } object = object[key]; } }); if (this._hasOnValidate) { this._changes.push({ path: changePath, property, previous: value, }); } if (object && object[property]) { object[property] = value; } } this._isChanged = true; } undo(object) { let change; for (let index = this._changes.length - 1; index !== -1; index--) { change = this._changes[index]; path.get(object, change.path)[change.property] = change.previous; } } isChanged(value) { return this._onIsChanged === undefined ? this._isChanged : this._onIsChanged(this.clone, value); } } class CloneArray extends CloneObject { static isHandledMethod(name) { return HANDLED_ARRAY_METHODS.has(name); } } class CloneDate extends CloneObject { undo(object) { object.setTime(this.clone.getTime()); } isChanged(value, equals) { return !equals(this.clone.valueOf(), value.valueOf()); } } class CloneSet extends CloneObject { static isHandledMethod(name) { return HANDLED_SET_METHODS.has(name); } undo(object) { for (const value of this.clone) { object.add(value); } for (const value of object) { if (!this.clone.has(value)) { object.delete(value); } } } } class CloneMap extends CloneObject { static isHandledMethod(name) { return HANDLED_MAP_METHODS.has(name); } undo(object) { for (const [key, value] of this.clone.entries()) { object.set(key, value); } for (const key of object.keys()) { if (!this.clone.has(key)) { object.delete(key); } } } } class CloneWeakSet extends CloneObject { constructor(value, path, argumentsList, hasOnValidate) { super(undefined, path, argumentsList, hasOnValidate); this._arg1 = argumentsList[0]; this._weakValue = value.has(this._arg1); } isChanged(value) { return this._weakValue !== value.has(this._arg1); } undo(object) { if (this._weakValue && !object.has(this._arg1)) { object.add(this._arg1); } else { object.delete(this._arg1); } } } class CloneWeakMap extends CloneObject { constructor(value, path, argumentsList, hasOnValidate) { super(undefined, path, argumentsList, hasOnValidate); this._weakKey = argumentsList[0]; this._weakHas = value.has(this._weakKey); this._weakValue = value.get(this._weakKey); } isChanged(value) { return this._weakValue !== value.get(this._weakKey); } undo(object) { const weakHas = object.has(this._weakKey); if (this._weakHas && !weakHas) { object.set(this._weakKey, this._weakValue); } else if (!this._weakHas && weakHas) { object.delete(this._weakKey); } else if (this._weakValue !== object.get(this._weakKey)) { object.set(this._weakKey, this._weakValue); } } } class SmartClone { constructor(hasOnValidate) { this._stack = []; this._hasOnValidate = hasOnValidate; } static isHandledType(value) { return isObject(value) || isArray(value) || isBuiltinWithMutableMethods(value); } static isHandledMethod(target, name) { if (isObject(target)) { return CloneObject.isHandledMethod(name); } if (isArray(target)) { return CloneArray.isHandledMethod(name); } if (target instanceof Set) { return CloneSet.isHandledMethod(name); } if (target instanceof Map) { return CloneMap.isHandledMethod(name); } return isBuiltinWithMutableMethods(target); } get isCloning() { return this._stack.length > 0; } start(value, path, argumentsList) { let CloneClass = CloneObject; if (isArray(value)) { CloneClass = CloneArray; } else if (value instanceof Date) { CloneClass = CloneDate; } else if (value instanceof Set) { CloneClass = CloneSet; } else if (value instanceof Map) { CloneClass = CloneMap; } else if (value instanceof WeakSet) { CloneClass = CloneWeakSet; } else if (value instanceof WeakMap) { CloneClass = CloneWeakMap; } this._stack.push(new CloneClass(value, path, argumentsList, this._hasOnValidate)); } update(fullPath, property, value) { this._stack[this._stack.length - 1].update(fullPath, property, value); } preferredThisArg(target, thisArg, thisProxyTarget) { const {name} = target; const isHandledMethod = SmartClone.isHandledMethod(thisProxyTarget, name); return this._stack[this._stack.length - 1] .preferredThisArg(isHandledMethod, name, thisArg, thisProxyTarget); } isChanged(isMutable, value, equals) { return this._stack[this._stack.length - 1].isChanged(isMutable, value, equals); } undo(object) { if (this._previousClone !== undefined) { this._previousClone.undo(object); } } stop() { this._previousClone = this._stack.pop(); return this._previousClone.clone; } } /* eslint-disable unicorn/prefer-spread */ const defaultOptions = { equals: Object.is, isShallow: false, pathAsArray: false, ignoreSymbols: false, ignoreUnderscores: false, ignoreDetached: false, details: false, }; const onChange = (object, onChange, options = {}) => { options = { ...defaultOptions, ...options, }; const proxyTarget = Symbol('ProxyTarget'); const {equals, isShallow, ignoreDetached, details} = options; const cache = new Cache(equals); const hasOnValidate = typeof options.onValidate === 'function'; const smartClone = new SmartClone(hasOnValidate); // eslint-disable-next-line max-params const validate = (target, property, value, previous, applyData) => !hasOnValidate || smartClone.isCloning || options.onValidate(path.concat(cache.getPath(target), property), value, previous, applyData) === true; const handleChangeOnTarget = (target, property, value, previous) => { if ( !ignoreProperty(cache, options, property) && !(ignoreDetached && cache.isDetached(target, object)) ) { handleChange(cache.getPath(target), property, value, previous); } }; // eslint-disable-next-line max-params const handleChange = (changePath, property, value, previous, applyData) => { if (smartClone.isCloning) { smartClone.update(changePath, property, previous); } else { onChange(path.concat(changePath, property), value, previous, applyData); } }; const getProxyTarget = value => value ? (value[proxyTarget] || value) : value; const prepareValue = (value, target, property, basePath) => { if ( isBuiltinWithoutMutableMethods(value) || property === 'constructor' || (isShallow && !SmartClone.isHandledMethod(target, property)) || ignoreProperty(cache, options, property) || cache.isGetInvariant(target, property) || (ignoreDetached && cache.isDetached(target, object)) ) { return value; } if (basePath === undefined) { basePath = cache.getPath(target); } return cache.getProxy(value, path.concat(basePath, property), handler, proxyTarget); }; const handler = { get(target, property, receiver) { if (isSymbol(property)) { if (property === proxyTarget || property === TARGET) { return target; } if ( property === UNSUBSCRIBE && !cache.isUnsubscribed && cache.getPath(target).length === 0 ) { cache.unsubscribe(); return target; } } const value = isBuiltinWithMutableMethods(target) ? Reflect.get(target, property) : Reflect.get(target, property, receiver); return prepareValue(value, target, property); }, set(target, property, value, receiver) { value = getProxyTarget(value); const reflectTarget = target[proxyTarget] || target; const previous = reflectTarget[property]; if (equals(previous, value) && property in target) { return true; } const isValid = validate(target, property, value, previous); if ( isValid && cache.setProperty(reflectTarget, property, value, receiver, previous) ) { handleChangeOnTarget(target, property, target[property], previous); return true; } return !isValid; }, defineProperty(target, property, descriptor) { if (!cache.isSameDescriptor(descriptor, target, property)) { const previous = target[property]; if ( validate(target, property, descriptor.value, previous) && cache.defineProperty(target, property, descriptor, previous) ) { handleChangeOnTarget(target, property, descriptor.value, previous); } } return true; }, deleteProperty(target, property) { if (!Reflect.has(target, property)) { return true; } const previous = Reflect.get(target, property); const isValid = validate(target, property, undefined, previous); if ( isValid && cache.deleteProperty(target, property, previous) ) { handleChangeOnTarget(target, property, undefined, previous); return true; } return !isValid; }, apply(target, thisArg, argumentsList) { const thisProxyTarget = thisArg[proxyTarget] || thisArg; if (cache.isUnsubscribed) { return Reflect.apply(target, thisProxyTarget, argumentsList); } if ( (details === false || (details !== true && !details.includes(target.name))) && SmartClone.isHandledType(thisProxyTarget) ) { let applyPath = path.initial(cache.getPath(target)); const isHandledMethod = SmartClone.isHandledMethod(thisProxyTarget, target.name); smartClone.start(thisProxyTarget, applyPath, argumentsList); let result = Reflect.apply( target, smartClone.preferredThisArg(target, thisArg, thisProxyTarget), isHandledMethod ? argumentsList.map(argument => getProxyTarget(argument)) : argumentsList, ); const isChanged = smartClone.isChanged(thisProxyTarget, equals); const previous = smartClone.stop(); if (SmartClone.isHandledType(result) && isHandledMethod) { if (thisArg instanceof Map && target.name === 'get') { applyPath = path.concat(applyPath, argumentsList[0]); } result = cache.getProxy(result, applyPath, handler); } if (isChanged) { const applyData = { name: target.name, args: argumentsList, result, }; const changePath = smartClone.isCloning ? path.initial(applyPath) : applyPath; const property = smartClone.isCloning ? path.last(applyPath) : ''; if (validate(path.get(object, changePath), property, thisProxyTarget, previous, applyData)) { handleChange(changePath, property, thisProxyTarget, previous, applyData); } else { smartClone.undo(thisProxyTarget); } } if ( (thisArg instanceof Map || thisArg instanceof Set) && isIterator(result) ) { return wrapIterator(result, target, thisArg, applyPath, prepareValue); } return result; } return Reflect.apply(target, thisArg, argumentsList); }, }; const proxy = cache.getProxy(object, options.pathAsArray ? [] : '', handler); onChange = onChange.bind(proxy); if (hasOnValidate) { options.onValidate = options.onValidate.bind(proxy); } return proxy; }; onChange.target = proxy => (proxy && proxy[TARGET]) || proxy; onChange.unsubscribe = proxy => proxy[UNSUBSCRIBE] || proxy; module.exports = onChange;