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
primaryKeyis 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
primaryKeyis 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 byprimaryKeyis 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
storageKeyis a class level option which gives the storage adapters something to interpolate into their specific key generation schemes. In the case ofLocalStorageorSessionStorageadapters, thestorageKeydefines what namespace to store this record under in thelocalStorageorsessionStoragehost objects, and with the case of theRestStoragefamily of adapters, thestorageKeyassists in URL generation. See the documentation for the storage adapter of your choice for more information.The default
storageKeyisnull. -
@persist(mechanism : StorageAdapter) : StorageAdapter
@persistis how aModelsubclass is told to persist itself by means of aStorageAdapter.@persistaccepts either aStorageAdapterclass 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]])
@encodespecifies a list ofkeysa 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
encoderObjectshould have anencodeand/or adecodekey 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
encodefunctions are as follows:valueis the client side value of thekeyon therecordkeyis the key which thevalueis stored under on therecord. This is useful when passing the sameencoderObjectwhich needs to pivot on what key is being encoded to different calls toencode.builtJSONis 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.recordis the record on whichtoJSONhas been called.
For
decodefunctions:valueis the server side value of thekeywhich will end up on therecord.keyis the key which thevalueis stored under in the incoming JSON.incomingJSONis the JSON which is being decoded into therecord. This can be used to create compound key decoders.outgoingObjectis the object which is built up by the decoders and thenmixin'd to the record.recordis the record on whichfromJSONhas been called.
The
encodeanddecodekeys can also be false to avoid using the default identity function encoder or decoder.Note:
Batman.Modelsubclasses have no encoders by default, except for one which automatically decodes theprimaryKeyof 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
validorinvalidbased 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.validateallows 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
@validatewith 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'slengthproperty is greater than the given number.maxLength : number: Assert that the value'slengthproperty is less than the given number.length : number: Assert that the value'slengthproperty is exactly the given number.lengthWithin : [number, number]orlengthIn : [number, number]: Assert that the value'slengthproperty 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: anErrorsSetinstance which expects to haveaddcalled 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::validatefor information on how to get a particular record's validity. -
@loaded : Set
The
loadedset 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 theloadedset. 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
allset is an alias to theloadedset but with an added implicitloadon the model.Model.get('all')will synchronously return theloadedset and asynchronously callModel.load()without options to load a batch of records and populate the set originally returned (theloadedset) 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 inallmay hold on the first 50 records instead of the entire backend set.allis useful for listing every instance of a model in a view, and since theloadedset will change when theloadreturns, 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 specifiedidfrom the storage adapter and calls back with an error if one occurred and the record if the operation was successful.finddelegates to the storage adapter theModelhas been@persisted 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. Thecallbackis a required function which should adopt the node style callback signature which accepts two arguments: an error, and the record asked for.findreturns an "unloaded" record which, following the load completion, will be populated with the data from the storage adapter.Note:
findgives two results to calling code: one immediately, and one later.findreturns 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 recordfindcalls 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:
findmust 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 givenoptionsfrom the storage adapter and calls back with an error if one occurred and the set of records if the operation was successful.loaddelegates to the storage adapter theModelhas been@persisted 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. Thecallbackis a required function which should adopt the node style callback signature which accepts two arguments, an error, and the array of records.loadreturns undefined.For the two main
StorageAdapters batman.js provides, theoptionsdo different things:- For
Batman.LocalStorage,optionsact as a filter. The adapter will scan all the records inlocalStorageand return only those records which match all the key/value pairs given in the options. For
Batman.RestStorage,optionsare serialized into query parameters on theGETrequest.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 = resultdelay ->
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 = resultdelay ->
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)
