validated-changeset
Version:
Changesets for your local state
950 lines • 33.2 kB
JavaScript
import Change, { getChangeValue, isChange } from './-private/change';
import { getKeyValues, getKeyErrorValues } from './utils/get-key-values';
import lookupValidator from './utils/validator-lookup';
import { notifierForEvent } from './-private/evented';
import Err from './-private/err';
import { hasKey, pathInChanges } from './utils/has-key';
import normalizeObject from './utils/normalize-object';
import { hasChanges } from './utils/has-changes';
import pureAssign from './utils/assign';
import { flattenValidations } from './utils/flatten-validations';
import isChangeset, { CHANGESET } from './utils/is-changeset';
import isObject from './utils/is-object';
import isPromise from './utils/is-promise';
import keyInObject from './utils/key-in-object';
import mergeNested from './utils/merge-nested';
import { buildOldValues } from './utils/build-old-values';
import { ObjectTreeNode } from './utils/object-tree-node';
import objectWithout from './utils/object-without';
import take from './utils/take';
import mergeDeep, { propertyIsUnsafe } from './utils/merge-deep';
import setDeep from './utils/set-deep';
import getDeep, { getSubObject } from './utils/get-deep';
import { objectToArray, arrayToObject, isArrayObject } from './utils/array-object';
import { ValidatedChangeset as ValidationChangeset, Changeset as ValidationChangesetFactory } from './validated';
export { ValidationChangeset, ValidationChangesetFactory, CHANGESET, Change, Err, isArrayObject, arrayToObject, objectToArray, buildOldValues, isChangeset, isObject, isChange, getChangeValue, isPromise, getKeyValues, keyInObject, lookupValidator, mergeNested, normalizeObject, objectWithout, pureAssign, take, mergeDeep, setDeep, getDeep, propertyIsUnsafe };
const { keys } = Object;
const CONTENT = '_content';
const PREVIOUS_CONTENT = '_previousContent';
const CHANGES = '_changes';
const ERRORS = '_errors';
const ERRORS_CACHE = '_errorsCache';
const VALIDATOR = '_validator';
const OPTIONS = '_options';
const RUNNING_VALIDATIONS = '_runningValidations';
const BEFORE_VALIDATION_EVENT = 'beforeValidation';
const AFTER_VALIDATION_EVENT = 'afterValidation';
const AFTER_ROLLBACK_EVENT = 'afterRollback';
const defaultValidatorFn = () => true;
const defaultOptions = { skipValidate: false };
const DEBUG = process.env.NODE_ENV !== 'production';
function assert(msg, property) {
if (DEBUG) {
if (!property) {
throw new Error(msg);
}
}
}
function maybeUnwrapProxy(content) {
return content;
}
export class BufferedChangeset {
constructor(obj, validateFn = defaultValidatorFn, validationMap = {}, options = {}) {
this.validateFn = validateFn;
this.validationMap = validationMap;
this.__changeset__ = CHANGESET;
this._eventedNotifiers = {};
/**
* @property isObject
* @override
*/
this.isObject = isObject;
/**
* @property maybeUnwrapProxy
* @override
*/
this.maybeUnwrapProxy = maybeUnwrapProxy;
/**
* @property setDeep
* @override
*/
this.setDeep = setDeep;
/**
* @property getDeep
* @override
*/
this.getDeep = getDeep;
/**
* @property mergeDeep
* @override
*/
this.mergeDeep = mergeDeep;
this[CONTENT] = obj;
this[PREVIOUS_CONTENT] = undefined;
this[CHANGES] = {};
this[VALIDATOR] = validateFn;
this[OPTIONS] = pureAssign(defaultOptions, JSON.parse(JSON.stringify(options)));
this[RUNNING_VALIDATIONS] = {};
let validatorMapKeys = this.validationMap ? keys(this.validationMap) : [];
if (this[OPTIONS].initValidate && validatorMapKeys.length > 0) {
let errors = this._collectErrors();
this[ERRORS] = errors;
this[ERRORS_CACHE] = errors;
}
else {
this[ERRORS] = {};
this[ERRORS_CACHE] = {};
}
}
on(eventName, callback) {
const notifier = notifierForEvent(this, eventName);
return notifier.addListener(callback);
}
off(eventName, callback) {
const notifier = notifierForEvent(this, eventName);
return notifier.removeListener(callback);
}
trigger(eventName, ...args) {
const notifier = notifierForEvent(this, eventName);
if (notifier) {
notifier.trigger(...args);
}
}
/**
* @property safeGet
* @override
*/
safeGet(obj, key) {
return obj[key];
}
/**
* @property safeSet
* @override
*/
safeSet(obj, key, value) {
return (obj[key] = value);
}
get _bareChanges() {
let obj = this[CHANGES];
return getKeyValues(obj).reduce((newObj, { key, value }) => {
newObj[key] = value;
return newObj;
}, Object.create(null));
}
/**
* @property changes
* @type {Array}
*/
get changes() {
let obj = this[CHANGES];
// [{ key, value }, ...]
return getKeyValues(obj);
}
/**
* @property errors
* @type {Array}
*/
get errors() {
let obj = this[ERRORS];
// [{ key, validation, value }, ...]
return getKeyErrorValues(obj);
}
get change() {
let obj = this[CHANGES];
if (hasChanges(this[CHANGES])) {
return normalizeObject(obj);
}
return {};
}
get error() {
return this[ERRORS];
}
get data() {
return this[CONTENT];
}
/**
* @property isValid
* @type {Array}
*/
get isValid() {
return getKeyErrorValues(this[ERRORS]).length === 0;
}
/**
* @property isPristine
* @type {Boolean}
*/
get isPristine() {
let validationKeys = Object.keys(this[CHANGES]);
const userChangesetKeys = this[OPTIONS].changesetKeys;
if (Array.isArray(userChangesetKeys) && userChangesetKeys.length) {
validationKeys = validationKeys.filter((k) => userChangesetKeys.includes(k));
}
if (validationKeys.length === 0) {
return true;
}
return !hasChanges(this[CHANGES]);
}
/**
* @property isInvalid
* @type {Boolean}
*/
get isInvalid() {
return !this.isValid;
}
/**
* @property isDirty
* @type {Boolean}
*/
get isDirty() {
return !this.isPristine;
}
/**
* Stores change on the changeset.
* This approximately works just like the Ember API
*
* @method setUnknownProperty
*/
setUnknownProperty(key, value) {
let config = this[OPTIONS];
let changesetKeys = config.changesetKeys;
if (Array.isArray(changesetKeys) && changesetKeys.length > 0) {
const hasKey = changesetKeys.find((chKey) => key.match(chKey));
if (!hasKey) {
return;
}
}
let content = this[CONTENT];
let oldValue = this.safeGet(content, key);
let skipValidate = config.skipValidate;
if (skipValidate) {
this._setProperty({ key, value, oldValue });
this._handleValidation(true, { key, value });
return;
}
this._setProperty({ key, value, oldValue });
this._validateKey(key, value);
}
/**
* String representation for the changeset.
*/
get [Symbol.toStringTag]() {
let normalisedContent = pureAssign(this[CONTENT], {});
return `changeset:${normalisedContent.toString()}`;
}
/**
* String representation for the changeset.
*/
toString() {
let normalisedContent = pureAssign(this[CONTENT], {});
return `changeset:${normalisedContent.toString()}`;
}
/**
* Provides a function to run before emitting changes to the model. The
* callback function must return a hash in the same shape:
*
* ```
* changeset
* .prepare((changes) => {
* let modified = {};
*
* for (let key in changes) {
* modified[underscore(key)] = changes[key];
* }
*
* return modified; // { first_name: "Jim", last_name: "Bob" }
* })
* .execute(); // execute the changes
* ```
*
* @method prepare
*/
prepare(prepareChangesFn) {
let changes = this._bareChanges;
let preparedChanges = prepareChangesFn(changes);
assert('Callback to `changeset.prepare` must return an object', this.isObject(preparedChanges));
let newObj = {};
if (this.isObject(preparedChanges)) {
let newChanges = keys(preparedChanges).reduce((newObj, key) => {
newObj[key] = new Change(preparedChanges[key]);
return newObj;
}, newObj);
// @tracked
this[CHANGES] = newChanges;
}
return this;
}
/**
* Executes the changeset if in a valid state.
*
* @method execute
*/
execute() {
let oldContent;
if (this.isValid && this.isDirty) {
let content = this[CONTENT];
let changes = this[CHANGES];
// keep old values in case of error and we want to rollback
oldContent = buildOldValues(content, this.changes, this.getDeep);
// we want mutation on original object
// @tracked
this[CONTENT] = this.mergeDeep(content, changes);
}
// trigger any registered callbacks by same keyword as method name
this.trigger('execute');
this[CHANGES] = {};
this[PREVIOUS_CONTENT] = oldContent;
return this;
}
unexecute() {
if (this[PREVIOUS_CONTENT]) {
this[CONTENT] = this.mergeDeep(this[CONTENT], this[PREVIOUS_CONTENT], {
safeGet: this.safeGet,
safeSet: this.safeSet
});
}
return this;
}
/**
* Executes the changeset and saves the underlying content.
*
* @method save
* @param {Object} options optional object to pass to content save method
*/
async save(options) {
let content = this[CONTENT];
let savePromise = Promise.resolve(this);
this.execute();
if (typeof content.save === 'function') {
savePromise = content.save(options);
}
else if (typeof this.safeGet(content, 'save') === 'function') {
// we might be getting this off a proxy object. For example, when a
// belongsTo relationship (a proxy on the parent model)
// another way would be content(belongsTo).content.save
let maybePromise = this.maybeUnwrapProxy(content).save();
if (maybePromise) {
savePromise = maybePromise;
}
}
try {
const result = await savePromise;
// cleanup changeset
this.rollback();
return result;
}
catch (e) {
throw e;
}
}
/**
* Merges 2 valid changesets and returns a new changeset. Both changesets
* must point to the same underlying object. The changeset target is the
* origin. For example:
*
* ```
* let changesetA = new Changeset(user, validatorFn);
* let changesetB = new Changeset(user, validatorFn);
* changesetA.set('firstName', 'Jim');
* changesetB.set('firstName', 'Jimmy');
* changesetB.set('lastName', 'Fallon');
* let changesetC = changesetA.merge(changesetB);
* changesetC.execute();
* user.get('firstName'); // "Jimmy"
* user.get('lastName'); // "Fallon"
* ```
*
* @method merge
*/
merge(changeset) {
let content = this[CONTENT];
assert('Cannot merge with a non-changeset', isChangeset(changeset));
assert('Cannot merge with a changeset of different content', changeset[CONTENT] === content);
if (this.isPristine && changeset.isPristine) {
return this;
}
let c1 = this[CHANGES];
let c2 = changeset[CHANGES];
let e1 = this[ERRORS];
let e2 = changeset[ERRORS];
// eslint-disable-next-line @typescript-eslint/no-use-before-define
let newChangeset = new ValidatedChangeset(content, this[VALIDATOR]); // ChangesetDef
let newErrors = objectWithout(keys(c2), e1);
let newChanges = objectWithout(keys(e2), c1);
let mergedErrors = mergeNested(newErrors, e2);
let mergedChanges = mergeNested(newChanges, c2);
newChangeset[ERRORS] = mergedErrors;
newChangeset[CHANGES] = mergedChanges;
newChangeset._notifyVirtualProperties();
return newChangeset;
}
/**
* Returns the changeset to its pristine state, and discards changes and
* errors.
*
* @method rollback
*/
rollback() {
// Get keys before reset.
let keys = this._rollbackKeys();
// Reset.
this[CHANGES] = {};
this[ERRORS] = {};
this[ERRORS_CACHE] = {};
this._notifyVirtualProperties(keys);
this.trigger(AFTER_ROLLBACK_EVENT);
return this;
}
/**
* Discards any errors, keeping only valid changes.
*
* @public
* @chainable
* @method rollbackInvalid
* @param {String} key optional key to rollback invalid values
* @return {Changeset}
*/
rollbackInvalid(key) {
let errorKeys = this.errors.map(({ key }) => key);
if (key) {
this._notifyVirtualProperties([key]);
// @tracked
this[ERRORS] = this._deleteKey(ERRORS, key);
this[ERRORS_CACHE] = this[ERRORS];
if (errorKeys.indexOf(key) > -1) {
this[CHANGES] = this._deleteKey(CHANGES, key);
}
}
else {
this._notifyVirtualProperties();
this[ERRORS] = {};
this[ERRORS_CACHE] = this[ERRORS];
// if on CHANGES hash, rollback those as well
errorKeys.forEach((errKey) => {
this[CHANGES] = this._deleteKey(CHANGES, errKey);
});
}
return this;
}
/**
* Discards changes/errors for the specified properly only.
*
* @public
* @chainable
* @method rollbackProperty
* @param {String} key key to delete off of changes and errors
* @return {Changeset}
*/
rollbackProperty(key) {
// @tracked
this[CHANGES] = this._deleteKey(CHANGES, key);
// @tracked
this[ERRORS] = this._deleteKey(ERRORS, key);
this[ERRORS_CACHE] = this[ERRORS];
return this;
}
/**
* Validates the changeset immediately against the validationMap passed in.
* If no key is passed into this method, it will validate all fields on the
* validationMap and set errors accordingly. Will throw an error if no
* validationMap is present.
*
* @method validate
*/
async validate(...validationKeys) {
if (keys(this.validationMap).length === 0 && !validationKeys.length) {
return Promise.resolve(null);
}
validationKeys =
validationKeys.length > 0
? validationKeys
: keys(flattenValidations(this.validationMap));
let maybePromise = validationKeys.map((key) => {
const value = this[key];
const resolvedValue = value instanceof ObjectTreeNode ? value.unwrap() : value;
return this._validateKey(key, resolvedValue);
});
return Promise.all(maybePromise);
}
/**
* Manually add an error to the changeset. If there is an existing
* error or change for `key`, it will be overwritten.
*
* @method addError
*/
addError(key, error) {
// Construct new `Err` instance.
let newError;
const isIErr = (error) => this.isObject(error) && !Array.isArray(error);
if (isIErr(error)) {
assert('Error must have value.', error.hasOwnProperty('value') || error.value !== undefined);
assert('Error must have validation.', error.hasOwnProperty('validation'));
newError = new Err(error.value, error.validation);
}
else {
let value = this[key];
newError = new Err(value, error);
}
// Add `key` to errors map.
let errors = this[ERRORS];
// @tracked
this[ERRORS] = this.setDeep(errors, key, newError, { safeSet: this.safeSet });
this[ERRORS_CACHE] = this[ERRORS];
// Return passed-in `error`.
return error;
}
/**
* Manually remove an error from the changeset.
*
* @method removeError
*/
removeError(key) {
if (key) {
this._notifyVirtualProperties([key]);
// @tracked
this[ERRORS] = this._deleteKey(ERRORS, key);
this[ERRORS_CACHE] = this[ERRORS];
}
else {
this._notifyVirtualProperties();
this[ERRORS] = {};
this[ERRORS_CACHE] = this[ERRORS];
}
return this;
}
/**
* @method removeErrors
*/
removeErrors() {
// @tracked
this[ERRORS] = {};
this[ERRORS_CACHE] = this[ERRORS];
}
/**
* Manually push multiple errors to the changeset as an array.
* key maybe in form 'name.short' so need to go deep
*
* @method pushErrors
*/
pushErrors(key, ...newErrors) {
let errors = this[ERRORS];
let existingError = this.getDeep(errors, key) || new Err(null, []);
let validation = existingError.validation;
let value = this[key];
if (!Array.isArray(validation) && Boolean(validation)) {
existingError.validation = [validation];
}
let v = existingError.validation;
validation = [...v, ...newErrors];
let newError = new Err(value, validation);
// @tracked
this[ERRORS] = this.setDeep(errors, key, newError, { safeSet: this.safeSet });
this[ERRORS_CACHE] = this[ERRORS];
return { value, validation };
}
/**
* Creates a snapshot of the changeset's errors and changes.
*
* @method snapshot
*/
snapshot() {
let changes = this[CHANGES];
let errors = this[ERRORS];
return {
changes: this.getChangesForSnapshot(changes),
errors: keys(errors).reduce((newObj, key) => {
let e = errors[key];
newObj[key] = { value: e.value, validation: e.validation };
return newObj;
}, {})
};
}
getChangesForSnapshot(changes) {
return keys(changes).reduce((newObj, key) => {
newObj[key] = isChange(changes[key])
? getChangeValue(changes[key])
: this.getChangesForSnapshot(changes[key]);
return newObj;
}, {});
}
/**
* Restores a snapshot of changes and errors. This overrides existing
* changes and errors.
*
* @method restore
*/
restore({ changes, errors }) {
let newChanges = this.getChangesFromSnapshot(changes);
let newErrors = keys(errors).reduce((newObj, key) => {
let e = errors[key];
newObj[key] = new Err(e.value, e.validation);
return newObj;
}, {});
// @tracked
this[CHANGES] = newChanges;
// @tracked
this[ERRORS] = newErrors;
this[ERRORS_CACHE] = this[ERRORS];
this._notifyVirtualProperties();
return this;
}
getChangesFromSnapshot(changes) {
return keys(changes).reduce((newObj, key) => {
newObj[key] = this.getChangeForProp(changes[key]);
return newObj;
}, {});
}
getChangeForProp(value) {
if (!isObject(value)) {
return new Change(value);
}
return keys(value).reduce((newObj, key) => {
newObj[key] = this.getChangeForProp(value[key]);
return newObj;
}, {});
}
/**
* Unlike `Ecto.Changeset.cast`, `cast` will take allowed keys and
* remove unwanted keys off of the changeset. For example, this method
* can be used to only allow specified changes through prior to saving.
*
* @method cast
*/
cast(allowed = []) {
let changes = this[CHANGES];
if (Array.isArray(allowed) && allowed.length === 0) {
return this;
}
let changeKeys = keys(changes);
let validKeys = changeKeys.filter((key) => allowed.includes(key));
let casted = take(changes, validKeys);
// @tracked
this[CHANGES] = casted;
return this;
}
/**
* Checks to see if async validator for a given key has not resolved.
* If no key is provided it will check to see if any async validator is running.
*
* @method isValidating
*/
isValidating(key) {
let runningValidations = this[RUNNING_VALIDATIONS];
let ks = keys(runningValidations);
if (key) {
return ks.includes(key);
}
return ks.length > 0;
}
/**
* Validates a specific key
*
* @method _validateKey
* @private
*/
_validateKey(key, value) {
let content = this[CONTENT];
let oldValue = this.getDeep(content, key);
let validation = this._validate(key, value, oldValue);
this.trigger(BEFORE_VALIDATION_EVENT, key);
// TODO: Address case when Promise is rejected.
if (isPromise(validation)) {
this._setIsValidating(key, validation);
let running = this[RUNNING_VALIDATIONS];
let promises = Object.entries(running);
return Promise.all(promises).then(() => {
return validation
.then((resolvedValidation) => {
delete running[key];
return this._handleValidation(resolvedValidation, { key, value });
})
.then((result) => {
this.trigger(AFTER_VALIDATION_EVENT, key);
return result;
});
});
}
let result = this._handleValidation(validation, { key, value });
this.trigger(AFTER_VALIDATION_EVENT, key);
return result;
}
/**
* Takes resolved validation and adds an error or simply returns the value
*
* @method _handleValidation
* @private
*/
_handleValidation(validation, { key, value }) {
// Happy path: remove `key` from error map.
// @tracked
// ERRORS_CACHE to avoid backtracking Ember assertion.
this[ERRORS] = this._deleteKey(ERRORS_CACHE, key);
// Error case.
if (!this._isValidResult(validation)) {
return this.addError(key, { value, validation });
}
return value;
}
/**
* runs the validator with the key and value
*
* @method _validate
* @private
*/
_validate(key, newValue, oldValue) {
let validator = this[VALIDATOR];
let content = this[CONTENT];
if (typeof validator === 'function') {
let validationResult = validator({
key,
newValue,
oldValue,
changes: this.change,
content
});
if (validationResult === undefined) {
// no validator function found for key
return true;
}
return validationResult;
}
return true;
}
/**
* Sets property on the changeset.
*/
_setProperty({ key, value, oldValue }) {
let changes = this[CHANGES];
// Happy path: update change map.
if (!isEqual(value, oldValue) || oldValue === undefined) {
// @tracked
let result = this.setDeep(changes, key, new Change(value), {
safeSet: this.safeSet
});
this[CHANGES] = result;
}
else if (keyInObject(changes, key)) {
// @tracked
// remove key if setting back to original
this[CHANGES] = this._deleteKey(CHANGES, key);
}
}
/**
* Increment or decrement the number of running validations for a
* given key.
*/
_setIsValidating(key, promise) {
let running = this[RUNNING_VALIDATIONS];
this.setDeep(running, key, promise);
}
/**
* Notifies virtual properties set on the changeset of a change.
* You can specify which keys are notified by passing in an array.
*
* @private
* @param {Array} keys
* @return {Void}
*/
_notifyVirtualProperties(keys) {
if (!keys) {
keys = this._rollbackKeys();
}
return keys;
}
/**
* Gets the changes and error keys.
*/
_rollbackKeys() {
let changes = this[CHANGES];
let errors = this[ERRORS];
return [...new Set([...keys(changes), ...keys(errors)])];
}
/**
* Deletes a key off an object and notifies observers.
*/
_deleteKey(objName, key = '') {
let obj = this[objName];
let keys = key.split('.');
if (keys.length === 1 && obj.hasOwnProperty(key)) {
delete obj[key];
}
else if (obj[keys[0]]) {
let [base, ...remaining] = keys;
let previousNode = obj;
let currentNode = obj[base];
let currentKey = base;
// find leaf and delete from map
while (this.isObject(currentNode) && currentKey) {
let curr = currentNode;
if (isChange(curr) || typeof curr.value !== 'undefined' || curr.validation) {
delete previousNode[currentKey];
}
currentKey = remaining.shift();
previousNode = currentNode;
if (currentKey) {
currentNode = currentNode[currentKey];
}
}
}
return obj;
}
_collectErrors() {
let validationKeys = keys(flattenValidations(this.validationMap));
return validationKeys.reduce((acc, key) => {
let content = this[CONTENT];
let value = this.getDeep(content, key);
let resolvedValue = value instanceof ObjectTreeNode ? value.unwrap() : value;
let result = this._validate(key, resolvedValue, null);
if (!this._isValidResult(result)) {
let errorResult = result;
let validationError = new Err(value, errorResult);
this.setDeep(acc, key, validationError, { safeSet: this.safeSet });
}
return acc;
}, {});
}
_isValidResult(result) {
return result === true || (Array.isArray(result) && result.length === 1 && result[0] === true);
}
get(key) {
// 'person'
// 'person.username'
let [baseKey, ...remaining] = key.split('.');
let changes = this[CHANGES];
let content = this[CONTENT];
if (Object.prototype.hasOwnProperty.call(changes, baseKey)) {
const changesValue = this.getDeep(changes, key);
const isObject = this.isObject(changesValue);
if (!isObject && changesValue !== undefined) {
// if safeGet returns a primitive, then go ahead return
return changesValue;
}
}
// At this point, we may have a changes object, a dot separated key, or a need to access the `key`
// on `this` or `content`
if (Object.prototype.hasOwnProperty.call(changes, baseKey) && hasChanges(changes)) {
let baseChanges = changes[baseKey];
// 'user.name'
const normalizedBaseChanges = normalizeObject(baseChanges);
if (this.isObject(normalizedBaseChanges)) {
const result = this.maybeUnwrapProxy(this.getDeep(normalizedBaseChanges, remaining.join('.')));
// need to do this inside of Change object
// basically anything inside of a Change object that is undefined means it was removed
if (typeof result === 'undefined' &&
pathInChanges(changes, key, this.safeGet) &&
!hasKey(changes, key, this.safeGet) &&
this.getDeep(content, key)) {
return;
}
if (this.isObject(result)) {
if (isChange(result)) {
return getChangeValue(result);
}
const baseContent = this.safeGet(content, baseKey) || {};
const subContent = this.getDeep(baseContent, remaining.join('.'));
const subChanges = getSubObject(changes, key);
// give back an object that can further retrieve changes and/or content
const tree = new ObjectTreeNode(subChanges, subContent, this.getDeep, this.isObject);
return tree.proxy;
}
else if (typeof result !== 'undefined') {
return result;
}
}
// this comes after the isObject check to ensure we don't lose remaining keys
if (isChange(baseChanges) && remaining.length === 0) {
return getChangeValue(baseChanges);
}
}
// return getters/setters/methods on BufferedProxy instance
if (baseKey in this || key in this) {
return this.getDeep(this, key);
}
const subContent = this.maybeUnwrapProxy(this.getDeep(content, key));
if (this.isObject(subContent)) {
let subChanges = this.getDeep(changes, key);
if (!subChanges) {
// if no changes, we need to add the path to the existing changes (mutate)
// so further access to nested keys works
subChanges = this.getDeep(this.setDeep(changes, key, {}), key);
}
// may still access a value on the changes or content objects
const tree = new ObjectTreeNode(subChanges, subContent, this.getDeep, this.isObject);
return tree.proxy;
}
else if (Array.isArray(subContent)) {
let subChanges = this.getDeep(changes, key);
if (!subChanges) {
// return array of contents. Dont need to worry about further access sibling keys in array case
return subContent;
}
if (isObject(subChanges)) {
if (isObject(subContent)) {
subChanges = normalizeObject(subChanges, this.isObject);
return Object.assign(Object.assign({}, subContent), subChanges);
}
else if (Array.isArray(subContent)) {
subChanges = normalizeObject(subChanges, this.isObject);
return objectToArray(mergeDeep(arrayToObject(subContent), subChanges));
}
}
return subChanges;
}
return subContent;
}
set(key, value) {
if (this.hasOwnProperty(key) || keyInObject(this, key)) {
this[key] = value;
}
else {
this.setUnknownProperty(key, value);
}
}
}
/**
* Creates new changesets.
*/
export function changeset(obj, validateFn, validationMap, options) {
return new BufferedChangeset(obj, validateFn, validationMap, options);
}
export class ValidatedChangeset {
/**
* Changeset factory class if you need to extend
*
* @class ValidatedChangeset
* @constructor
*/
constructor(obj, validateFn, validationMap, options) {
const c = changeset(obj, validateFn, validationMap, options);
return new Proxy(c, {
get(targetBuffer, key /*, receiver*/) {
const res = targetBuffer.get(key.toString());
return res;
},
set(targetBuffer, key, value /*, receiver*/) {
targetBuffer.set(key.toString(), value);
return true;
}
});
}
}
export function Changeset(obj, validateFn, validationMap, options) {
const c = changeset(obj, validateFn, validationMap, options);
return new Proxy(c, {
get(targetBuffer, key /*, receiver*/) {
const res = targetBuffer.get(key.toString());
return res;
},
set(targetBuffer, key, value /*, receiver*/) {
targetBuffer.set(key.toString(), value);
return true;
}
});
}
// determine if two values are equal
function isEqual(v1, v2) {
if (v1 instanceof Date && v2 instanceof Date) {
return v1.getTime() === v2.getTime();
}
return v1 === v2;
}
//# sourceMappingURL=index.js.map