@type-r/models
Version:
The serializable type system for JS and TypeScript
286 lines (220 loc) • 10.2 kB
text/typescript
import { IOEndpoint } from '../../io-tools';
import { LogLevel, tools, Logger } from '@type-r/mixture';
import { TransactionOptions, Transactional } from '../../transactions';
import { AttributesContainer, AttributeUpdatePipeline, ModelTransaction, setAttribute } from '../updates';
const { notEqual, assign} = tools;
export type Transform = ( this : AnyType, next : any, prev : any, record : AttributesContainer, options : TransactionOptions ) => any;
export type ChangeHandler = ( this : AnyType, next : any, prev : any, record : AttributesContainer, options : TransactionOptions ) => void;
export interface AttributeOptions {
_metatype? : typeof AnyType
validate? : ( record : AttributesContainer, value : any, key : string ) => any
isRequired? : boolean
changeEvents? : boolean
endpoint? : IOEndpoint
type? : Function
value? : any
hasCustomDefault? : boolean
parse? : Parse
toJSON? : AttributeToJSON
getHooks? : GetHook[]
transforms? : Transform[]
changeHandlers? : ChangeHandler[]
_onChange? : ChangeAttrHandler
}
export type Parse = ( value : any, key? : string ) => any;
export type GetHook = ( value : any, key? : string ) => any;
export type AttributeToJSON = ( value : any, key? : string ) => any
export type AttributeParse = ( value : any, key? : string ) => any
export type ChangeAttrHandler = ( ( value : any, attr? : string ) => void ) | string;
// TODO: interface differs from options, do something obout it
const emptyOptions : TransactionOptions = {};
/**
* Typeless attribute. Is the base class for all other attributes.
*/
export class AnyType implements AttributeUpdatePipeline {
// Factory method to create attribute from options
static create : ( options : AttributeOptions, name : string ) => AnyType;
/**
* Update pipeline functions
* =========================
*
* Stage 0. canBeUpdated( value )
* - presence of this function implies attribute's ability to update in place.
*/
canBeUpdated( prev, next, options : TransactionOptions ) : any {}
/**
* Stage 1. Transform stage
*/
transform( next : any, prev : any, model : AttributesContainer, options : TransactionOptions ) : any { return next; }
// convert attribute type to `this.type`.
convert( next : any, prev : any, model : AttributesContainer, options : TransactionOptions ) : any { return next; }
/**
* Stage 2. Check if attr value is changed
*/
isChanged( a : any, b : any ) : boolean {
return notEqual( a, b );
}
/**
* Stage 3. Handle attribute change
*/
handleChange( next : any, prev : any, model : AttributesContainer, options : TransactionOptions ) {}
/**
* End update pipeline definitions.
*/
// create empty object passing backbone options to constructor...
create() { return void 0; }
// generic clone function for typeless attributes
// Must be overriden in sublass
clone( value : any, record : AttributesContainer ) {
return value;
}
dispose( record : AttributesContainer, value : any ) : void {
this.handleChange( void 0, value, record, emptyOptions );
}
validate( record : AttributesContainer, value : any, key : string ) : any {}
toJSON( value, key, options? : object ) {
return value && value.toJSON ? value.toJSON( options ) : value;
}
isMutableType(){
return this.type && this.type.prototype instanceof Transactional;
}
createPropertyDescriptor() : PropertyDescriptor | void {
const { name, getHook } = this;
if( name !== 'id' ){
return {
// call to optimized set function for single argument.
set( value ){
setAttribute( this, name, value );
},
// attach get hook to the getter function, if it present
get : (
getHook ?
function() {
return getHook.call( this, this.attributes[ name ], name );
} :
function() { return this.attributes[ name ]; }
),
configurable : true
}
}
}
value : any
// Used as global default value for the given metatype
static defaultValue : any;
type : Function
initialize( name : string, options : TransactionOptions ){}
options : AttributeOptions
doInit( value, record : AttributesContainer, options : TransactionOptions ){
const v = value === void 0 ? this.defaultValue() : value,
x = this.transform( v, void 0, record, options );
this.handleChange( x, void 0, record, options );
return x;
}
doUpdate( value, record : AttributesContainer, options : TransactionOptions, nested? : ModelTransaction[] ){
const { name } = this,
{ attributes } = record,
prev = attributes[ name ];
const next = this.transform( value, prev, record, options );
attributes[ name ] = next;
if( this.isChanged( next, prev ) ) {
// Do the rest of the job after assignment
this.handleChange( next, prev, record, options );
return true;
}
return false;
}
propagateChanges : boolean
protected _log( level : LogLevel, code : string, text : string, value, record : AttributesContainer, logger : Logger ){
record._log( level, code, `${record.getClassName()}.${ this.name } ${ text }`, {
'New value' : value,
'Prev. value' : record.attributes[ this.name ]
}, logger );
}
defaultValue(){
return this.value;
}
constructor( public name : string, a_options : AttributeOptions ) {
// Save original options...
this.options = a_options;
// Clone options.
const options : AttributeOptions = { getHooks : [], transforms : [], changeHandlers : [], ...a_options };
options.getHooks = options.getHooks.slice();
options.transforms = options.transforms.slice();
options.changeHandlers = options.changeHandlers.slice();
const {
value, type, parse, toJSON, changeEvents,
validate, getHooks, transforms, changeHandlers
} = options;
// Initialize default value...
this.value = value;
this.type = type;
// TODO: An opportunity to optimize for attribute subtype.
if( !options.hasCustomDefault && type ){
this.defaultValue = this.create;
}
else if( tools.isValidJSON( value ) ){
// JSON literals must be deep copied.
this.defaultValue = new Function( `return ${ JSON.stringify( value ) };` ) as any;
}
else{
this.defaultValue = this.defaultValue;
}
// Changes must be bubbled when they are not disabled for an attribute and transactional object.
this.propagateChanges = changeEvents !== false;
this.toJSON = toJSON === void 0 ? this.toJSON : toJSON;
this.validate = validate || this.validate;
if( options.isRequired ){
this.validate = wrapIsRequired( this.validate );
}
/**
* Assemble pipelines...
*/
// `convert` is default transform, which is always present...
transforms.unshift( this.convert );
// Get hook from the attribute will be used first...
if( this.get ) getHooks.unshift( this.get );
// let subclasses configure the pipeline...
this.initialize.call( this, options );
// let attribute spec configure the pipeline...
if( getHooks.length ){
const getHook = this.getHook = getHooks.reduce( chainGetHooks );
const { validate } = this;
this.validate = function( record : AttributesContainer, value : any, key : string ){
return validate.call( this, record, getHook.call( record, value, key ), key );
}
}
this.transform = transforms.length ? transforms.reduce( chainTransforms ) : this.transform;
this.handleChange = changeHandlers.length ? changeHandlers.reduce( chainChangeHandlers ) : this.handleChange;
// Attribute-level parse transform are attached as update hooks modifiers...
const { doInit, doUpdate } = this;
this.doInit = parse ? function( value, record : AttributesContainer, options : TransactionOptions ){
return doInit.call( this, options.parse && value !== void 0 ? parse.call( record, value, this.name ) : value, record, options );
} : doInit;
this.doUpdate = parse ? function( value, record : AttributesContainer, options : TransactionOptions, nested? : ModelTransaction[] ){
return doUpdate.call( this, options.parse && value !== void 0 ? parse.call( record, value, this.name ) : value, record, options, nested );
} : doUpdate;
}
getHook : ( value, key : string ) => any = null
get : ( value, key : string ) => any
}
function chainGetHooks( prevHook : GetHook, nextHook : GetHook ) : GetHook {
return function( value, name ) {
return nextHook.call( this, prevHook.call( this, value, name ), name );
}
}
function chainTransforms( prevTransform : Transform, nextTransform : Transform ) : Transform {
return function( next, prev, record, options ) {
return nextTransform.call( this, prevTransform.call( this, next, prev, record, options ), prev, record, options );
}
}
function chainChangeHandlers( prevHandler : ChangeHandler, nextHandler : ChangeHandler ) : ChangeHandler {
return function( next, prev, record, options ) {
prevHandler.call( this, next, prev, record, options );
nextHandler.call( this, next, prev, record, options );
}
}
function wrapIsRequired( validate ){
return function( record : AttributesContainer, value : any, key : string ){
return value ? validate.call( this, record, value, key ) : 'Required';
}
}