Skip to content

Commit

Permalink
Merge pull request #1917 from bookshelf/rg-event-docs
Browse files Browse the repository at this point in the history
Add events tutorial to documentation
  • Loading branch information
ricardograca committed Nov 22, 2018
2 parents 6cee00e + fc643a2 commit 629a914
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 45 deletions.
3 changes: 2 additions & 1 deletion .github/CONTRIBUTING.md
Expand Up @@ -233,5 +233,6 @@ just make sure to follow the process explained below in the correct order.
shouldn't publish it yet, but just save it as a draft instead.
2. Update the `CHANGELOG.md` file and update the version number of `package.json`. For the changelog just follow the
format of the previous update. In general you should link to PRs instead of issues when mentioning changes. If the PRs'
descriptions are well written they should already include any associated issues. At this point there is no need to commit and/or push these changes since that is taken care of automatically by the release scripts.
descriptions are well written they should already include any associated issues. At this point there is no need to
commit and/or push these changes since that is taken care of automatically by the release scripts.
3. Just run `npm publish` and sit back.
8 changes: 4 additions & 4 deletions lib/base/events.js
Expand Up @@ -81,10 +81,10 @@ class Events extends EventEmitter {
* A promise version of {@link Events#trigger}, returning a promise which
* resolves with all return values from triggered event handlers. If any of the
* event handlers throw an `Error` or return a rejected promise, the promise
* will be rejected. Used internally on the {@link Model#creating "creating"},
* {@link Model#updating "updating"}, {@link Model#saving "saving"}, and {@link
* Model@destroying "destroying"} events, and can be helpful when needing async
* event handlers (for validations, etc).
* will be rejected. Used internally on the {@link Model#event:creating "creating"},
* {@link Model#event:updating "updating"}, {@link Model#event:saving "saving"}, and
* {@link Model@event:destroying "destroying"} events, and can be helpful when needing
* async event handlers (e.g. for validations).
*
* @param {string} name
* The event name, or a whitespace-separated list of event names, to be
Expand Down
1 change: 1 addition & 0 deletions lib/collection.js
Expand Up @@ -137,6 +137,7 @@ const BookshelfCollection = (module.exports = CollectionBase.extend(
.tap(function(response) {
/**
* @event Collection#fetched
* @tutorial events
*
* @description
* Fired after a `fetch` operation. A promise may be returned from the
Expand Down
72 changes: 44 additions & 28 deletions lib/model.js
Expand Up @@ -735,6 +735,7 @@ const BookshelfModel = ModelBase.extend(
* event handler for async behaviour.
*
* @event Model#fetched
* @tutorial events
* @param {Model} model
* The model firing the event.
* @param {Object} response
Expand Down Expand Up @@ -830,6 +831,7 @@ const BookshelfModel = ModelBase.extend(
* may be returned from the event handler for async behaviour.
*
* @event Model#fetching:collection
* @tutorial events
* @param {Collection} collection
* The collection that is going to be fetched. At this point it's still empty since the
* fetch hasn't happened yet.
Expand All @@ -848,6 +850,7 @@ const BookshelfModel = ModelBase.extend(
* may be returned from the event handler for async behaviour.
*
* @event Model#fetched:collection
* @tutorial events
* @param {Collection} collection The collection that has been fetched.
* @param {Object} response
* The raw response from the underlying query builder. This will be an array with objects
Expand Down Expand Up @@ -938,31 +941,36 @@ const BookshelfModel = ModelBase.extend(
* // ...
* });
*
* Several events fired on the model when saving: a {@link Model#creating
* "creating"}, or {@link Model#updating "updating"} event if the model is
* being inserted or updated, and a "saving" event in either case. To
* prevent saving the model (with validation, etc.), throwing an error inside
* one of these event listeners will stop saving the model and reject the
* promise. A {@link Model#created "created"}, or {@link Model#"updated"}
* event is fired after the model is saved, as well as a {@link Model#saved
* "saved"} event either way. If you wish to modify the query when the {@link
* Model#saving "saving"} event is fired, the knex query object should is
* available in `options.query`.
*
* // Save with no arguments
* Model.forge({id: 5, firstName: 'John', lastName: 'Smith'}).save().then(function() {
* //...
* });
* Several events fire on the model when saving: a {@link Model#event:creating
* "creating"}, or {@link Model#event:updating "updating"} event if the model is
* being inserted or updated, and a "saving" event in either case.
*
* // Or add attributes during save
* Model.forge({id: 5}).save({firstName: 'John', lastName: 'Smith'}).then(function() {
* //...
* });
* To prevent saving the model (for example, with validation), throwing an error
* inside one of these event listeners will stop saving the model and reject the
* promise.
*
* // Or, if you prefer, for a single attribute
* Model.forge({id: 5}).save('name', 'John Smith').then(function() {
* //...
* });
* A {@link Model#event:created "created"}, or {@link Model#event:updated "updated"}
* event is fired after the model is saved, as well as a {@link Model#event:saved "saved"}
* event either way. If you wish to modify the query when the {@link Model#event:saving
* "saving"} event is fired, the knex query object is available in `options.query`.
*
* See the {@tutorial events} guide for further details.
*
* @example
* // Save with no arguments
* Model.forge({id: 5, firstName: 'John', lastName: 'Smith'}).save().then(function() {
* //...
* });
*
* // Or add attributes during save
* Model.forge({id: 5}).save({firstName: 'John', lastName: 'Smith'}).then(function() {
* //...
* });
*
* // Or, if you prefer, for a single attribute
* Model.forge({id: 5}).save('name', 'John Smith').then(function() {
* //...
* });
*
* @param {string=} key Attribute name.
* @param {string=} val Attribute value.
Expand Down Expand Up @@ -1050,6 +1058,7 @@ const BookshelfModel = ModelBase.extend(
* exception from the handler will cancel the save.
*
* @event Model#saving
* @tutorial events
* @param {Model} model
* The model firing the event. Its attributes are already changed but
* not commited to the database yet.
Expand All @@ -1073,6 +1082,7 @@ const BookshelfModel = ModelBase.extend(
* exception from the handler will cancel the save operation.
*
* @event Model#creating
* @tutorial events
* @param {Model} model The model firing the event.
* @param {Object} attrs
* Attributes that will be inserted.
Expand All @@ -1094,6 +1104,7 @@ const BookshelfModel = ModelBase.extend(
* exception from the handler will cancel the save operation.
*
* @event Model#updating
* @tutorial events
* @param {Model} model
* The model firing the event. Its attributes are already changed but
* not commited to the database yet.
Expand Down Expand Up @@ -1138,6 +1149,7 @@ const BookshelfModel = ModelBase.extend(
* Fired after an `insert` or `update` query.
*
* @event Model#saved
* @tutorial events
* @param {Model} model The model firing the event.
* @param {(Array|Number)} response
* A list containing the id of the newly created model in case of an
Expand All @@ -1153,6 +1165,7 @@ const BookshelfModel = ModelBase.extend(
* Fired after an `insert` query.
*
* @event Model#created
* @tutorial events
* @param {Model} model The model firing the event.
* @param {Array} newId A list containing the id of the newly created model.
* @param {Object} options Options object passed to {@link Model#save save}.
Expand All @@ -1165,6 +1178,7 @@ const BookshelfModel = ModelBase.extend(
* Fired after an `update` query.
*
* @event Model#updated
* @tutorial events
* @param {Model} model The model firing the event.
* @param {Number} affectedRows Number of rows affected by the update.
* @param {Object} options Options object passed to {@link Model#save save}.
Expand All @@ -1180,12 +1194,12 @@ const BookshelfModel = ModelBase.extend(
* `destroy` performs a `delete` on the model, using the model's {@link
* Model#idAttribute idAttribute} to constrain the query.
*
* A {@link Model#destroying "destroying"} event is triggered on the model before being
* destroyed. To prevent destroying the model (with validation, etc.), throwing an error
* inside one of these event listeners will stop destroying the model and
* reject the promise.
* A {@link Model#event:destroying "destroying"} event is triggered on the model
* before being destroyed. To prevent destroying the model, throwing an error
* inside one of the event listeners will stop destroying the model and reject the
* promise.
*
* A {@link Model#destroyed "destroyed"} event is fired after the model's
* A {@link Model#event:destroyed "destroyed"} event is fired after the model's
* removal is completed.
*
* @method Model#destroy
Expand Down Expand Up @@ -1226,6 +1240,7 @@ const BookshelfModel = ModelBase.extend(
* will reject the promise and cancel the deletion.
*
* @event Model#destroying
* @tutorial events
* @param {Model} model The model firing the event.
* @param {Object} options Options object passed to {@link Model#destroy destroy}.
* @returns {Promise}
Expand All @@ -1248,6 +1263,7 @@ const BookshelfModel = ModelBase.extend(
* handler for async behaviour.
*
* @event Model#destroyed
* @tutorial events
* @param {Model} model The model firing the event.
* @param {Object} options Options object passed to {@link Model#destroy destroy}.
* @returns {Promise}
Expand Down
2 changes: 2 additions & 0 deletions lib/sync.js
Expand Up @@ -95,6 +95,7 @@ _.extend(Sync.prototype, {
* returned from the event handler for async behaviour.
*
* @event Model#counting
* @tutorial events
* @param {Model} model The model firing the event.
* @param {Object} options Options object passed to {@link Model#count count}.
* @returns {Promise}
Expand Down Expand Up @@ -175,6 +176,7 @@ _.extend(Sync.prototype, {
* event handler for async behaviour.
*
* @event Model#fetching
* @tutorial events
* @param {Model} model
* The model which is about to be fetched.
* @param {string[]} columns
Expand Down
10 changes: 6 additions & 4 deletions scripts/jsdoc.sh
@@ -1,6 +1,5 @@
#!/bin/bash -e

static_assets=./.jsdoc-temp
static_assets=.jsdoc-temp

# Clean existing docs
rm -rf ./docs
Expand All @@ -9,8 +8,11 @@ rm -rf ./docs
rm -rf $static_assets
mkdir $static_assets

# Then create the docs.
# Create the docs.
$(npm bin)/jsdoc --configure ./scripts/jsdoc.config.json

# Now remove temporary folder to clean up.
# Copy file required for custom GitHub pages domain name
cp CNAME docs/

# Remove temporary folder to clean up.
rm -rf $static_assets
12 changes: 5 additions & 7 deletions scripts/postpublish.sh
Expand Up @@ -11,17 +11,15 @@ echo "(Re)Creating bookshelf-source remote"
git remote remove bookshelf-source || true
git remote add bookshelf-source git@github.com:bookshelf/bookshelf.git

echo "Committing new release version w/ any outstanding changes"
echo "Regenerating documentation"
npm run jsdoc

echo "Committing new release version ($version) w/ any outstanding changes"
git commit -am "Release $version"

echo "Tagging version w/ number"
git tag $version

echo "Pushing commit to source master"
echo "Pushing commit and tags to source master"
git push bookshelf-source master

echo "Pushing new version tag to source master"
git push bookshelf-source master --tags

echo "Running gh-pages publish"
npm run jsdoc
110 changes: 110 additions & 0 deletions tutorials/events.md
@@ -0,0 +1,110 @@
Events allow you to tap into the life cycle of a query request and perform actions before and after the query is
executed. This can be used for adding validation before saving a model, hashing a password or calling some external
process after a model is saved.

The way this is done is by attaching event handlers to a model that will listen for these events. If the event
handlers return Promises, the handlers will wait for the Promise to be resolved or rejected before progressing. If
the promise is rejected, the process will be interrupted.

### Listening to events

In order to attach an event listener to a model you can do it in the {@link Model#initialize Model's initialize}
method like so:

```js
const User = bookshelf.Model.extend({
tableName: 'users',

initialize() {
this.on('updated', (model) => {
// This is fired after a model is updated
})
}
})
```

You can attach multiple event listeners for the same event by calling the {@link Model#on on} method multiple times
and the listeners will run sequentially.

### Available save events

The available {@link Model model} save related events are:

- {@link Model#event:saving saving}
- {@link Model#event:creating creating}
- {@link Model#event:updating updating}
- {@link Model#event:created created}
- {@link Model#event:updated updated}
- {@link Model#event:saved saved}

They are fired in this order and it's possible to prevent the request from advancing further by throwing an error or
returning a rejected Promise from any one of these event listeners, e.g.:

```js
const User = bookshelf.Model.extend({
tableName: 'users',

initialize() {
this.on('saving', (model) => {
if (model.get('status') !== 'active') {
// Throwing an error will prevent the model from being saved
throw new Error('Cannot save inactive user')
}
})

this.on('saved', (model) => {
// This won't be reached if the previous event threw an error
})
}
})
```

This feature can be used to perform validation before saving a model. For example, checking if an email address
already exists and preventing the model from being saved if it does:

```js
const User = bookshelf.Model.extend({
tableName: 'users',

initialize() {
this.on('saving', (model) => {
return User.forge({email: model.get('email')}).fetch().then((user) => {
if (user) throw new Error('That email address already exists')
})
})
}
})
```

### Available fetch related events

The available {@link Model model} data retrieval related events are:

- {@link Model#event:fetching fetching}
- {@link Model#fetching:collection fetching:collection}
- {@link Model#event:counting counting}
- {@link Model#event:fetched fetched}
- {@link Model#fetched:collection fetched:collection}

### Available destroy related events

The available {@link Model model} destruction related events are:

- {@link Model#event:destroying destroying}
- {@link Model#event:destroyed destroyed}

Note that you can get the model's previous attributes after it's destroyed by calling the
{@link Model#previousAttributes previousAttributes} method:

```js
const User = bookshelf.Model.extend({
tableName: 'users',

initialize() {
this.on('destroyed', (model) => {
const previousAttributes = JSON.stringify(model.previousAttributes())
log(`Destroyed model with attributes: ${previousAttributes}`)
})
}
})
```
11 changes: 10 additions & 1 deletion tutorials/models.md
@@ -1 +1,10 @@
Models are the backbone of Bookshelf.
Models are the backbone of Bookshelf. They encapsulate most of the functionality that you'll need to work with your
data.

### Events

There are several events fired by models at different stages of the query request process. For more information about
this see the {@tutorial events} guide.

**Note**: This section of the documentation is still a WIP. For more in-depth information about models check out the
{@link Model API Reference}.
3 changes: 3 additions & 0 deletions tutorials/tutorials.json
Expand Up @@ -19,6 +19,9 @@
"models": {
"title": "Models",
"children": {
"events": {
"title": "Events"
},
"parse-and-format": {
"title": "Parse and Format"
}
Expand Down

0 comments on commit 629a914

Please sign in to comment.