UNPKG

transactional

Version:

Reactive objects with transactional updates and automatic serialization

323 lines (253 loc) 9 kB
import { RecordMixin, setAttribute } from './transactions.ts' import compile from './compile.ts' import { Class } from '../class.ts' import { assign } from '../tools.ts' import {CollectionConstructor, IRecord, RecordConstructor} from '../types.ts' let _cidCount = 0; class Attributes {} export class Record extends Class implements IRecord { static Collection : CollectionConstructor static define( spec ) { const baseProto = Object.getPrototypeOf( this.prototype ), BaseCtor : RecordConstructor = baseProto.constructor; // Create collection if( this.Collection === BaseCtor.Collection ) { this.Collection = class Collection extends BaseCtor.Collection {}; this.Collection.prototype.Record = this; } if( spec ) { // define stuff const { attributes } = spec, compiled = defaults( compile( attributes, baseProto._attributes ), spec ); assign( compiled.properties, spec.properties ); super.define( compiled ); const { collection } = spec; if( collection ) { if( typeof collection === 'function' ) { // Link model to collection this.Collection = collection; this.Collection.prototype.Record = this; } else { // Configure our local Collection this.Collection.define( collection ); } } } } attributes : {} /** * Construction and cloning */ constructor( attributes, opts ) { super(); var attrs = this.__attributes, values = attributes || {}, options = opts || {}; this.__duringSet = 0; this._changing = this._pending = false; this._changeToken = {}; this.attributes = {}; this.cid = this.cidPrefix + _cidCount++; // Make owner accessible in initialize if( this._owner = options.owner ) { // do not pass it to nested objects. // No side effect here, options copied at the upper level in this case options.owner = null; } if( options.parse ) { values = this.parse( values, options ) || {}; } if( values && Object.getPrototypeOf( values ) !== Object.prototype ) { error.argumentIsNotAnObject( this, values ); values = {}; } values = options.deep ? deepCloneAttrs( this, values ) : this.defaults( values ); // Execute attributes transform function instead of this.set this.forEachAttr( values, ( key, value ) => { const attr = attrs[ key ]; if( attr ) { const next = values[ key ] = attr.transform( value, options, this, key ); attr.handleChange( next ); } else { error.unknownAttribute( model, key, value ); } } ); this._previousAttributes = this.attributes = values; this.initialize.apply( this, arguments ); } initialize(){} defaults( attrs, options ) { return new this.Attributes( attrs ); } clone( options = { deep : true } ) : this { return new (this.constructor)( this.attributes, options ); } /** * Attributes handling and ownership */ Attributes : new ( attrs : {} ) => {}; forEachAttr( obj, fun ) { } get id() { // (!) No get hooks on id attribute. const { idAttribute } = this; return idAttribute && this.attributes[ idAttribute ]; } set id( value ) { const { idAttribute } = this; idAttribute && setAttribute( this, idAttribute, value ); } get collection() { return ( !this._ownerKey && this._owner ) || null; } getOwner() { const { _owner } = this; return this._ownerKey ? _owner : ( _owner && _owner._owner ); } /** * Object sync API * set( { attrs }, options ) */ set( values, options ) { if( values ) { if( Object.getPrototypeOf( values ) === Object.prototype ) { this.createTransaction( values, options ).commit( options ); } else { // TODO: log.error('Model.set argument must be string or object'); } } return this; } /** * Transactional API stubs (provided by separate mixin) */ createTransaction( values, options ) {} transaction( fun, options ) {} _onChildrenChange( child, options = {} ) { this.forceChange( child._ownerAttr, options ); } forceChange( key, options = {} ){ const isRoot = begin( this ); if( !options.silent ){ this._pending = options; key && this._notifyChangeAttr( key, options ); } isRoot && commit( this, options ); } /** * Events system stubs */ _notifyChange( options ) { this._changeToken = {}; this.trigger( 'change', this, options ); } _notifyChangeAttr( name, options ) { this.trigger( 'change:' + name, this.attributes[ name ], this, options ); } /** * Serialization API * toJSON(), parse( data ) */ toJSON() { var self = this, res = {}, attrSpecs = this.__attributes; this.forEachAttr( this.attributes, function( value, key ) { var attrSpec = attrSpecs[ key ], toJSON = attrSpec && attrSpec.toJSON; if( toJSON ) { res[ key ] = toJSON.call( self, value, key ); } } ); return res; } parse( resp ) { return this._parse( resp ); } _parse( resp ) { return resp; } /** * Changes tracking API * hasChanges( attr ), changedAttributes( diff ), previousAttributes() */ get changed() { let changed = this._changed; if( !changed ) { changed = this._changed = {}; const { attributes, _previousAttributes } = this; this.forEachAttr( this._attributes, ( attr, key ) => { const curr = attributes[ key ], prev = _previousAttributes[ key ]; if( attr.isChanged( curr, prev ) ) { changed[ key ] = curr; } } ); } return changed; } hasChanged( attr ) { if( attr == null ) { return !_.isEmpty( this.changed ); //TODO: remove underscore. } return this._attributes[ attr ].isChanged( this.attributes[ attr ], this._previousAttributes[ attr ] ); } changedAttributes( diff ) { if( !diff ) { return this.hasChanged() ? _.clone( this.changed ) : false; } var val, changed = false, old = this._changing ? this._previousAttributes : this.attributes, attrSpecs = this._attributes; for( var attr in diff ) { if( !attrSpecs[ attr ].isChanged( old[ attr ], ( val = diff[ attr ] ) ) ) { continue; } (changed || (changed = {}))[ attr ] = val; } return changed; } previousAttributes() { return new this.Attributes( this._previousAttributes ); } } assign( Record.prototype, RecordMixin ); const s = { // extend Model and its Collection extend : function( protoProps, staticProps ) { var Child; if( typeof protoProps === 'function' ) { Child = protoProps; protoProps = null; } else if( protoProps && protoProps.hasOwnProperty( 'constructor' ) ) { Child = protoProps.constructor; } else { var Parent = this; Child = function Model( attrs, options ) { return Parent.call( this, attrs, options ); }; } var This = Object.extend.call( this, Child ); This.Collection = this.Collection.extend(); return protoProps ? This.define( protoProps, staticProps ) : This; } , // define Model and its Collection. All the magic starts here. define : function( protoProps, staticProps ) { var Base = Object.getPrototypeOf( this.prototype ).constructor, spec = createDefinition( protoProps, Base ), This = this; Object.extend.Class.define.call( This, spec, staticProps ); attachMixins( This ); // define Collection var collectionSpec = { model : This }; spec.urlRoot && ( collectionSpec.url = spec.urlRoot ); This.Collection.define( _.defaults( protoProps.collection || {}, collectionSpec ) ); return This; } };