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 } from './utils/array-object' ; import { Changes , Config , Content , Errors , IErr , IChangeset , INotifier , InternalMap , NewProperty , PrepareChangesFn , RunningValidations , Snapshot , ValidationErr , ValidationResult , ValidatorAction , ValidatorMap } from './types' ; export { CHANGESET , Change , Err , 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 implements IChangeset { constructor ( obj , public validateFn = defaultValidatorFn , public validationMap = { } , options = { } ) { this [ CONTENT ] = obj ; this [ PREVIOUS_CONTENT ] = undefined ; this [ CHANGES ] = { } ; this [ ERRORS ] = { } ; this [ ERRORS_CACHE ] = { } ; this [ VALIDATOR ] = validateFn ; this [ OPTIONS ] = pureAssign ( defaultOptions , JSON . parse ( JSON . stringify ( options ) ) ) ; this [ RUNNING_VALIDATIONS ] = { } ; } /**
     * Any property that is not one of the getter/setter/methods on the
     * BufferedProxy instance. The value type is `unknown` in order to avoid
     * having to predefine key/value pairs of the correct types in the target
     * object. Setting the index signature to `[key: string]: T[K]` would allow us
     * to typecheck the value that is set on the proxy. However, no
     * getters/setters/methods can be added to the class. This is the tradeoff
     * we make for this particular design pattern (class based BufferedProxy).
     */ [ key ] : unknown ; [ CONTENT ] : object ; [ PREVIOUS_CONTENT ] : object | undefined ; [ CHANGES ] : Changes ; [ ERRORS ] : Errors < any > ; [ ERRORS_CACHE ] : Errors < any > ; [ VALIDATOR ] : ValidatorAction ; [ OPTIONS ] : Config ; [ RUNNING_VALIDATIONS ] : { } ; __changeset__ = CHANGESET ; _eventedNotifiers = { } ; 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 @override
      isObject = isObject ; property @override
      maybeUnwrapProxy = maybeUnwrapProxy ; property @override
      setDeep = setDeep ; property @override
      getDeep = getDeep ; property @override
      mergeDeep = mergeDeep ; property @override
      safeGet ( obj , key ) { return obj [ key ] ; } property @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 type { Array } get changes ( ) { let obj = this [ CHANGES ] ; return getKeyValues ( obj ) ; } property type { Array } get errors ( ) { let obj = this [ ERRORS ] ; 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 type { Array } get isValid ( ) { return getKeyErrorValues ( this [ ERRORS ] ) . length === 0 ; } property 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 type { Boolean } get isInvalid ( ) { return ! this . isValid ; } property type { Boolean } get isDirty ( ) { return ! this . isPristine ; } method setUnknownProperty < T > ( 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 ] ( ) : string { let normalisedContent = pureAssign ( this [ CONTENT ] , { } ) ; return `changeset:${ normalisedContent . toString ( ) }` ; } /**
     * String representation for the changeset.
     */ toString ( ) { let normalisedContent = pureAssign ( this [ CONTENT ] , { } ) ; return `changeset:${ normalisedContent . toString ( ) }` ; } method 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 as Record < string , any > ) . reduce ( ( newObj , key ) => { newObj [ key ] = new Change ( ( preparedChanges as Record < string , any > ) [ key ] ) ; return newObj ; } , newObj ) ; this [ CHANGES ] = newChanges ; } return this ; } method execute ( ) { let oldContent ; if ( this . isValid && this . isDirty ) { let content = this [ CONTENT ] ; let changes = this [ CHANGES ] ; oldContent = buildOldValues ( content , this . changes , this . getDeep ) ; this [ CONTENT ] = this . mergeDeep ( content , changes ) ; } 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 ; } method param { Object } options 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' ) { let maybePromise = this . maybeUnwrapProxy ( content ) . save ( ) ; if ( maybePromise ) { savePromise = maybePromise ; } } try { const result = await savePromise ; this . rollback ( ) ; return result ; } catch ( e ) { throw e ; } } method 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 ] ; let newChangeset = new ValidatedChangeset ( content , this [ VALIDATOR ] ) ; 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 ; } method rollback ( ) { let keys = this . _rollbackKeys ( ) ; this [ CHANGES ] = { } ; this [ ERRORS ] = { } ; this [ ERRORS_CACHE ] = { } ; this . _notifyVirtualProperties ( keys ) ; this . trigger ( AFTER_ROLLBACK_EVENT ) ; return this ; } public chainable method param { String } key return { Changeset } rollbackInvalid ( key ) { let errorKeys = keys ( this [ ERRORS ] ) ; if ( key ) { this . _notifyVirtualProperties ( [ key ] ) ; this [ ERRORS ] = this . _deleteKey ( ERRORS , key ) as Errors < any > ; this [ ERRORS_CACHE ] = this [ ERRORS ] ; if ( errorKeys . indexOf ( key ) > - 1 ) { this [ CHANGES ] = this . _deleteKey ( CHANGES , key ) as Changes ; } } else { this . _notifyVirtualProperties ( ) ; this [ ERRORS ] = { } ; this [ ERRORS_CACHE ] = this [ ERRORS ] ; errorKeys . forEach ( errKey => { this [ CHANGES ] = this . _deleteKey ( CHANGES , errKey ) as Changes ; } ) ; } return this ; } public chainable method param { String } key return { Changeset } rollbackProperty ( key ) { this [ CHANGES ] = this . _deleteKey ( CHANGES , key ) as Changes ; this [ ERRORS ] = this . _deleteKey ( ERRORS , key ) as Errors < any > ; this [ ERRORS_CACHE ] = this [ ERRORS ] ; return this ; } method async validate ( ... validationKeys ) { if ( keys ( this . validationMap as object ) . length === 0 && ! validationKeys . length ) { return Promise . resolve ( null ) ; } validationKeys = validationKeys . length > 0 ? validationKeys : keys ( flattenValidations ( this . validationMap as object ) ) ; 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 ) ; } method addError < T > ( key , error ) { let newError ; const isIErr = < T > ( error ) : error is IErr < T > => 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 as ValidationErr ) ; } let errors = this [ ERRORS ] ; this [ ERRORS ] = this . setDeep ( errors , key , newError , { safeSet : this . safeSet } ) ; this [ ERRORS_CACHE ] = this [ ERRORS ] ; return error ; } method 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 ) ; this [ ERRORS ] = this . setDeep ( errors , key as string , newError , { safeSet : this . safeSet } ) ; this [ ERRORS_CACHE ] = this [ ERRORS ] ; return { value , validation } ; } method snapshot ( ) { let changes = this [ CHANGES ] ; let errors = this [ ERRORS ] ; return { changes : keys ( changes ) . reduce ( ( newObj , key ) => { newObj [ key ] = getChangeValue ( changes [ key ] ) ; return newObj ; } , { } ) , errors : keys ( errors ) . reduce ( ( newObj , key ) => { let e = errors [ key ] ; newObj [ key ] = { value : e . value , validation : e . validation } ; return newObj ; } , { } ) } ; } method restore ( { changes , errors } ) { let newChanges = keys ( changes ) . reduce ( ( newObj , key ) => { newObj [ key ] = new Change ( changes [ key ] ) ; return newObj ; } , { } ) ; let newErrors = keys ( errors ) . reduce ( ( newObj , key ) => { let e = errors [ key ] ; newObj [ key ] = new Err ( e . value , e . validation ) ; return newObj ; } , { } ) ; this [ CHANGES ] = newChanges ; this [ ERRORS ] = newErrors ; this [ ERRORS_CACHE ] = this [ ERRORS ] ; this . _notifyVirtualProperties ( ) ; return this ; } method 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 ) ; this [ CHANGES ] = casted ; return this ; } method isValidating ( key ? ) { let runningValidations = this [ RUNNING_VALIDATIONS ] ; let ks = keys ( runningValidations ) ; if ( key ) { return ks . includes ( key ) ; } return ks . length > 0 ; } method private _validateKey < T > ( 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 ) ; if ( isPromise ( validation ) ) { this . _setIsValidating ( key , validation as Promise < ValidationResult > ) ; let running = this [ RUNNING_VALIDATIONS ] ; let promises = Object . entries ( running ) ; return Promise . all ( promises ) . then ( ( ) => { return ( validation as Promise < ValidationResult > ) . 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 as ValidationResult , { key , value } ) ; this . trigger ( AFTER_VALIDATION_EVENT , key ) ; return result ; } method private _handleValidation < T > ( validation , { key , value } ) { const isValid = validation === true || ( Array . isArray ( validation ) && validation . length === 1 && validation [ 0 ] === true ) ; this [ ERRORS ] = this . _deleteKey ( ERRORS_CACHE , key ) as Errors < any > ; if ( ! isValid ) { return this . addError ( key , { value , validation } as IErr < T > ) ; } return value ; } method 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 ) { return true ; } return validationResult ; } return true ; } /**
     * Sets property on the changeset.
     */ _setProperty < T > ( { key , value , oldValue } ) { let changes = this [ CHANGES ] ; if ( ! isEqual ( value , oldValue ) || oldValue === undefined ) { let result = this . setDeep ( changes , key , new Change ( value ) , { safeSet : this . safeSet } ) ; this [ CHANGES ] = result ; } else if ( keyInObject ( changes , key ) ) { this [ CHANGES ] = this . _deleteKey ( CHANGES , key ) as Changes ; } } /**
     * 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 ) ; } 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 ] as InternalMap ; 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 ; 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 ; } get ( key ) { 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 ) { return changesValue ; } } if ( Object . prototype . hasOwnProperty . call ( changes , baseKey ) && hasChanges ( changes ) ) { let baseChanges = changes [ baseKey ] ; const normalizedBaseChanges = normalizeObject ( baseChanges ) ; if ( this . isObject ( normalizedBaseChanges ) ) { const result = this . maybeUnwrapProxy ( this . getDeep ( normalizedBaseChanges , remaining . join ( '.' ) ) ) ; 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 ) ; const tree = new ObjectTreeNode ( subChanges , subContent , this . getDeep , this . isObject ) ; return tree . proxy ; } else if ( typeof result !== 'undefined' ) { return result ; } } if ( isChange ( baseChanges ) && remaining . length === 0 ) { return getChangeValue ( baseChanges ) ; } } 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 ) { subChanges = this . getDeep ( this . setDeep ( changes , key , { } ) , key ) ; } 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 subContent ; } if ( isObject ( subChanges ) ) { if ( isObject ( subContent ) ) { subChanges = normalizeObject ( subChanges , this . isObject ) ; return { ... subContent , ... subChanges } ; } else if ( Array . isArray ( subContent ) ) { subChanges = normalizeObject ( subChanges , this . isObject ) ; return objectToArray ( mergeDeep ( arrayToObject ( subContent ) , subChanges ) ) ; } } return subChanges ; } return subContent ; } set < T > ( 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 ) ; } type T20 = InstanceType < typeof BufferedChangeset > ; export class ValidatedChangeset { class constructor constructor ( obj , validateFn ? , validationMap ? , options ? ) { const c = changeset ( obj , validateFn , validationMap , options ) ; return new Proxy ( c , { get ( targetBuffer , key ) { const res = targetBuffer . get ( key . toString ( ) ) ; return res ; } , set ( targetBuffer , key , value ) { targetBuffer . set ( key . toString ( ) , value ) ; return true ; } } ) as T20 ; } } export function Changeset ( obj , validateFn ? , validationMap ? , options ? ) { const c = changeset ( obj , validateFn , validationMap , options ) ; return new Proxy ( c , { get ( targetBuffer , key ) { const res = targetBuffer . get ( key . toString ( ) ) ; return res ; } , set ( targetBuffer , key , value ) { targetBuffer . set ( key . toString ( ) , value ) ; return true ; } } ) ; } function isEqual ( v1 , v2 ) { if ( v1 instanceof Date && v2 instanceof Date ) { return v1 . getTime ( ) === v2 . getTime ( ) ; } return v1 === v2 ; }