Batman.Model
For a general explanation of Batman.Model
and it works, see the guide.
Note: This documentation uses the term model to refer to the class Model
or a Model
subclass, and the term record to refer to one instance of a
model.
-
@primaryKey : string
primaryKey
is a class level configuration option to change which key batman.js uses as the primary key. Change the option usingset
, like so:test 'primary key can be set using @set', -> class Shop extends Batman.Model @set 'primaryKey', 'shop_id' equal Shop.get('primaryKey'), 'shop_id'
The
primaryKey
is what batman.js uses to compare instances to see if they represent the same domain-level object: if two records have the same value at the key specified byprimaryKey
, only one will be in the identity map. The key specified byprimaryKey
is also used by the associations system when determining if a record is related to another record, and by the remote storage adapters to generate URLs for records.Note: The default primaryKey is 'id'.
-
@storageKey : string
storageKey
is a class level option which gives the storage adapters something to interpolate into their specific key generation schemes. In the case ofLocalStorage
orSessionStorage
adapters, thestorageKey
defines what namespace to store this record under in thelocalStorage
orsessionStorage
host objects, and with the case of theRestStorage
family of adapters, thestorageKey
assists in URL generation. See the documentation for the storage adapter of your choice for more information.The default
storageKey
isnull
. -
@persist(mechanism : StorageAdapter) : StorageAdapter
@persist
is how aModel
subclass is told to persist itself by means of aStorageAdapter
.@persist
accepts either aStorageAdapter
class or instance and will return either the instantiated class or the instance passed to it for further modification.test 'models can be told to persist via a storage adapter', -> class Shop extends Batman.Model @resourceName: 'shop' @persist TestStorageAdapter record = new Shop ok record.hasStorage() test '@persist returns the instantiated storage adapter', -> adapter = false class Shop extends Batman.Model @resourceName: 'shop' adapter = @persist TestStorageAdapter ok adapter instanceof Batman.StorageAdapter test '@persist accepts already instantiated storage adapters', -> adapter = new Batman.StorageAdapter adapter.someHandyConfigurationOption = true class Shop extends Batman.Model @resourceName: 'shop' @persist adapter record = new Shop ok record.hasStorage()
-
@encode(keys...[, encoderObject : [Object|Function]])
@encode
specifies a list ofkeys
a model should expect from and send back to a storage adapter, and any transforms to apply to those attributes as they enter and exit the world of batman.js in the optionalencoderObject
.The
encoderObject
should have anencode
and/or adecode
key which point to functions. The functions accept the "raw" data (the batman.js land value in the case ofencode
, and the backend land value in the case ofdecode
), and should return the data suitable for the other side of the link. The functions should have the following signatures:encoderObject = { encode: (value, key, builtJSON, record) -> decode: (value, key, incomingJSON, outgoingObject, record) -> }
By default these functions are the identity functions. They apply no transformation. The arguments for
encode
functions are as follows:value
is the client side value of thekey
on therecord
key
is the key which thevalue
is stored under on therecord
. This is useful when passing the sameencoderObject
which needs to pivot on what key is being encoded to different calls toencode
.builtJSON
is the object which is modified by each encoder which will eventually be returned bytoJSON
. To send the server the encoded value under a different key than thekey
, modify this object by putting the value under the desired key, and returnundefined
.record
is the record on whichtoJSON
has been called.
For
decode
functions:value
is the server side value of thekey
which will end up on therecord
.key
is the key which thevalue
is stored under in the incoming JSON.incomingJSON
is the JSON which is being decoded into therecord
. This can be used to create compound key decoders.outgoingObject
is the object which is built up by the decoders and thenmixin
'd to the record.record
is the record on whichfromJSON
has been called.
The
encode
anddecode
keys can also be false to avoid using the default identity function encoder or decoder.Note:
Batman.Model
subclasses have no encoders by default, except for one which automatically decodes theprimaryKey
of the model, which is usuallyid
. To get any data into or out of your model, you must white-list the keys you expect from the server or storage attribute.test '@encode accepts a list of keys which are used during decoding', -> class Shop extends Batman.Model @resourceName: 'shop' @encode 'name', 'url', 'email', 'country' json = {name: "Snowdevil", url: "snowdevil.ca"} record = new Shop() record.fromJSON(json) equal record.get('name'), "Snowdevil" test '@encode accepts a list of keys which are used during encoding', -> class Shop extends Batman.Model @resourceName: 'shop' @encode 'name', 'url', 'email', 'country' record = new Shop(name: "Snowdevil", url: "snowdevil.ca") deepEqual record.toJSON(), {name: "Snowdevil", url: "snowdevil.ca"} test '@encode accepts custom encoders', -> class Shop extends Batman.Model @resourceName: 'shop' @encode 'name', encode: (name) -> name.toUpperCase() record = new Shop(name: "Snowdevil") deepEqual record.toJSON(), {name: "SNOWDEVIL"} test '@encode accepts custom decoders', -> class Shop extends Batman.Model @resourceName: 'shop' @encode 'name', decode: (name) -> name.replace('_', ' ') record = new Shop() record.fromJSON {name: "Snow_devil"} equal record.get('name'), "Snow devil" test '@encode can be passed an encoderObject with false to prevent the default encoder or decoder', -> class Shop extends Batman.Model @resourceName: 'shop' @encode 'name', {encode: false, decode: (x) -> x} @encode 'url' record = new Shop() record.fromJSON {name: "Snowdevil", url: "snowdevil.ca"} equal record.get('name'), 'Snowdevil' equal record.get('url'), "snowdevil.ca" deepEqual record.toJSON(), {url: "snowdevil.ca"}, 'The name key is absent because of encode: false'
Some more handy examples:
test '@encode can be used to turn comma separated values into arrays', -> class Post extends Batman.Model @resourceName: 'post' @encode 'tags', decode: (string) -> string.split(', ') encode: (array) -> array.join(', ') record = new Post() record.fromJSON({tags: 'new, hot, cool'}) deepEqual record.get('tags'), ['new', 'hot', 'cool'] deepEqual record.toJSON(), {tags: 'new, hot, cool'} test '@encode can be used to turn arrays into sets', -> class Post extends Batman.Model @resourceName: 'post' @encode 'tags', decode: (array) -> new Batman.Set(array...) encode: (set) -> set.toArray() record = new Post() record.fromJSON({tags: ['new', 'hot', 'cool']}) ok record.get('tags') instanceof Batman.Set deepEqual record.toJSON(), {tags: ['new', 'hot', 'cool']}
-
@validate(keys...[, options : [Object|Function]])
Validations allow a model to be marked as
valid
orinvalid
based on a set of programmatic rules. By validating a model's data before it gets to the server we can provide immediate feedback to the user about what they have entered and forgo waiting on a round trip to the server.validate
allows the attachment of validations to the model on particular keys, where the validation is either a built in one (invoked by use of options to pass to them) or a custom one (invoked by use of a custom function as the second argument).Note: Validation in batman.js is always asynchronous, despite the fact that none of the validations may use an asynchronous operation to check for validity. This is so that the API is consistent regardless of the validations used.
Built in validators are attached by calling
@validate
with options designating how to calculate the validity of the key:test '@validate accepts options to check for validity', -> QUnit.expect(0) class Post extends Batman.Model @resourceName: 'post' @validate 'title', 'body', {presence: true}
The built in validation options are listed below:
presence : boolean
: Assert that the string value is existent (not undefined or null) and has length greather than 0.numeric : true
: Assert that the value is or can be coerced into a number usingparseFloat
.greaterThan : number
: Assert that the value is greater than the given number.greaterThanOrEqualTo : number
: Assert that the value is greater than or equal to the given number.equalTo : number
: Assert that the value is equal to the given number.lessThan : number
: Assert that the value is less than the given number.lessThanOrEqualTo : number
: Assert that the value is less than or equal to the given number.minLength : number
: Assert that the value'slength
property is greater than the given number.maxLength : number
: Assert that the value'slength
property is less than the given number.length : number
: Assert that the value'slength
property is exactly the given number.lengthWithin : [number, number]
orlengthIn : [number, number]
: Assert that the value'slength
property is within the ranger specified by the given array of two numbers, where the first number is the lower bound and the second number is the upper bound.inclusion : in : [list, of, acceptable, values]
: Assert that the value is equal to one of the values in an array.exclusion : in : [list, of, unacceptable, values]
: Assert that the value is not equal to any of the values in an array.
Custom validators should have the signature
(errors, record, key, callback)
. The arguments are as follows:errors
: anErrorsSet
instance which expects to haveadd
called on it to add errors to the modelrecord
: the record being validatedkey
: the key to which the validation has been attachedcallback
: a function to call once validation has been completed. Calling this function is mandatory.
See
Model::validate
for information on how to get a particular record's validity. -
@loaded : Set
The
loaded
set is available on every model class and holds every model instance seen by the system in order to function as an identity map. Successfully loading or saving individual records or batches of records will result in those records being added to theloaded
set. Destroying instances will remove records from the identity set.test 'the loaded set stores all records seen', -> class Post extends Batman.Model @resourceName: 'post' @persist TestStorageAdapter @encode 'name' ok Post.get('loaded') instanceof Batman.Set equal Post.get('loaded.length'), 0 post = new Post() post.save() equal Post.get('loaded.length'), 1 test 'the loaded adds new records caused by loads and removes records caused by destroys', -> class Post extends Batman.Model @resourceName: 'post' @encode 'name' adapter = new TestStorageAdapter(Post) adapter.storage = 'posts1': {name: "One", id:1} 'posts2': {name: "Two", id:2} Post.persist(adapter) Post.load() equal Post.get('loaded.length'), 2 post = false Post.find(1, (err, result) -> post = result) post.destroy() equal Post.get('loaded.length'), 1
-
@all : Set
The
all
set is an alias to theloaded
set but with an added implicitload
on the model.Model.get('all')
will synchronously return theloaded
set and asynchronously callModel.load()
without options to load a batch of records and populate the set originally returned (theloaded
set) with the records returned by the server.Note: The notion of "all the records" is relative only to the client. It completely depends on the storage adapter in use and any backends which they may contact to determine what comes back during a
Model.load
. This means that if for example your API paginates records, the set found inall
may hold on the first 50 records instead of the entire backend set.all
is useful for listing every instance of a model in a view, and since theloaded
set will change when theload
returns, it can be safely bound to.asyncTest 'the all set asynchronously fetches records when gotten', -> class Post extends Batman.Model @resourceName: 'post' @encode 'name' adapter = new AsyncTestStorageAdapter(Post) adapter.storage = 'posts1': {name: "One", id:1} 'posts2': {name: "Two", id:2} Post.persist(adapter) equal Post.get('all.length'), 0, "The synchronously returned set is empty" delay -> equal Post.get('all.length'), 2, "After the async load the set is populated"
-
@clear() : Set
Model.clear()
empties thatModel
's identity map. This is useful for tests and other unnatural situations where records new to the system are guaranteed to be as such.test 'clearing a model removes all records from the identity map', -> class Post extends Batman.Model @resourceName: 'post' @encode 'name' adapter = new TestStorageAdapter(Post) adapter.storage = 'posts1': {name: "One", id:1} 'posts2': {name: "Two", id:2} Post.persist(adapter) Post.load() equal Post.get('loaded.length'), 2 Post.clear() equal Post.get('loaded.length'), 0, "After clear() the loaded set is empty"
-
@find(id, callback : Function) : Model
Model.find()
retrieves a record with the specifiedid
from the storage adapter and calls back with an error if one occurred and the record if the operation was successful.find
delegates to the storage adapter theModel
has been@persist
ed with, so it is up to the storage adapter's semantics to determine what type of errors may return and the timeline on which the callback may be called. Thecallback
is a required function which should adopt the node style callback signature which accepts two arguments: an error, and the record asked for.find
returns an "unloaded" record which, following the load completion, will be populated with the data from the storage adapter.Note:
find
gives two results to calling code: one immediately, and one later.find
returns a record synchronously as it is called and calls back with a record, and importantly these two records are not guaranteed to be the same instance. This is because batman.js maps the identities of incoming and outgoing records such that there is only ever one canonical instance representing a record, which is useful so bindings are always bound to the same thing. In practice, this means that calling code should use the recordfind
calls back with if anything is going to bind to that object, which is most of the time. The returned record however remains useful for state inspection and bookkeeping.asyncTest '@find calls back the requested model if no error occurs', -> class Post extends Batman.Model @resourceName: 'post' @encode 'name' @persist AsyncTestStorageAdapter, storage: 'posts2': {name: "Two", id:2} post = Post.find 2, (err, result) -> throw err if err post = result equal post.get('name'), undefined delay -> equal post.get('name'), "Two"
Note:
find
must be passed a callback function. This is for two reasons: calling code must be aware thatfind
's return value is not necessarily the canonical instance, and calling code must be able to handle errors.asyncTest '@find calls back with the error if an error occurs', -> class Post extends Batman.Model @resourceName: 'post' @encode 'name' @persist AsyncTestStorageAdapter error = false post = Post.find 3, (err, result) -> error = err delay -> ok error instanceof Error
-
@load(options = {}, callback : Function)
Model.load()
retrieves an array of records according to the givenoptions
from the storage adapter and calls back with an error if one occurred and the set of records if the operation was successful.load
delegates to the storage adapter theModel
has been@persist
ed with, so it is up to the storage adapter's semantics to determine what the options do, what kind of errors may arise, and the timeline on which the callback may be called. Thecallback
is a required function which should adopt the node style callback signature which accepts two arguments, an error, and the array of records.load
returns undefined.For the two main
StorageAdapter
s batman.js provides, theoptions
do different things:- For
Batman.LocalStorage
,options
act as a filter. The adapter will scan all the records inlocalStorage
and return only those records which match all the key/value pairs given in the options. For
Batman.RestStorage
,options
are serialized into query parameters on theGET
request.asyncTest '@load calls back an array of records retrieved from the storage adapter', -> class Post extends Batman.Model
@resourceName: 'post' @encode 'name' @persist TestStorageAdapter, storage: 'posts1': {name: "One", id:1} 'posts2': {name: "Two", id:2}
posts = false Post.load (err, result) ->
throw err if err posts = result
delay ->
equal posts.length, 2 equal posts[0].get('name'), "One"
asyncTest '@load calls back with an empty array if no records are found', -> class Post extends Batman.Model
@resourceName: 'post' @encode 'name' @persist TestStorageAdapter, storage: []
posts = false Post.load (err, result) ->
throw err if err posts = result
delay ->
equal posts.length, 0
- For
-
@create(attributes = {}, callback) : Model
-
@findOrCreate(attributes = {}, callback) : Model
-
id : value
-
dirtyKeys : Set
-
errors : ErrorsSet
-
constructor(idOrAttributes = {}) : Model
-
isNew() : boolean
-
updateAttributes(attributes) : Model
-
toString() : string
-
toJSON() : Object
-
fromJSON() : Model
-
toParam() : value
-
state() : string
-
hasStorage() : boolean
-
load(options = {}, callback)
-
save(options = {}, callback)
-
destroy(options = {}, callback)
-
validate(callback)