Skip to content

Latest commit

 

History

History
 
 

README.md

Model API

Create and dispose

new Model( attrs?, options?)

Create the model. If no attrs is supplied, initialize it with defaults taken from the attributes definition.

When no default value is explicitly provided for an attribute, it's initialized as new AttributeType() (just AttributeType() for primitives). When the default value is provided and it's not compatible with the attribute type, the value is converted to the proper type with new Type( defaultValue ) call.

If {parse: true} option is set the attrs is assumed to be the JSON. In this case, model.parse( attr ) and attribute's parse hooks will be called to give you an option to transform the JSON.

@define class Book extends Model {
    static attributes = {
        title  : '',
        author : ''
    }
}

const book = new Book({
  title: "One Thousand and One Nights",
  author: "Scheherazade"
});

ModelClass.from(attrs, options?)

Create RecordClass from attributes. Similar to direct model creation, but supports additional option for strict data validation. If { strict : true } option is passed the model validation will be performed immediately and an exception will be thrown in case of an error.

Type-R always perform type checks on assignments, convert types, and reject improper updates reporting it as error. It won't, however, execute custom validation rules on every updates as validation is evaluated lazily. strict option will invoke custom validators and will throw on every error or warning instead of reporting them and continue.

// Fetch model with a given id.
const book = await Book.from({ id : 5 }).fetch();

// Validate the body of an incoming HTTP request.
// Throw an exception if validation fails.
const body = MyRequestBody.from( ctx.request.body, { parse : true, strict : true });

static ModelClass.create( attrs, options )

Static factory function used internally by Type-R to create instances of the model.

May be redefined in the abstract Model base class to make it serializable type.

@define class Widget extends Model {
    static attributes = {
        type : String
    }

    static create( attrs, options ){
        switch( attrs.type ){
            case "typeA" : return new TypeA( attrs, options );
            case "typeB" : return new TypeB( attrs, options );
        }
    }
}

@define class TypeA extends Widget {
    static attributes = {
        type : "typeA",
        ...
    }
}

@define class TypeB extends Widget {
    static attributes = {
        type : "typeB",
        ...
    }
}

model.clone()

Create the deep copy of the aggregation tree, recursively cloning all aggregated models and collections. References to shared members will be copied, but not shared members themselves.

callback model.initialize(attrs?, options?)

Called at the end of the Model constructor when all attributes are assigned and the model's inner state is properly initialized. Takes the same arguments as a constructor.

model.dispose()

Recursively dispose the model and its aggregated members. "Dispose" means that elements of the aggregation tree will unsubscribe from all event sources. It's crucial to prevent memory leaks in SPA.

The whole aggregation tree will be recursively disposed, shared members won't.

Read and update

model.cid

Read-only client-side model's identifier. Generated upon creation of the model and is unique for every model's instance. Cloned models will have different cid.

model.id

Predefined model's attribute, the id is an arbitrary string (integer id or UUID). id is typically generated by the server. It is used in JSON for id-references.

Records can be retrieved by id from collections, and there can be just one instance of the model with the same id in the particular collection.

model.attrName

Model's attributes can be directly accessed with their names as a regular class properties.

If the value is not compatible with attribute's type from the declaration on assignment, it is converted with Type( value ) call for primitive types, and with new Type( value ) for other types.

There is an important exception in type convertion logic for models and collections. Instead of applying a contructor, Type-R will try to update existing model and collection instances in place calling their set() method instead. This logic keeps the model and collection references stable and safe to pass around.

Model triggers events on changes:

  • change:attrName ( model, value ).
  • change ( model ).
Please note, that you *have to declare all attributes* in `static attributes` declaration.
@define class Book extends Model {
    static attributes = {
        title : String,
        author : String
        price : Number,
        publishedAt : Date,
        available : Boolean
    }
}

const myBook = new Book({ title : "State management with Type-R" });
myBook.author = 'Vlad'; // That works.
myBook.price = 'Too much'; // Converted with Number( 'Too much' ), resulting in NaN.
myBook.price = '123'; // = Number( '123' ).
myBook.publishedAt = new Date(); // Type is compatible, no conversion.
myBook.publishedAt = '1678-10-15 12:00'; // new Date( '1678-10-15 12:00' )
myBook.available = some && weird || condition; // Will always be Boolean. Or null.

model.set({ attrName : value, ... }, options? : options)

Bulk assign model's attributes using the same logic as attribute's assignment.

Model will trigger change:attrName ( model, value ) event per changed attribute and a single change ( model ) event at the end.

model.transaction(fun)

Execute the all changes made to the model in fun as single transaction triggering the single change event at the end.

All model updates occurs in the scope of transactions. Transaction is the sequence of changes which results in a single change event. Transaction can be opened either manually or implicitly with calling set() or assigning an attribute. Any additional changes made to the model in change:attr event handler will be executed in the scope of the original transaction, and won't trigger additional change events.

some.model.transaction( model => {
    model.a = 1; // `change:a` event is triggered.
    model.b = 2; // `change:b` event is triggered.
}); // `change` event is triggered.

Manual transactions with attribute assignments are superior to model.set() in terms of both performance and flexibility.

model.assignFrom(otherRecord)

Makes an existing model to be the full clone of otherRecord, recursively assigning all attributes. In contracts to model.clone(), the model is updated in place.

// Another way of doing the bestSeller.clone()
const book = new Book();
book.assignFrom(bestSeller);

Validation

Overview

Type-R supports validation API allowing developer to attach custom validation rules to attributes, models, and collections. Type-R validation mechanics based on following principles:

  • Validation happens transparently on the first access to the validation error. There's no special API to trigger the validation.
  • Validation is performed recursively on the aggregated models. If a model at the bottom of the model tree is not valid all its owners are not valid as well.
  • Validation results are cached across the models and collections, thus consequent validation error reads are cheap. Only changed models and collections will be validated again when necessary.

model.isValid( attr? )

When called without arguments, returns true if the model is valid having the same effect as !model.getValidationError().

When attr name is specified, returns true if the particular attribute is valid having the same effect as !model.getValidationError( attrName )

model.getValidationError( attrName? )

Return the validation error object for the model or the given attribute, or return null if there's no error.

When called without arguments and when the attribute is another model or collection the ValidationError object is returned which is an internal Type-R validation cache. It has the following shape:

{
    error : /* as returned from collection.validate() */,

    // Members validation errors.
    nested : {
        // key is an attrName for the model, and model.cid for the collcation
        key : validationError,
        ...
    }
}

callback model.validate()

Override this method to define model-level validation rules. Whatever is returned from validate() is treated as validation error.

Do not call this method directly, that's not the way how validation works.

model.eachValidationError( iteratee : ( error, key, obj ) => void )

Recursively traverse validation errors in all aggregated models.

iteratee is a function taking following arguments:

  • error is the value of the error as specified at type( T ).check( validator, error ) or returned by validate() callback.
  • obj is the reference to the current model or collection having an error.
  • key is:
    • an attribute name for a model.
    • model.id for collection.
    • null for the object-level validation error returned by validate().

I/O

model.isNew()

Has this model been saved to the server yet? If the model does not yet have an id, it is considered to be new.

async model.fetch( options? )

Asynchronously fetch the model using endpoint.read() method. Returns an abortable ES6 promise.

An endpoint must be defined for the model in order to use that method.

async model.save( options? )

Asynchronously save the model using endpoint.create() (if there are no id) or endpoint.update() (if id is present) method. Returns an abortable ES6 promise.

An endpoint must be defined for the model in order to use that method.

async model.destroy( options? )

Asynchronously destroy the model using endpoint.destroy() method. Returns an abortable ES6 promise. The model is removed from the aggregating collection upon the completion of the I/O request.

An endpoint must be defined for the model in order to use that method.

model.hasPendingIO()

Returns an promise if there's any I/O pending with the object, or null otherwise. Can be used to check for active I/O in progress.

model.getEndpoint()

Returns an model's IO endpoint. Normally, this is an endpoint which is defined in object's static endpoint = ... declaration, but it might be overridden by the parent's model using type( Type ).endpoint( ... ) attribute declaration.

@define class User extends Model {
    static endpoint = restfulIO( '/api/users' );
    ...
}

@define class UserRole extends Model {
    static endpoint = restfulIO( '/api/roles' );
    static attributes = {
        // Use the relative path '/api/roles/:id/users'
        users : type( User.Collection ).endpoint( restfulIO( './users' ) ),
        ...
    }
}

model.toJSON( options? )

Serialize model or collection to JSON. Used internally by save() I/O method (options.ioMethod === 'save' when called from within save()). Can be overridden to customize serialization.

Produces the JSON for the given model or collection and its aggregated members. Aggregation tree is serialized as nested JSON. Model corresponds to an object in JSON, while the collection is represented as an array of objects.

If you override toJSON(), it usually means that you must override parse() as well, and vice versa.

Serialization can be controlled on per-attribute level with type( Type ).toJSON() declaration.
@define class Comment extends Model {
    static attributes = {
        body : ''
    }
}

@define class BlogPost extends Model {
    static attributes = {
        title : '',
        body : '',
        comments : Comment.Collection
    }
}

const post = new BlogPost({
    title: "Type-R is cool!",
    comments : [ { body : "Agree" }]
});

const rawJSON = post.toJSON()
// { title : "Type-R is cool!", body : "", comments : [{ body : "Agree" }] }

option { parse : true }

obj.set() and constructor's option to force parsing of the raw JSON. Is used internally by I/O methods to parse the data received from the server.

// Another way of doing the bestSeller.clone()
// Amazingly, this is guaranteed to work by default.
const book = new Book();
book.set( bestSeller.toJSON(), { parse : true } );

callback model.parse( json, options? )

Optional hook called to transform the JSON when it's passes to the model or collection with set( json, { parse : true }) call. Used internally by I/O methods (options.ioMethod is either "save" or "fetch" when called from I/O method).

If you override toJSON(), it usually means that you must override parse() as well, and vice versa.

Parsing can be controlled on per-attribute level with type( Type ).parse() declaration.

Change events

Type-R implements deeply observable changes on the object graph constructed of models and collection.

All of the model and collection updates happens in a scope of the transaction followed by the change event. Every model or collection update operation opens implicit transaction. Several update operations can be groped to the single explicit transaction if executed in the scope of the obj.transaction() or col.updateEach() call.

@define class Author extends Model {
    static attributes = {
        name : ''
    }
}

@define class Book extends Model {
    static attributes = {
        name : '',
        datePublished : Date,
        author : Author
    }
}

const book = new Book();
book.on( 'change', () => console.log( 'Book is changed') );

// Implicit transaction, prints to the console
book.author.name = 'John Smith';

Events mixin methods (7)

Model implements Events mixin.

event "change" ( model )

Triggered by the model at the end of the attributes update transaction in case if there were any changes applied.

event "change:attrName" ( model, value )

Triggered by the model during the attributes update transaction for every changed attribute.

model.changed

The changed property is the internal hash containing all the attributes that have changed during its last transaction. Please do not update changed directly since its state is internally maintained by set(). A copy of changed can be acquired from changedAttributes().

model.changedAttributes( attrs? )

Retrieve a hash of only the model's attributes that have changed during the last transaction, or false if there are none. Optionally, an external attributes hash can be passed in, returning the attributes in that hash which differ from the model. This can be used to figure out which portions of a view should be updated, or what calls need to be made to sync the changes to the server.

model.previous( attr )

During a "change" event, this method can be used to get the previous value of a changed attribute.

@define class Person extends Model{
    static attributes = {
        name: ''
    }
}

const bill = new Person({
  name: "Bill Smith"
});

bill.on("change:name", ( model, name ) => {
  alert( `Changed name from ${ bill.previous('name') } to ${ name }`);
});

bill.name = "Bill Jones";

model.previousAttributes()

Return a copy of the model's previous attributes. Useful for getting a diff between versions of a model, or getting back to a valid state after an error occurs.

Other

model.getOwner()

If the model is an nested in an aggregated attribute return the owner model or null otherwise. If the model is a member of an Collection.of( ModelType ), the collection will be bypassed and the owner of the collection will be returned.

model.collection

If the model is a member of a some Collection.of( ModelType ) return this collection or null otherwise.