transactional
Version:
Reactive objects with transactional updates and automatic serialization
323 lines (253 loc) • 9 kB
text/typescript
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;
}
};