UNPKG

@type-r/models

Version:

The serializable type system for JS and TypeScript

424 lines (288 loc) 16.1 kB
# 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. ```javascript @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. ```javascript // 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. ```javascript @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 )*. <aside class="warning">Please note, that you *have to declare all attributes* in `static attributes` declaration.</aside> ```javascript @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. ```javascript 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. ```javascript // 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: ```javascript { 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. <aside class="notice">Do not call this method directly, that's not the way how validation works.</aside> ### 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. ```javascript @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. <aside class="notice"> Serialization can be controlled on per-attribute level with <b>type( Type ).toJSON()</b> declaration. </aside> ```javascript @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. ```javascript // 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. <aside class="notice"> Parsing can be controlled on per-attribute level with <b>type( Type ).parse()</b> declaration. </aside> ## 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. ```javascript @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](#events-mixin) 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. ```javascript @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.