-
Notifications
You must be signed in to change notification settings - Fork 13
Working with models and 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)
}
- Registers the function to expose, so that controllers (or other services) can call
postService.findCommentsByUser
- 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 theawait
keyword:let comments = await postService.findCommentsByUser(user)
. - 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").
- 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 theasync
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.
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')
}
})
- Expose Comment as a Bookshelf model to be used with
models.Comment
- Configure the actual database table that this model actually binds to
- 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. - Let Bookshelf manage timestamps through
created_at
/updated_at
columns. You can read them, but you never need to write into them manually. - Configure the
user
relation of the model, letting Bookshelf know which other model we link Comment to and through which foreign key.