orange-orm
Version:
Object Relational Mapper
247 lines (199 loc) • 6.79 kB
JavaScript
/* eslint-disable unicorn/prefer-spread */
import {TARGET, UNSUBSCRIBE} from './lib/constants.js';
import {isBuiltinWithMutableMethods, isBuiltinWithoutMutableMethods} from './lib/is-builtin.js';
import path from './lib/path.js';
import isSymbol from './lib/is-symbol.js';
import isIterator from './lib/is-iterator.js';
import wrapIterator from './lib/wrap-iterator.js';
import ignoreProperty from './lib/ignore-property.js';
import Cache from './lib/cache.js';
import SmartClone from './lib/smart-clone/smart-clone.js';
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;
export default onChange;