Skip to content
Lance Pollard edited this page Oct 17, 2012 · 10 revisions

Overview

Tower.Model provides a standard set of interfaces for manipulating data. It also helps building custom ORMs for use outside of Tower.

Tower.Model provides the same interface on the client and the server. When you're on the client, it knows how to store the data in memory or through ajax and/or websockets to the server (more below). When you're on the server, it's saving it to MongoDB (by default), and once support for other databases such as CouchDB, MySQL, Neo4j, etc. is integrated, it can save to those with no change to your code.

This means you can write one set of models and it will work in Node.js and the browser. In reality though, you'll want to add functionality specific to the client (jQuery stuff) and server (background jobs like emailing, etc.), but for simple apps you can get away with one set of models!

Note: If a particular datastore isn't implemented yet, feel free to implement it and create a pull request, many people would probably use the datastore you created. It's pretty easy to create, it just takes implementing a few methods. See the Tower.Store section for details.

Getting started with Models in Tower

class App.User extends Tower.Model
  @field 'firstName'

The Criteria Object

/-------------------\               
| URL Parameter API |-----\
\-------------------/     |
                          |
 /------------------\     |
 | Command Line API |-----+
 \------------------/     |     /----------\
                          +-----| Criteria |
        /-----------\     |     \----------/
        | Model API |-----+
        \-----------/     |
                          |
         /----------\     |
         | JSON API |-----/
         \----------/

Ditaa diagram version

Create a record

The following happens for create, update, and destroy, no matter if it's called on the client or server.

/-------------------\
| App.User.create() |
\-------------------/
  | /-----------------\
  +-| cursor.create() |
    \-----------------/
      | /----------------\
      +-| store.create() |
      | \----------------/
      | /------------------------------------\
      +-| Tower.notifyConnections('created') |
        \------------------------------------/
          | /--------------------------\
          +-| each connection.notify() |
            \--------------------------/
              | /--------------------------------\
              +-| socket.emit('created', record) |
                \--------------------------------/

Ditaa diagram

When called from the server, it emits to the connected clients; when called from the client, it emits to the server, which will then run the same process on the server, emitting to the connected clients.

Model Attributes

Consider a simple class for modeling a user in an application. A user may have a first name, last name, and middle name. We can define these attributes on a user by using the field macro function.

class App.User extends Tower.Model
  @field 'firstName', type: 'String'
  @field 'middleName', type: 'String'
  @field 'lastName', type: 'String'

Below is a list of valid types for fields.

  • Array
  • [todo] BigDecimal (Stores as a String in the database)
  • Boolean
  • Float
  • Object
  • Integer
  • String
  • Time
  • Date
  • DateTime

If you decide not to specify the field type, Tower will treat it as a JavaScript String. If you don't want Tower to try typecasting your value, type it to Object.

Getting and Setting Field Values

When a field is defined, Tower provides several different ways of accessing the field.

# Get the value of the first name field.
user.get('firstName')

# Set the value for the first name field.
user.set('firstName', 'Jean')

In cases where you want to set multiple field values at once, there are a few different ways of handling this as well.

App.User.new(firstName: 'Jean-Baptiste', middleName: 'Emmanuel')

# Get the field values as a hash.
user.getProperties('firstName', 'middleName')
#=> { firstName: 'Jean-Baptiste', middleName: 'Emmanuel' }

# Set the field values in the record.
user.setProperties(firstName: 'Jean-Baptiste', middleName: 'Emmanuel')

This is straight from Ember.js. It wraps all set calls in a single operation.

Defaults

You can tell a field in Tower to always have a default value if nothing has been provided. Defaults are either static values or callback functions.

class App.User extends Tower.Model
  @field 'bloodAlcoholLevel', type: 'Float', default: 0.40
  @field 'lastLogin', type: 'Time', default: -> _(10).minutes().ago().toDate()

Be wary that default values that are not defined as functions are evaluated at class load time, so the following 2 definitions are not equivalent. (You probably would prefer the second, which is at record creation time.)

class App.User extends Tower.Model
  @field 'dob', type: 'Time', default: new Date
  @field 'dob', type: 'Time', default: -> new Date

If you want to set a default with a dependency on the record's state, this inside a callback evaluates to the record instance.

class App.User extends Tower.Model
  @field 'joinedAt', type: 'Time', default: -> if @get('isNew') then _(2).hours().ago() else new Date

Computed Attributes

If attributes are computed from fields, don't make it a field, just use computed properties directly:

class App.User extends Tower.Model
  @field 'firstName'
  @field 'lastName'

  name: Ember.computed(->
    "#{@get('firstName')} #{@get('lastName')}"
  ).property('firstName', 'lastName')

Tower Model Attribute Initialization

You can instantiate models two ways:

  1. Explicitly with build
  2. Internally from the datastore

The reason for this is when the record comes from the database, isNew == false, and you must run the find callbacks.

# this is what you normally do
user = App.User.build(firstName: 'Josh')
# this is what the datastore does
user = App.User.build(attributesFromDatabase, isNew: false)
# that isNew: false triggers this:
user._initializeFromStore(attributesFromDatabase)

(in progress) Authentication Layer

class App.User extends Tower.Model
  @authenticated 'password'
user.authenticate('crazy-passwword')

(in progress) Authorization Layer

class App.User extends Tower.Model
class App.Ability extends Tower.Model
class App.ApplicationController extends Tower.Controller
  @beforeAction 'setCurrentUser'
  @beforeAction 'setCurrentAbility'
  
  setCurrentUser: (next) ->
    App.User.first (error, user) => # need to use a real solution with sessions, etc.
      @currentUser = user
      next()
  
  setCurrentAbility: ->
    @currentAbility = new App.Ability(@currentUser)

Model Callbacks

Tower supports 3 main callbacks:

  • before
  • around
  • after

The following callbacks are implemented:

  • @after 'initialize'
  • @before 'validate'
  • @after 'validate'
  • @before 'create'
  • @after 'create'
  • @before 'update'
  • @after 'update'
  • @before 'save'
  • @after 'save'
  • @before 'destroy'
  • @after 'destroy'

Callbacks are available on any model.

Define a callback with the callback phase helpers

class App.Post extends Tower.Model
  @field 'title', type: 'String'
  @field 'slug', type: 'String'
  
  @before 'save', 'generateSlug'
  
  generateSlug:  ->
    @set('slug', _.parameterize(@get('title')))

Define the phase and callback directly

class App.Post extends Tower.Model
  @field 'title', type: 'String'
  @field 'slug', type: 'String'
  
  @callback 'save', 'before', 'generateSlug'
  
  generateSlug:  ->
    @set('slug', _.parameterize(@get('title')))

Define callbacks with anonymous functions

class App.Post extends Tower.Model
  @field 'title', type: 'String'
  @field 'slug', type: 'String'
  
  @before 'save', ->
    @set('slug', _.parameterize(@get('title')))

Callbacks can be asynchronous

If you have a callback that executes asynchronous code, you can add the callback argument to your function, and call it when complete:

class App.Post extends Tower.Model
  @field 'title', type: 'String'
  @field 'url', type: 'String'
  
  @before 'save', 'scrapeWebsite'
  
  scrapeWebsite: (callback) ->
    SomeCrawler.scrapeHTML @get('url'), (error, html) ->
      callback(error)

Callbacks are called in series, so if you have several async callbacks, know they will be executed one after another.

Background Processing

class App.User extends Tower.Model
  @after 'create', 'welcome'

  @sendWelcomeEmail: (id) ->
    App.User.find id, (error, user) =>
      App.Notification.welcome(user).deliver()
    
  welcome: ->
    App.User.enqueue 'sendWelcomeEmail', @get('id')

In the above example, after a User is created we call it's welcome method. That queues a class method, passing the user id as a parameter. You pass the id rather than the whole User model to the queue method because this is going to be serialized to some backend key-value store, like Redis, and it's both a smaller amount of data to store, and it's easier to serialize/deserialize just an id.

The Cursor

Any of the finder or persistence methods that return an array are really returning a cursor.

Attribute "Dirty Tracking"

Tower supports tracking of changed or "dirty" fields with an API that mirrors that of Active Model. If a defined field has been modified in a model the model will be marked as dirty and some additional behavior comes into play.

Viewing Changes

There are various ways to view what has been altered on a model. Changes are recorded from the time a record is instantiated, either as a new record or via loading from the database up to the time it is saved. Any persistence operation clears the changes.

class App.User extends Tower.Model
  @field 'name', type: 'String'

user = App.User.first()

# Check to see if the record has changed.
user.isDirty() #=> false

user.set('name', 'Alan Garner')

# Check to see if the record has changed.
user.get('isDirty') #=> true

# Get a hash of the old and changed values for each field.
user.get('changes') #=> { 'name' : [ 'Alan Parsons', 'Alan Garner' ] }

# Get the changes for a specific field.
user.attributeChange('name') #=> [ 'Alan Parsons', 'Alan Garner' ]

# Get the previous value for a field.
user.attributeWas('name') #=> 'Alan Parsons'

Resetting Changes

You can reset changes of a field to it's previous value by calling the reset method.

user = App.User.first()

user.set('name', 'Alan Garner')

# Reset the changed name back to the original
user.resetAttribute('name')

user.get('name') #=> 'Alan Parsons'

Notes on Dirty Tracking and Persistence

Tower uses dirty tracking as the core of its persistence operations. It looks at the changes on a record and atomically updates only what has changed unlike other frameworks that write the entire record on each save. If no changes have been made, Tower will not hit the database on a call to Model#save.

Finders

Here are the methods used to query models in a datastore:

  • Tower.Model.all
  • Tower.Model.find
  • Tower.Model.first
  • Tower.Model.last
  • Tower.Model.count
  • Tower.Model.exists
  • Tower.Model.batch

These methods are delegated to a method of the same name a Tower.Model.Scope instance. By delegating all query and persistence calls to the Tower.Model.Scope object, there's one place in the Tower code to build out a very powerful API for chainable scopes (more on that later). This means you can do:

App.User.all()

or create a reusable scope:

App.User.where(firstName: /^[aA]/).limit(10)

By calling one of the finder methods, the scope's criteria are compiled into an optimized query and the models are queried.

Tower.Model.all

Returns an array of models. It only takes one argument, the callback. If you're using the memory store, it will also return an array of models so you don't need to pass in a callback. This makes TDD much easier. BUT, don't count on that, as the other stores return sometimes random things. Use the callback whenever you can. As usual, the first argument in the callback is an error.

App.User.all (error, models) ->
  for model in models
    model.get('id')

Tower.Model.find

Provides the ability to find one or many models given a set of ids. This is a more all-inclusive API than all.

The first way to use this method is for finding a single record given the provided id. If no record is found this will raise an error unless the configuration option is changed. You can call this method on a scope as well, so you can find all users with a last name of 'Black' who have this id.

App.User.find(id)
App.User.find('4baa56f1230048567300485c')
App.User.where(lastName: 'Black').find(id)

You may also find multiple records given the provided array of ids. If a single record is not found the error will get raised.

App.User.find([idOne, idTwo])
App.User.find(['4baa56f1230048567300485c','4baa56f1230048567300485d'])
App.User.where(lastName: 'Black').find([idOne, idTwo])

If multiple ids are passed, you will get an array back. If you only pass 1 id, then you get a record back. The complete signature looks like this:

App.User.find '4baa56f1230048567300485c', (error, record) ->
App.User.find ['4baa56f1230048567300485c', '4baa56f1230048567300485d'], (error, records) ->

Tower.Model.first

Find the first record in the datastore given the provided criteria. Will return a record or null if nothing is found and defaults to the natural sorting of records in the datastore. You can provide sort criteria as well if you want to dictate the exact record that would be returned first.

App.User.first (error, record) ->

Tower.Model.last

Find the last record in the datastore given the provided criteria. Will return a record or null if nothing is found and defaults to to sorting by id in descending order. You may provide sort criteria as well if you want to dictate the exact record that would be returned - Tower will invert the sort criteria you provide.

App.User.last (error, record) ->

Tower.Model.count

Get the count of records given the provided criteria.

App.User.count (error, count) ->

Tower.Model.exists

Returns true if any records in the datastore exist given the provided criteria and false if there are none.

# Do any records exist in the datastore for the provided conditions?
App.User.exists (error, exists) ->

(todo) Tower.Model.batch

This will grab records from the datastore in chunks, to prevent a memory usage explosion if you have a lot of records.

App.User.batch(20).each (user) ->

Adding Field Indices

Tower.Model.index

Model Inheritance

Tower supports inheritance in both root and embedded records. In scenarios where records are inherited from their fields, relations, validations and scopes get copied down into their child records, but not vise-versa.

class App.Canvas extends Tower.Model
  @field 'name', type: 'String'
  
  @hasMany 'shapes', embedded: true

class App.Browser extends App.Canvas
  @field 'version', type: 'Integer'
  
  @scope 'recent', @where(version: '>': 3)

class App.Firefox extends Browser

class App.Shape extends Tower.Model
  @field 'x', type: 'Integer'
  @field 'y', type: 'Integer'
  
  @belongsTo 'canvas', embedded: true

class App.Circle extends App.Shape
  @field 'radius', type: 'Float'

class App.Rectangle extends App.Shape
  @field 'width', type: 'Float'
  @field 'height', type: 'Float'

In the above example, Canvas, Browser and Firefox will all save in the canvases collection. An additional attribute _type is stored in order to make sure when loaded from the database the correct record is returned. This also holds true for the embedded records Circle, Rectangle, and Shape.

Querying for Subclasses

Querying for subclasses is handled in the normal manner, and although the records are all in the same collection, queries will only return records of the correct type, similar to Single Table Inheritance in ActiveRecord.

# Returns Canvas records and subclasses
App.Canvas.where(name: 'Paper')
# Returns only Firefox records
App.Firefox.where(name: 'Window 1')

Model Associations

You can add any type of subclass App.to a has one or has many association, through either normal setting or through the build and create methods on the association:

firefox = App.Firefox.build()
# Builds a Shape object
firefox.get('shapes').build({ x: 0, y: 0 })
# Builds a Circle object
firefox.get('shapes').build({ x: 0, y: 0 }, App.Circle)
# Creates a Rectangle object
firefox.get('shapes').create({ x: 0, y: 0 }, App.Rectangle)

rect = App.Rectangle.build(width: 100, height: 200)
firefox.get('shapes')

Deep Dive into Tower Models

Instantiating a record

To instantiate a record you have two options, to use build:

user = App.User.build()

The build function is an alias to Ember.Object.create. The reason we did this was because, when dealing with database records specifially, the create method intuitively should actually persist a record to the database. So, App.User.create builds the object using App.User.build as well as saves it to the database. We thought about calling this method insert rather than create, but insert doesn't feel like it should return a record. In summary, App.User.build is the same as Ember.Object.create, which just constructs an object in JavaScript in memory, while App.User.create has the additional functionality of persisting it to the database. Just wanted to avoid confusion there.

Currently you shouldn't instantiate a model (or anything in Tower or Ember, really) with the native JavaScript constructor function:

user = App.User.build()

The reason for this is because Tower models extend Ember.Object, and Ember.Object.create is implemented differently than new Ember.Object. This API may be made simpler in the future.

When you instantiate a record, you can pass it attributes:

App.User.build(email: '[email protected]')

For those of you from the Ember world, this roughly equivalent to:

Ember.Object.create().setProperties(email: '[email protected]')

When you instantiate a record without persisting it, it will not be added to any published cursor. To add it to a cursor without persisting it to the database, you can call Tower.ModelCursor.push(records) and it will add it to the published cursors. If the record is persisted, however, Tower.ModelCursor.push(record) will be called automatically.

Creating a record

You can create a record from a model's class or instance:

# `.create` calls both `.build` and `#save`
user = App.User.create()
# or you can do both manually
user = App.User.build()
user.save()

Whenever you create a record like this, it will be saved to the database. If you're on the client, it will use either web sockets or Ajax to send the record to the server. After the record is created it will notify all published cursors of its presence. Note that on the client, it will notify cursors before the Ajax response comes, to make it look as if the record saved immediately. If the server responds with any errors, it will keep the newly created record in the cursor but set validation errors.

Here is how the record creation process works internally.

First you call the App.User.create. The create method gets delegated to a new Tower.Model.Scope instance, which then delegates it to a new Tower.ModelCursor instance (this will be optimized in the future, but it is necessary for now to keep the API simple). In Tower.Model.Scope#create, it normalizes the attributes you just passed in and sets the arguments to the @cursor.data array, which holds all the records you're just about to create. You can pass in a single hash of attributes, and array of attribute hashes, or a splat of arguments, all followed by a callback.

Then Tower.ModelCursor#create iterates through each of attributes in its @data array and builds a model, such as calling App.User.build(attributes). It then iterates through each of the model instances and calls model.save, which goes through the model validation lifecycle (described later). The model then creates another cursor and passes itself in (again, this will be optimized, but it was necessary to keep the API simple for now), calling the create method. So basically, it's back to where it was, only this time the cursor knows we don't need to call model.save. Instead, the cursors create method calls @store.create, passing itself in as the first parameter - that is, the store.create method takes a cursor. The reason we do this is to give the store enough context about the database operation we're about to perform, so the store can optimize its actual implementation.

The store then creates the record (say for example we're using the MongoDB store on the server and the Memory store on the client). When it completes, it will return the newly created records - each with a new id property - to the calling cursor. If you passed in a callback, you will now get the record or array of records in the callback. Finally, the cursor notifies Tower that a record was created.

When Tower gets notified that a record was created, here's what happens on the client. First, it iterates through all of the client-side controllers and finds any cursors you've published on them. It then pushes the record(s) into any of the matching cursors - so if any of those cursors were bound to Ember CollectionViews for example, they would display the new record.

When a record gets created on the server, here's what happens. First, it iterates through all the connected clients (instances of Tower.Net.Connection), which are stored in the Tower.connections property. Then for each connected client, it does a similar thing to what it did on the client: iterates through the controllers, finds the cursors, and tests the record(s) against them. But instead of adding it to the cursor, it just compiles a final array of records the client is allowed to receive. Once the records have been collected for a single client, it then sends a push notification to the client (through web sockets or fallbacks) with the newly created records. Then on the client, it just loads them into the store, pushing them into the matching cursors, etc. (same process). Effectively, this means "cursors" work the same on the client and server.

What is the "Cursor"?

Most generally, Tower.ModelCursor is a set of criteria for matching a class of models. On the client, it also is used as an Array, acting as a collection of models matching its criteria. Technically you can do the same thing on the server, but by default it won't add records to the cursor on the server because it's unnecessary since no state is maintained on the server.

Tower.Model.Persistence

Tower's standard persistence methods come in the form of common methods you would find in other mapping frameworks.

  • Tower.Model.create
  • Tower.Model.update
  • Tower.Model.destroy
  • Tower.Model#save
  • Tower.Model#updateAttributes
  • Tower.Model#updateAttribute
  • Tower.Model#destroy

Tower.Model.create

Inserts a new record into the datastore given the provided attributes. This will run validations and will return the record whether it was persisted or not. You can check Tower.Model#persisted? to see if it was successful.

# Insert a new German poet to the db.
App.User.create(firstName: 'Heinrich', lastName: 'Heine')
# This can also take a block.
App.User.create firstName: 'Heinrich', (record) ->
  record.set('lastName', 'Heine')

App.User.create(firstName: 'Lance')
App.User.where(firstName: 'Lance').create()
App.User.where(firstName: 'Lance').create([{lastName: 'Pollard'}, {lastName: 'Smith'}])
App.User.where(firstName: 'Lance').create(App.User.build(lastName: 'Pollard'))

Tower.Model.update

App.User.update(1, name: 'John')
App.User.update(1, 2, 3, name: 'John')
App.User.update([1, 2, 3], name: 'John')
App.User.update(name: 'John')

App.User.where(firstName: 'Lance').update(1, 2, 3)
App.User.update(App.User.first(), App.User.last(), firstName: 'Lance')
App.User.update([App.User.first(), App.User.last()], firstName: 'Lance')
App.User.update([1, 2], firstName: 'Lance')

Tower.Model.destroy

Deletes all matching records in the datastore given the supplied conditions. See the criteria section on deletion for preferred ways to perform these actions. This runs destroy callbacks on all matching records.

# Destroy all the records from the collection.
App.User.destroy()

# Destroy all matching records.
App.User.where(firstName: 'Heinrich').destroy()

Tower.Model#save

Saves the record to the datastore. If the record is new then the entire record will be inserted. If the record is already saved then only changes to the record will the persisted. This runs validations by default, however they can be switched off by providing an option to the method. Returns true if validation passed and false if not.

# Insert a new German poet to the db.
user = App.User.build(firstName: 'Heinrich', lastName: 'Heine')
user.save()

# Save without running validations.
user.save(validate: false)

# Save an existing record's changed fields.
user.set('firstName', 'Christian Johan')

user.save (error) ->

Tower.Model#updateAttributes

Modifies the provided attributes to new values and persists them in a single call. This runs validations and will return true if they passed, false if not.

# Update the provided attributes.
user.updateAttributes(firstName: 'Jean', lastName: 'Zorg')

Tower.Model#updateAttribute

Updates a single attribute in the datastore without going through the normal validation procedure, but does fire callbacks. Returns true if save was successful, false if not. These are "atomic updates" in MongoDB.

# Update the provided attribute.
user.updateAttribute(:firstName, 'Jean')

Tower.Model#destroy

Deletes the record from the datastore while running destroy callbacks.

user.destroy()

Tower.Model.Scopes Part 2 - Querying

The following are a list of chainable query methods in Tower. Shown alongside each example are the generated query parameters and options which are passed to the store object. The stores then convert these normalized criteria into the datastore-specific format.

Please note that criteria are lazy evaluated, and with each chained method it will be cloned and return a new criteria copy.

Query Methods

  • Tower.Model.allIn
  • Tower.Model.allOf
  • Tower.Model.alsoIn
  • Tower.Model.anyIn
  • Tower.Model.anyOf
  • Tower.Model.asc
  • Tower.Model.desc
  • Tower.Model.distinct
  • Tower.Model.excludes
  • Tower.Model.includes
  • Tower.Model.limit
  • Tower.Model.near
  • Tower.Model.notIn
  • Tower.Model.only
  • Tower.Model.order
  • Tower.Model.paginate
  • Tower.Model.offset
  • Tower.Model.where
  • Tower.Model.within

Tower.Model.allIn

Adds a criterion that specifies values that must all match in order to return results.

Model

# Match all people with Bond and 007 as aliases.
App.User.allIn(aliases: ['Bond', '007'])

Criteria

{ 'aliases' : { '$all' : [ 'Bond', '007' ] }}

Tower.Model.allOf

Adds a criterion that specifies expressions that must all match in order to return results.

Model

# Match all crazy old people.
App.User.allOf(age: {'>=': 60}, mentalState: 'crazy mofos')

Criteria

{ '$and' : [{ 'age' : { '$gt' : 60 }}, { 'mentalState' : 'crazy mofos' }] }

Tower.Model.alsoIn

Adds a criterion that specifies values where any value can be matched in order to return results. This is similar to Criteria#anyIn with the exception here that if if it chained with values for the same field it performs a union of the values where anyIn perform an intersection.

Model

# Match all people with either Bond or 007 as aliases.
App.User.alsoIn(aliases: [ 'Bond', '007' ])
App.User.anyIn(aliases: [ 'Bond' ]).alsoIn(aliases: [ '007' ])

Criteria

{ 'aliases' : { '$in' : [ 'Bond', '007' ] }}

Tower.Model.anyIn

Adds a criterion that specifies values where any value can be matched in order to return results. This is similar to Criteria#alsoIn with the exception here that if if it chained with values for the same field it performs an intersection of the values where alsoIn perform a union.

Model

# Match all people with either Bond or 007 as aliases.
App.User.anyIn(aliases: [ 'Bond', '007' ])
App.User
  .anyIn(aliases: [ 'Bond', '007', 'James' ])
  .anyIn(aliases: [ 'Bond', '007' ])

Criteria

{ 'aliases' : { '$in' : [ 'Bond', '007' ] }}

Tower.Model.anyOf

Adds a criterion that specifies a set of expressions that any can match in order to return results. The underlying MongoDB expression is $or.

Model

# Match all people with either last name Penn or Teller
App.User.anyOf({ lastName: 'Penn' }, { lastName: 'Teller' })

Criteria

{ 'lastName' :
  { '$or' :
    [ { 'lastName' : 'Penn' }, { 'lastName' : 'Teller' } ]
  }
}

Tower.Model.asc

Adds ascending sort options for the provided fields.

Model

# Sort people by first and last name ascending.
App.User.asc('firstName', 'lastName')

Criteria

{ 'sort' :
    {[ [ 'firstName', 'asc' ],
      [ 'lastName', 'asc' ] ]} }

Tower.Model.desc

Adds descending sort options for the provided fields.

Model

# Sort people by first and last name descending.
App.User.desc('firstName', 'lastName')

Criteria

{ 'sort' :
    {[ [ 'firstName', 'desc' ],
      [ 'lastName', 'desc' ] ]} }

Tower.Model.distinct(name)

Get the distinct values for the provided field.

Model

# Get the distinct values for last names
App.User.distinct('lastName')

Criteria

{ 'distinct' : 'lastName' }

Tower.Model.excludes

Adds a criterion that specifies a set of expressions that cannot match in order to return results. The underlying MongoDB expression is $ne.

Model

# Match all people without either last name Teller and first name Bob.
App.User.excludes(lastName: 'Teller', firstName: 'Bob')

Criteria

{{ 'lastName' : { '$ne' : 'Teller' } }, { 'firstName' : { '$ne' : 'Bob' } }}

Tower.Model.includes

Adds a criterion that specifies a list of relational associations to eager load when executing the query. This is to prevent the n+1 issue when iterating over records that access their relations during the iteration.

This only works with hasMany, hasOne, and belongsTo relations and only 1 level deep at the current moment. If you try to eager load a many to many an exception will get raised. Many to many is not supported due to the performance actually being slower despite lowering the number of datastore calls.

Model

# Eager load the posts and games when retrieving the people.
App.User.includes('posts', 'comments')

Criteria

peopleIds = people.find({}, { 'fields' : { '_id' : 1 }})
posts.find({ 'personId' : { '$in' : peopleIds }})
comments.find({ 'personId' : { '$in' : peopleIds }})

Tower.Model.limit

Limits the number of returned results by the provided value.

Model

# Only return 20 records.
App.User.limit(20)

Criteria

{ 'limit' : 20 }

Tower.Model.near

Adds a criterion to find locations that are near the supplied coordinates. This performs a MongoDB $near selection and requires a 2d index to be on the provided field.

Model

# Match all bars near Berlin
Bar.near(location: [ 52.30, 13.25 ])

Criteria

{ 'location' : { '$near' : [ 52.30, 13.25 ] }}

Tower.Model.notIn

Adds a criterion that specifies a set of expressions that cannot match in order to return results. The underlying MongoDB expression is $nin.

Model

# Match all people without last names Zorg and Dallas
App.User.notIn(lastName: [ 'Zorg', 'Dallas' ])

Criteria

{{ 'lastName' : { '$nin' : [ 'Zorg', 'Dallas' ] } }}

Tower.Model.only

Limits the fields returned from the datastore to those supplied to the method. Extremely useful for list views where the entire records are not needed. Cannot be used in conjunction with #without.

Model

# Return only the first and last names of each person.
App.User.only('firstName', 'lastName')

Criteria

options: { 'fields' : { 'firstName' : 1, 'lastName' : 1 }}

Tower.Model.order

Sorts the results given the arguments that must match the MongoDB driver sorting syntax (key/value pairs of field and direction).

Model

# Provide the sorting options.
App.User.order('firstName', 'asc').order('lastName', 'desc')

Criteria

{ 'sort' :
    {[ [ 'firstName', 'asc' ],
      [ 'lastName', 'desc' ] ]} }

Tower.Model.skip

Skips the number of the results given the provided value, similar to a SQL 'offset'.

Model

# Skip 20 records.
App.User.skip(20)

Criteria

{ 'skip' : 20 }

Tower.Model.where

Adds a criterion that must match in order to return results. If provided a string it interperets it as a javascript function and converts it to the proper $where clause. Tower also provides convenience h4s on Symbol to make advanced queries simpler to write.

Model

# Match all people with first name Emmanuel
App.User.where(firstName: 'Emmanuel')

# Match all people who live in Berlin, where address is embedded.
App.User.where('addresses.city': 'Berlin')

# Same as above but with a hash.
App.User.where(addresses: city: 'Berlin')

# Match all people who live at an address in Berlin or
# Munich where address is embedded.
App.User.where('addresses.city': {'$in': ['Berlin', 'Munich']})

# Example complex queries
App.User.where(age: '>': 21)
App.User.where(age: $gt: 21)
App.User.where(age: '>=': 21)
App.User.where(age: $gte: 21)
App.User.where(title: $in: ['Sir', 'Madam'])
App.User.where(age: '<': 55)
App.User.where(age: $lt: 55)
App.User.where(age: '<=': 55)
App.User.where(age: $lte: 55)
App.User.where(title: $ne: 'Mr')
App.User.where(title: $nin: ['Esquire'])
App.User.where(age: '>=': 18, '<=': 55)

Criteria

# Match all people with first name Emmanuel
{ 'firstName' : 'Emmanuel' }

# Match all people who live in Berlin, where address is embedded.
{ 'addresses.city' : 'Berlin' }

# Example queries using symbol h4s to perform more complex criteria.
{ 'age' : { '$gt' : 18 }}
{ 'age' : { '$gt' : 18 }}
{ 'age' : { '$gte' : 18 }}
{ 'age' : { '$gte' : 18 }}
{ 'title' : { '$in' : [ 'Sir', 'Madam' ] }}
{ 'age' : { '$lt' : 55 }}
{ 'age' : { '$lt' : 55 }}
{ 'age' : { '$lte' : 55 }}
{ 'age' : { '$lte' : 55 }}
{ 'title' : { '$ne' : 'Mr' }}
{ 'title' : { '$nin' : [ 'Esquire' ] }}
{ 'age' : { '$gte' : 18, '$lte' : 55 }}

Models on the Server

Models on the server work the same as they do on the client, you just have a different set of stores available:

  • Tower.Store.Memory: in memory
  • Tower.Store.MongoDB: node-mongo-native driver

I'm working on a Neo4j store right now, and the CouchDB one should be super easy to implement. If anyone wants to implement a PostGreSQL, MySQL, or SQLite3 store, I'd be happy to include it in!

As with any ORM, sometimes you're going to need direct access to the (in this case) MongoDB driver. Reasons for this are usually either you're doing some complex/custom/optimized query that requires features outside of the scope of the ORM, or using the ORM overly complicates what you're trying to do (this happens way down the road, and using an ORM is definitely beneficial up front). You can access the store directly from a model like this:

class App.User extends Tower.Model
  @store Tower.Store.MongoDB
  
store = App.User.store()  # instance of Tower.Store.MongoDB
store.lib()               # the core library, such as `require("mongodb")`

Sometimes a Tower.Store implementation may have more than the base API. For instance, in Tower.Store.MongoDB, you have access to collections and the database connection, which is helpful.

Tower.Model.States

Tower.Model.States =
  isLoaded:  false
  isDirty:   false
  isSaving:  false
  isDeleted: false
  isError:   false
  isNew:     true
  isValid:   true

Stores

There's a unified interface to the different types of stores, so you can use the model and have it transparently manage data. For example, for the browser, you can use the memory store, and for the server, you can use the mongodb store. Redis, PostgreSQL, and Neo4j are in the pipeline.

The Store knows about the Model.

class App.Page extents Tower.Model
  @store "mongodb"

Resources

Stores are the interface models use to find their data.

all()

all(title: "Title")

all({title: "Title"}, {safe: true})

all({title: "Title"}, {safe: true}, (error, records) ->)

You can only do the last one!

Create

App.User.store().insert(new User(firstName: "Lance"))
App.User.store().insert(firstName: "Lance")
App.User.store().insert([{firstName: "Lance"}, {firstName: "Dane"}])

Validations

The Errors Object

Validation Helpers

Tower offers many pre-defined validation helpers that you can use directly inside your model class App.definitions. These helpers provide common validation rules. Every time a validation fails, an error message is added to the object's errors collection, and this message is associated with the field being validated.

Each helper accepts an arbitrary number of attribute names, so with a single line of code you can add the same kind of validation to several attributes.

All of them accept the on and message options, which define when the validation should be run and what message should be added to the errors collection if it fails, respectively. The on option takes one of the values save (the default), create or update. There is a default error message for each one of the validation helpers. These messages are used when the message option isn't specified. Let's take a look at each one of the available helpers.

Acceptance

Validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your application's terms of service, confirm reading some text, or any similar concept. This validation is very specific to web applications and this 'acceptance' does not need to be recorded anywhere in your database (if you don't have a field for it, the helper will just create a virtual attribute).

class App.User extends Tower.Model
  @validates 'termsOfService', acceptance: true

The default error message for this helper is 'must be accepted'.

It can receive an accept option, which determines the value that will be considered acceptance. It defaults to '1' and can be easily changed.

class App.User extends Tower.Model
  @validates 'termsOfService', acceptance: { accept: 'yes' }

Associated

You should use this helper when your model has associations with other models and they also need to be validated. When you try to save your object, valid? will be called upon each one of the associated objects.

class App.Library extends Tower.Model
  @hasMany 'books'
  
  @validates associated: 'books'

This validation will work with all of the association types.

Don't use validatesAssociated on both ends of your associations. They would call each other in an infinite loop.

The default error message for validatesAssociated is 'is invalid'. Note that each associated object will contain its own errors collection; errors do not bubble up to the calling model.

Confirmation

You should use this helper when you have two text fields that should receive exactly the same content. For example, you may want to confirm an email address or a password. This validation creates a virtual attribute whose name is the name of the field that has to be confirmed with '_confirmation' appended.

class App.User extends Tower.Model
  @validates 'email', confirmation: true

This check is performed only if emailConfirmation is not nil. To require confirmation, make sure to add a presence check for the confirmation attribute (we'll take a look at presence later on this guide):

class App.User extends Tower.Model
  @validates 'email', confirmation: true
  @validates 'emailConfirmation', presence: true

The default error message for this helper is 'doesn't match confirmation'.

Exclusion

This helper validates that the attributes' values are not included in a given set. In fact, this set can be any enumerable object.

class App.Account extends Tower.Model
  @validates 'subdomain', exclusion: { in: ['www', 'us', 'ca', 'jp'], message: 'Subdomain %{value} is reserved.' }

The exclusion helper has an option in that receives the set of values that will not be accepted for the validated attributes. The in option has an alias called within that you can use for the same purpose, if you'd like to. This example uses the message option to show how you can include the attribute's value.

The default error message is 'is reserved'.

Format

This helper validates the attributes' values by testing whether they match a given regular expression, which is specified using the with option.

class App.Product extends Tower.Model
  @validates 'legacyCode', format: { with: /\A[a-zA-Z]+\z/, message: 'Only letters allowed' }

The default error message is 'is invalid'.

Inclusion

This helper validates that the attributes' values are included in a given set. In fact, this set can be any enumerable object.

class App.Coffee extends Tower.Model
  @validates 'size', inclusion: { in: ['small', 'medium', 'large'], message: '%{value} is not a valid size' }

The inclusion helper has an option in that receives the set of values that will be accepted. The in option has an alias called within that you can use for the same purpose, if you'd like to. The previous example uses the message option to show how you can include the attribute's value.

The default error message for this helper is 'is not included in the list'.

Length

This helper validates the length of the attributes' values. It provides a variety of options, so you can specify length constraints in different ways:

class App.User extends Tower.Model
  @validates 'name', length: { minimum: 2 }
  @validates 'bio', length: { maximum: 500 }
  @validates 'password', length: { minimum: 6, maximum: 20 }
  @validates 'registrationNumber', length: 6

The possible length constraint options are:

minimum – The attribute cannot have less than the specified length. maximum – The attribute cannot have more than the specified length. in (or within) – The attribute length must be included in a given interval. The value for this option must be a range. is – The attribute length must be equal to the given value. The default error messages depend on the type of length validation being performed. You can personalize these messages using the wrongLength, tooLong, and tooShort options and %{count} as a placeholder for the number corresponding to the length constraint being used. You can still use the message option to specify an error message.

class App.User extends Tower.Model
  @validates 'bio', length: { maximum: 1000, tooLong: '%{count} characters is the maximum allowed' }

This helper counts characters by default, but you can split the value in a different way using the tokenizer option:

class App.Essay extends Tower.Model
  @validates 'content', length:
    minimum:   300,
    maximum:   400,
    tooShort:  'must have at least %{count} words',
    tooLong:   'must have at most %{count} words'

Note that the default error messages are plural (e.g., 'is too short (minimum is %{count} characters)'). For this reason, when minimum is 1 you should provide a personalized message or use validatesPresenceOf instead. When in or within have a lower limit of 1, you should either provide a personalized message or call presence prior to length.

The size helper is an alias for length.

Numericality

This helper validates that your attributes have only numeric values. By default, it will match an optional sign followed by an integral or floating point number. To specify that only integral numbers are allowed set onlyInteger to true.

If you set onlyInteger to true, then it will use the

/\A[+-]?\d+\Z/

regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using Float.

Note that the regular expression above allows a trailing newline character.

class App.Player extends Tower.Model
  @validates 'points', numericality: true
  @validates 'gamesPlayed', numericality: { onlyInteger: true }

Besides onlyInteger, this helper also accepts the following options to add constraints to acceptable values:

greaterThan – Specifies the value must be greater than the supplied value. The default error message for this option is 'must be greater than %{count}'. greaterThanOrEqualTo – Specifies the value must be greater than or equal to the supplied value. The default error message for this option is 'must be greater than or equal to %{count}'. equalTo – Specifies the value must be equal to the supplied value. The default error message for this option is 'must be equal to %{count}'. lessThan – Specifies the value must be less than the supplied value. The default error message for this option is 'must be less than %{count}'. lessThanOrEqualTo – Specifies the value must be less than or equal the supplied value. The default error message for this option is 'must be less than or equal to %{count}'. odd – Specifies the value must be an odd number if set to true. The default error message for this option is 'must be odd'. even – Specifies the value must be an even number if set to true. The default error message for this option is 'must be even'. The default error message is 'is not a number'.

Presence

This helper validates that the specified attributes are not empty. It uses the blank? method to check if the value is either nil or a blank string, that is, a string that is either empty or consists of whitespace.

class App.User extends Tower.Model
  @validates 'name', 'login', 'email', presence: true

If you want to be sure that an association is present, you'll need to test whether the foreign key used to map the association is present, and not the associated object itself.

class App.LineItem extends Tower.Model
  @belongsTo 'order'

  @validates 'orderId', presence: true

Since false.blank? is true, if you want to validate the presence of a boolean field you should use validates fieldName, inclusion: { in: [true, false] }.

The default error message is 'can't be empty'.

Uniqueness

This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index in your database.

class App.Account extends Tower.Model
  @validates 'email', uniqueness: true

The validation happens by performing an SQL query into the model's table, searching for an existing record with the same value in that attribute.

There is a scope option that you can use to specify other attributes that are used to limit the uniqueness check:

class App.Holiday extends Tower.Model
  @validates 'name', uniqueness: { scope: 'year', message: 'should happen once per year' }

There is also a caseSensitive option that you can use to define whether the uniqueness constraint will be case sensitive or not. This option defaults to true.

class App.User extends Tower.Model
  @validates 'name', uniqueness: { caseSensitive: false }

Note that some databases are configured to perform case-insensitive searches anyway.

The default error message is 'has already been taken'.

Relations

One-to-One Relationships

One to one relationships where the children are referenced in the parent record are defined using Tower's @hasOne and @belongsTo macros.

Defining

The parent record of the relation should use the @hasOne macro to indicate is has 1 referenced child, where the record that is referenced in it uses @belongsTo.

class App.User extends Tower.Model
  @hasOne "game"

class App.Game extends Tower.Model
  @field 'name', type: 'String'
  
  @belongsTo 'user'

Definitions are required on both sides to the relation in order for it to work properly.

Storage

When defining a relation of this nature, each record is stored in it's respective collection, but the child record contains a "foreign key" reference to the parent.

# The parent user record.
{ "_id" : ObjectId("4d3ed089fb60ab534684b7e9") }

# The child post record.
{
  "_id" : ObjectId("4d3ed089fb60ab534684b7f1"),
  "user_id" : ObjectId("4d3ed089fb60ab534684b7e9")
}

Accessors

Accessing the relations is handled through the methods created based on the names of the relations. The following example shows basic access on both sides of the relation.

# Return the child game.
user.get('game')

# Set the child game.
user.set('game', [ App.Game.build() ])

# Return the parent user.
game.get('user')

# Set the parent user.
game.get('user').build(App.User.build())

Build and Create

From the parent side, records in the referenced child can be initialized or created using the specially defined methods.

# Create a new child game given the provided attributes.
user.get('game').build(name: "Tron")

# Create a persisted child game.
user.get('game').create(name: "Tron")

# Replace the parent with a new one from the attributes.
game.get('user').build(title: "Prince")

# Replace the parent with a newly saved one from the attributes.
game.get('user').create(title: "Prince")

=== Removal

Documents in the referenced many can be removed by either calling delete on the child or setting it to nil.

# Delete the child record
user.get('game').destroy()
user.set('game', nil)

Polymorphic behavior

When a child referenced record can belong to more than one type of parent record, you can tell Tower to support this by adding the as option to the definition on the parents, and the polymorphic option on the child.

class App.Arcade extends Tower.Model
  @hasOne 'game', as: 'playable'

class App.User extends Tower.Model
  @hasOne 'game', as: 'playable'

class App.Game extends Tower.Model
  @belongsTo 'playable', polymorphic: true

Dependent Behavior

You can tell Tower what to do with child relations of a has one when unsetting the relation via the dependent option. The valid options are:

  • 'destroy' Destroy the child record.
  • 'nullify' Orphan the child record.

Polymorphic example illustrating different kinds of dependency behavior:

class App.User extends Tower.Model
  @hasOne 'game', as: 'playable', dependent: 'destroy'

class App.Arcade extends Tower.Model
  @hasOne 'game', as: 'playable', dependent: 'nullify'

# Deletes the existing game on the user.
user.set('game', [])
user.set('game', null)

# Orphans the existing game on the arcade (no delete).
user.set('game', [])
user.set('game', null)

If the dependent option is not defined, the default is to nullify.

One-to-Many Relationships

One to many relationships where the children are stored in a separate collection from the parent record are defined using Tower's hasMany and belongsTo macros. This exhibits similar behavior to Rails' Active Record.

Defining

The parent record of the relation should use the hasMany macro to indicate is has n number of referenced children, where the record that is referenced uses belongsTo.

class App.User extends Tower.Model
  @hasMany 'posts'

class App.Post extends Tower.Model
  @field 'title', type: 'String'
  
  @belongsTo 'user'

Definitions are required on both sides to the relation in order for it to work properly.

Accessors

Accessing the relations is handled through the methods created based on the names of the relations. The following example shows basic access on both sides of the relation.

# Return the child posts.
user.get('posts').all()

# Set the child posts.
user.get('posts').addEach([App.Post.build()])
user.get('posts').add(App.Post.build())

# Return the parent user relation.
post.get('user')

# Set the parent user.
post.get('user').create(App.User.build())

Building and Creating

From the parent side, records in the referenced child can be appended to using traditional array syntax or the special association proxy methods. On the child side the only option is to replace the existing with a newly built or created record.

# Append one or many child posts, saving them if the user is persisted.
user.get('posts').create(App.Post.build())

# Appends and returns a new child post from the attirbutes.
user.get('posts').build(title: 'Berlin never sleeps.')

# Appends, saves, and returns a new child post from the attirbutes.
user.get('posts').create(title: 'Berlin is far cooler than New York.')

# Replace the parent with a new one from the attributes.
post.get('user').build(title: 'Prince')

# Replace the parent with a newly saved one from the attributes.
post.get('user').create(title: 'Prince')

Removal

Records in the referenced many can be removed in several different manners, either through the relation, criteria, or accessors.

# Delete all referenced records
user.get('posts').destroy()

# Delete all matching referenced records.
user.get('posts').where(title: 'Berlin').destroy()

# Delete the parent referenced record.
post.get('user').destroy()

Finding

Finding records in the referenced children is handled through find or by using chained criteria on the relation.

# Find a child by a single or multiple ids.
user.get('posts').find(id)
user.get('posts').find([ idOne, idTwo ])

# Find matching referenced children.
user.get('posts').where(title: 'Berlin')

# Do any children exist that are persisted?
user.get('posts').exists()

Polymorphic Behavior

When a child referenced record can belong to more than one type of parent record, you can tell Tower to support this by adding the as option to the definition on the parents, and the polymorphic option on the child.

class App.Company extends Tower.Model
  @hasMany 'posts', as: 'postable'

class App.User extends Tower.Model
  @hasMany 'posts', as: 'postable'

class App.Post extends Tower.Model
  @belongsTo 'postable', polymorphic: true

Dependent Behavior

You can tell Tower what to do with child relations of a has many when unsetting the relation via the dependent option. This also applies to calling #destroy on the relation. The valid options are:

  • 'destroy': Destroy the child records.
  • 'nullify': Orphan the child records.
class App.Company extends Tower.Model
  @hasMany 'posts', as: 'postable', dependent: 'destroy'

class App.User extends Tower.Model
  @hasMany 'posts', as: 'postable', dependent: 'nullify'

# Delete all the child relations:

company.get('posts').destroy()

# Orphan all the child relations:
user.get('posts').destroy()

# Delete a single child relation:
company.get('posts').destroy(post)

# Orphan a single child relation:
user.get('posts').destroy(post)

If the dependent option is not defined, the default is to nullify.

Storage

The database-specific ways the data is stored is recorded in the Tower.Store section. Below we define how associations are stored in general; i.e. in MongoDB the id is actually saved in an _id field and defaults to a MongoDB specific object, so a MongoDB record with an id would look like this: { '_id' : ObjectId('4d2ed089fb60ab534684b7e9') }.

Referenced

When defining a relation of this nature, each record is stored in it's respective collection, but the child record contains a 'foreign key' reference to the parent.

# The parent `user` record.
# user.save()
{ 'id' : 123 }

# The child `post` record.
# user.get('posts').create()
{
  'id' : 987,
  'userId' : 123,
  'title': 'A Post!'
}

Embedded

Records that are embedded using the embedsMany macro are stored as an array of hashes inside the parent in the parent's database collection.

# users collection
{
  'id' : 123,
  'posts' : [
    { 
      'id' : 987,
      'title': 'A Post!'
    }
  ]
}

Cached

Sometimes you don't want to embed a record inside another, but you want a quick way to query the associated records. This is when you use the cache option, which stores the ids of the associated record in an array on the parent record.

# users collection
{
  'id' : 123,
  'postIds' : [987]
}

# posts collection
{
  'id' : 987,
  'userId' : 123,
  'title': 'A Post!'
}

Resources

Clone this wiki locally