Skip to content

Working with models and services

Marwane Kalam-Alami edited this page Apr 4, 2019 · 6 revisions

Services

Services are were the bulk of the logic happens. Fetching and sorting the comments of a blog post, computing the rankings for an event, eliminating a theme... Each of these tasks is simply exposed by a JavaScript function, and these functions are regrouped by topic in services.

A simple example from the actual code:

// post-service.js

module.exports = {
  findCommentsByUser  // (1)
}

async function findCommentsByUser (user) {  // (2)
  return models.Comment.where('user_id', user.get('id'))  // (3)
    .orderBy('created_at', 'DESC')
    .fetchAll({ withRelated: ['user'] })  // (4)
}
  1. Registers the function to expose, so that controllers (or other services) can call postService.findCommentsByUser
  2. The actual definition. Most of our service functions start the async keyword, because they do asynchronous work like querying the database, or reading/writing a file. The most common way to use that function will be with the await keyword: let comments = await postService.findCommentsByUser(user).
  3. The implementation here is a simple-ish database query, constructed with the Bookshelf library. The API call will return a Promise for a list of comments (or in Bookshelf terms, a "collection" of comment "models").
  4. If you know SQL, the main part of the Bookshelf API that may puzzle you is the withRelated bit: Bookshelf allows us here to do joins on related tables, fetching all the data we need in a single API call.

Here is a quick example of how the API Bookshelf:

let comments = await postService.findCommentsByUser(user)

// "comments" is a Bookshelf "Collection"
let firstComment = comments.get(0)

// "firstComment" is a Bookshelf Model
let firstCommentBody = firstComment.get('body')
firstComment.set('body', 'I hacked this comment')
await firstComment.save()

// "firstCommentUser" is also a Bookshelf Model, thanks to the
// `{ withRelated: ['user' ] }` option passed in the service query
let firstCommentUser = firstComment.related('user')
let userName = user.get('name')

ℹ️ If you're comfortable with Promises, you might notice that findCommentsByUser does not actually require the async keyword. We still like to use it to make Promise-returning functions recognizable at a glance.

If you need to split your function into several smaller ones, or simply want to share code inside a service, feel free to do so. The convention for internal functions is to prefix them with an underscore.

Models

The Bookshelf API masks a lot of non-exciting SQL stuff like:

  • Tables joins ;
  • Serialization/deserialization to the correct data types ;
  • Timestamp management (= creation/update date of table rows) ;
  • Properly cascading deletions.

All the magic is made possible by registering all the tables and their relations as Bookshelf "models", in the core/models.js file. Here is an example:

module.exports.Comment = bookshelf.model('Comment', {  // (1)
  tableName: 'comment',  // (2)
  idAttribute: 'id',  // (3)
  hasTimestamps: true,  // (4)

  user: function () {  // (5)
    return this.belongsTo('User', 'user_id', 'id')
  }
})
  1. Expose Comment as a Bookshelf model to be used with models.Comment
  2. Configure the actual database table that this model actually binds to
  3. Configure the primary key of the table. Note that while there are more columns to this table, we don't need to mention them here. In the actual [models.js](https://github.com/alakajam-team/alakajam/blob/master/core/models.js) file you'll notice handy comments (also rendered as this doc page) that list the columns instead, to help developers.
  4. Let Bookshelf manage timestamps through created_at/updated_at columns. You can read them, but you never need to write into them manually.
  5. Configure the user relation of the model, letting Bookshelf know which other model we link Comment to and through which foreign key.
Clone this wiki locally