Fork me on GitHub

Caboose Model

caboose-model is a MongoDB model library for caboose. It uses node-mongodb-native as the underlying driver.


Installation

To install caboose-model, use the caboose plugin install command.

$ caboose plugin install caboose-model
    

Configuration

After installation, there will be a caboose-model.json file in your config directory. This file holds the configuration for connecting to MongoDB. The initial value should look something like this.

{
  "host": "localhost",
  "port": 27017,
  "database": "new-app"
}
    

This should be fine for a while, but eventually you'll need to deploy your app to a server, like Heroku. When this happens, just move that caboose-model.json file into one of the config/environments directories.

You can also specify just the url.

{
  "url": "mongodb://user:pass@server:port/database"
}
    

Alternatively (for situations such as Heroku), you can use the config/application.coffee or config/environments/[env].coffee files to set the model configuration like this.

module.exports = (config, next) ->
  config['caboose-model'] =
    url: process.env.MONGOHQ_URL

  next()
    

Definition

Models can simply be defined in coffeescript by extending the Model class. Here is the basic model that caboose generate model Post will create.

class Post extends Model
  store_in 'post'
    

Models have attributes and methods.

Attributes generally describe the model class. So above we have a model named Post which should located in the app/models/post.coffee file and will store instances of itself in the post collection. There are a variety of attributes you can add a model, which can change it's behavior.

Methods are actually just an instance attribute with syntactic sugar. All class methods you define in your model class will just become methods of the model class, like you'd expect.

Here's a more complex model to illustrate some of what you can do.

import 'StringHelper'

class Post extends Model
  store_in 'post'

  static 'latest_timestamp', (callback) ->
    @sort({posted_at: -1}).fields({posted_at: 1}).first (err, post) ->
      return callback(err) if err?
      callback err, post.posted_at

  summary: ->
    StringHelper.summarize(@content)
    

In the above example, we first import in functionality of the StringHelper (the import method is described here) which we will use in the summary method.

Then we define the Post model and store it in the post collection just like the generated model.

Next we add a class method named latest_timestamp to the model. Class methods are defined with the static attribute and are a great way to add shortcut methods like this. In this case, we could just call Post.latest_timestamp(...) rather than writing out all the query code in a controller. It's important to realize that latest_timestamp will not be available from instances. So new Post().latest_timestamp(...) would not work.

Finally we add an instance method named summary which uses the StringHelper to provide a summary of the content of the post. The summary method could have also been defined as

instance 'summary', ->
  StringHelper.summarize(@content)
    

Supported attributes

store_in(collection_name)

Specifies the collection to persist this model to.

class User extends Model
  store_in 'user'
          
instance(method_name, method)

Adds a method to the model's prototype to be accessed by instances.
NOTE This is the same as adding a method to the class.

class User extends Model
  store_in 'user'
  instance 'set_password', (password) ->
    @password = encrypt(password)
          
static(method_name, method)

Adds a method to the model that is only available at the class level.

class User extends Model
  store_in 'user'
  static 'find_by_email', (email, callback) ->
    @where {email: email}, callback
          
property(property_name, method)

Defines a derived property on an instance.

class User extends Model
  store_in 'user'
  property 'full_name', -> "#{@first_name} #{@last_name}"
          

Finders

These methods will retrieve records or metadata from the database and should be the last method you call in a query.

first(callback)

Fetches the first record in the current query.

# Retrieve the first Post object
Post.first (err, post) ->
  # post is either null or a Post object

# Retrieve the first Post object in the query
Post.where(...).first (err, post) ->
  # post is either null or a Post object
      

array(callback)

Fetches all the records in the current query as an array.

# Retrieve all Post objects
Post.array (err, posts) ->
  # posts is either [] or an array of Post objects

# Retrieve all Post objects in the query
Post.where(...).array (err, posts) ->
  # posts is either [] or an array of Post objects
      

each(callback)

Fetches all the records in the current query and calls the callback one record at a time. When there are no more records, a null will be passed to the callback for both the error and item.

NOTE: Even if there are no records in the collection, the callback will be invoked once with nulls for both arguments.

# Retrieve all Post objects one at a time (streaming)
Post.each (err, post) ->
  # if there are no more posts, post is null
  # otherwise, post is a Post object

# Retrieve all Post objects one at a time (streaming) in the query
Post.where(...).each (err, post) ->
  # if there are no more posts, post is null
  # otherwise, post is a Post object
      

count(callback)

Counts the records in the current query.

# Counts the records in the post collection
Post.count (err, count) ->
  console.log "There are #{count} post(s)."

# Filters and counts the records in the post collection
Post.where(...).count (err, count) ->
  console.log "There are #{count} filtered post(s)."
      

distinct(key, callback)

Collects the distinct values for key.

# Collect the distinct titles from all posts
Post.distinct 'title', (err, titles) ->
  # titles is an array of the distinct post titles

# Collect the distinct titles from all posts in the query
Post.where(...).distinct 'title', (err, titles) ->
  # titles is an array of the distinct post titles in the query
      

Modifiers

These methods modify the records returned by the above finders. They return a query object that can be further modified or executed by a finder.

skip(count)

Skips count records in the current query.

# Skip the first 5 posts
Post.skip(5).first (err, post) ->
  # post is the sixth post

# Filter then skip the first 5 posts
Post.where(...).skip(5).first (err, post) ->
  # post is sixth post in this query
      

limit(count)

Limits the current query to count records.

# Retrieves the first 5 posts
Post.limit(5).array (err, posts) ->
  # posts is an array of Post objects

# Filter then retrieve the next 5 posts
Post.where(...).limit(5).array (err, posts) ->
  # posts is an array of Post objects
      

sort(fields)

Sorts the records on the MongoDB side.

# Retrieves all post records sorted by title descending
Post.sort({title: -1}).array (err, posts) ->
  # posts is an array of Post objects

# Filter then sort all posts by title ascending, created_at descending
Post.where(...).sort({title: 1, created_at: -1}).array (err, posts) ->
  # posts is an array of Post objects
      

fields(fields)

Choose the fields to include or exclude.

# Retrieve the first record
Post.fields({title: 1}).first (err, post) ->
  # Post object with only the _id and title fields

# Filter then retrieve the first record
Post.where(...).fields({title: 0}).array (err, posts) ->
  # Post object without the title field
      

Queries

Queries are written just like in the MongoDB Shell, only using the where method.

where(query)

Filters the collection using any options that MonogDB supports. There is little magic here. You can use any JSON object that would work in the find() method on the MongoDB shell. For more information on querying MongoDB, checkout the docs.

# Find all posts authored by Matt Insler with
# tags caboose or mongodb
Post.where({
  author: 'Matt Insler',
  tags: {$in: ['caboose', 'mongodb']}
}).array (err, posts) ->
  # Post object without the title field
      

Actions

save(obj, callback = null)

The save method either creates or updates a model in MongoDB. You can either pass an object to the model's save method or you can call save directly on a model.

If the object you're saving already has an _id field and exists in the database, the model will be updated. However, regardless of the existence of the _id field, if the ID does not exist in the database, save will create the object.

# Save a new user in normal mode
User.save {
  first_name: 'Jolly Green',
  last_name: 'Giant',
  email: 'jolly.green@giant.com'
}
# Save a new user in safe mode, processing a callback when finished
Post.save {
  first_name: 'Jolly Green',
  last_name: 'Giant',
  email: 'jolly.green@giant.com'
}, (err, post) ->
  # post will now have _id defined

user = new User(email: 'jolly.green@giant.com')
user.first_name = 'Jolly Green'
# Save user in normal mode without a callback
user.save()

# Save user in safe mode and process callback when finished
user.save (err, user) ->
  console.log user._id
      

update(query, update, callback = null)

The update method will update the first record that matches the query in the database. You can refer to the MongoDB docs for atomic update operators. Please note that if you're not using update modifiers, like $set, the document you are updating will be overwritten.

# Update the first user named Matt to be named Jim
User.update {first_name: 'Matt'}, {$set: {first_name: 'Jim'}}

# Same as above, but in safe mode, processing the result
User.update {first_name: 'Matt'}, {$set: {first_name: 'Jim'}}, (err) ->
  console.error(err.stack) if err?
      

update_multi(query, update, callback = null)

While update only updates the first matching record, update_multi will update all matching records.

# Update all users named Matt to be named Jim
User.update_multi {first_name: 'Matt'}, {$set: {first_name: 'Jim'}}

# Same as above, but in safe mode, processing the result
User.update_all {first_name: 'Matt'}, {$set: {first_name: 'Jim'}}, (err) ->
  console.error(err.stack) if err?
      

upsert(query, update, callback = null)

This method will either update the first matching record or insert the object. If the object is inserted, the query is combined with the update in order to create the saved object.

# Assuming an empty user collection

# This should create a new user object containing {first_name: 'Matt', last_name: 'Insler'}
User.upsert {first_name: 'Matt'}, {$set: {last_name: 'Insler'}}

# This time, the email address will be set on the user we just created.
# This also shows a safe version with callback.
User.upsert {first_name: 'Matt'},
            {$set: {email: 'matt.insler@gmail.com'}}, (err) ->
  console.error(err.stack) if err?
      

remove(query, callback = null)

The remove method deleted the records matching the specified query. Please remember that this will delete ALL matching records.

# Delete all users named Matt
User.remove {first_name: 'Matt'}

# Same as above in safe mode processing a callback
User.remove {first_name: 'Matt'}, (err) ->
  console.error(err.stack) if err?
      

find_and_modify(options, callback)

map_reduce(map, reduce, options, callback)