You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
7.4 KiB
240 lines
7.4 KiB
3 years ago
|
# Milestones
|
||
|
|
||
|
Milestones provide opportunities to run custom application code at various important steps throughout the duration of the request.
|
||
|
|
||
|
Resources have properties for each controller action: `create`, `list`, `read`, `update`, and `delete`.
|
||
|
Also find a meta property `all` as a convenience for hooking into milestones across all controllers.
|
||
|
Each of those properties in turn has methods for setting custom behavior.
|
||
|
|
||
|
For each milestone on a given controller we accept a function to specify custom behavior. If multiple functions are
|
||
|
registered for a hook they will be ran in order.
|
||
|
|
||
|
Functions can expect three parameters: a request, a response, and a context object.
|
||
|
|
||
|
For example to run before the main fetch milestone:
|
||
|
|
||
|
```javascript
|
||
|
// check the cache first
|
||
|
users.list.fetch.before(function(req, res, context) {
|
||
|
var instance = cache.get(context.criteria);
|
||
|
|
||
|
if (instance) {
|
||
|
// keep a reference to the instance and skip the fetch
|
||
|
context.instance = instance;
|
||
|
return context.skip;
|
||
|
} else {
|
||
|
// cache miss; we continue on
|
||
|
return context.continue;
|
||
|
}
|
||
|
})
|
||
|
```
|
||
|
|
||
|
We have the following milestones:
|
||
|
|
||
|
- start - ran at the beginning of the request
|
||
|
- auth - authorize the request
|
||
|
- fetch - fetch data from the database
|
||
|
- data - transform the database data
|
||
|
- write - write to the database
|
||
|
- send - send response to the user
|
||
|
- complete - request completed
|
||
|
|
||
|
Only send is defined for all actions, and defaults to returning the json format of context.instance to the browser.
|
||
|
|
||
|
# Default Milestones for actions
|
||
|
|
||
|
## create
|
||
|
|
||
|
### write
|
||
|
|
||
|
Reads from context.attributes and req.body and creates a new instance of the model. Causes the response to have a
|
||
|
HTTP 201 response and sets the instance on context.instance
|
||
|
|
||
|
## delete
|
||
|
|
||
|
### fetch
|
||
|
|
||
|
Fetches from the database based on the endpoint parameters.
|
||
|
Sets context.instance to the data if it is found, otherwise throws a 404 error.
|
||
|
|
||
|
### write
|
||
|
|
||
|
Deletes the instance from the database and clears context.instance
|
||
|
|
||
|
## list
|
||
|
|
||
|
### fetch
|
||
|
|
||
|
Fetches a list from the database based on the URL parameters and endpoint configuration. Sets context.instance to the
|
||
|
returned list.
|
||
|
|
||
|
## read
|
||
|
|
||
|
### fetch
|
||
|
|
||
|
Fetches data from the database based on the request parameters and endpoint configuration and sets context.instance to
|
||
|
the returned object or throws a 404 error if none found that match
|
||
|
|
||
|
# Milestone Flow
|
||
|
|
||
|
Inside the function you must do one of the following:
|
||
|
|
||
|
1. return either `context.stop`, `context.skip` or `context.continue`
|
||
|
2. return a promise/thenable that resolves to `context.stop`, `context.skip` or `context.continue`
|
||
|
3. call either `context.stop()`, `context.skip()` or `context.continue()`
|
||
|
4. throw an error
|
||
|
5. call `context.error(err)` with an error to show
|
||
|
|
||
|
Examples:
|
||
|
|
||
|
```javascript
|
||
|
// example for 1
|
||
|
users.list.fetch.before(function(req, res, context) {
|
||
|
return context.continue;
|
||
|
});
|
||
|
|
||
|
// example for 2 and 4
|
||
|
var ForbiddenError = require('epilogue').Errors.ForbiddenError;
|
||
|
|
||
|
users.list.fetch.before(function(req, res, context) {
|
||
|
return checkLoggedIn(function(loggedIn) {
|
||
|
if(!loggedIn) {
|
||
|
throw new ForbiddenError();
|
||
|
}
|
||
|
return context.continue;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// example for 3 and 5
|
||
|
users.list.fetch.before(function(req, res, context) {
|
||
|
passport.authenticate('bearer', function(err, user, info) {
|
||
|
if(err) {
|
||
|
res.status(500);
|
||
|
return context.stop();
|
||
|
}
|
||
|
|
||
|
if(user) {
|
||
|
context.continue();
|
||
|
} else {
|
||
|
context.error(new ForbiddenError());
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
```
|
||
|
|
||
|
## context.continue
|
||
|
|
||
|
This says to continue on to the next hook in the chain.
|
||
|
|
||
|
## context.skip
|
||
|
|
||
|
This skips all the remaining functions on a milestone and skips to the start of the next one
|
||
|
|
||
|
## context.stop
|
||
|
|
||
|
This stops execution of all the remaining hooks and milestones and finishes the request. If you use context.stop you
|
||
|
should also set the response `res.status(200).json({})`
|
||
|
|
||
|
## context.error(error)
|
||
|
|
||
|
This should be called when throwing an error won't work. If you are returning a promise or working sync then throwing
|
||
|
an error will be better. If more than one parameter is passed to `context.error` then this function will assume the
|
||
|
syntax `context.error(status, message, [errors], [cause])` and will create a new EpilogueError out of them and will use
|
||
|
that instead.
|
||
|
|
||
|
# Errors
|
||
|
|
||
|
Throwing an error within a milestone function will cause the milestones to stop execution and the error message will be displayed to the user in the format:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"message": "Error message",
|
||
|
"errors": []
|
||
|
}
|
||
|
```
|
||
|
|
||
|
To make use of this you must use an error that extends EpilogueError. Any error thrown that is not an instance of
|
||
|
EpilogueError will be shown as an Internal Server Error with it's errors array set to the error message.
|
||
|
|
||
|
In situations where throwing an error is not possible you can use `context.error(err)` with the error object or
|
||
|
alternatively call `context.error(status, message, [errors], [cause])` for it to build an EpilogueError for you with the
|
||
|
supplied parameters
|
||
|
|
||
|
The following errors are provided on epilogue.Errors:
|
||
|
|
||
|
## EpilogueError(statusCode, message, errors, cause)
|
||
|
|
||
|
This is the parent class you can extend to make your own errors
|
||
|
|
||
|
`statusCode` = the HTTP status code to return, defaults to 500
|
||
|
`message` = the error message to show in the message field, defaults to 'EpilogueError'
|
||
|
`errors` = an array of error strings to show in the message, defaults to []
|
||
|
`cause` = the original error that caused this error, defaults to undefined (no cause)
|
||
|
|
||
|
## BadRequestError(message, errors, cause)
|
||
|
|
||
|
This returns a HTTP 400 response
|
||
|
|
||
|
`message` = the error message to show in the message field, defaults to 'Bad Request'
|
||
|
`errors` = an array of error strings to show in the message, defaults to []
|
||
|
`cause` = the original error that caused this error, defaults to undefined (no cause)
|
||
|
|
||
|
## ForbiddenError(message, errors, cause)
|
||
|
|
||
|
This returns a HTTP 403 response
|
||
|
|
||
|
`message` = the error message to show in the message field, defaults to 'Forbidden'
|
||
|
`errors` = an array of error strings to show in the message, defaults to []
|
||
|
`cause` = the original error that caused this error, defaults to undefined (no cause)
|
||
|
|
||
|
## NotFoundError(message, errors, cause)
|
||
|
|
||
|
This returns a HTTP 404 response
|
||
|
|
||
|
`message` = the error message to show in the message field, defaults to 'Not Found'
|
||
|
`errors` = an array of error strings to show in the message, defaults to []
|
||
|
`cause` = the original error that caused this error, defaults to undefined (no cause)
|
||
|
|
||
|
# Overriding Error formatting
|
||
|
|
||
|
You can override how errors are formatted for a milestone by setting the `action.error` function.
|
||
|
|
||
|
For example:
|
||
|
|
||
|
```javascript
|
||
|
resource.create.error = function(req, res, error) {
|
||
|
res.status(500);
|
||
|
res.json({message: 'Internal Error'});
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The error object passed will be an EpilogueError. If the error is wrapping another error (for example a ValidationError
|
||
|
from Sequelize) the original error can be found at `error.cause`
|
||
|
|
||
|
```javascript
|
||
|
resource.create.error = function(req, res, error) {
|
||
|
if(error.cause && error.cause instanceof sequelize.ValidationError) {
|
||
|
res.status(400);
|
||
|
res.json({message: 'Bad Request'});
|
||
|
} else {
|
||
|
res.status(500);
|
||
|
res.json({message: 'Internal Error'});
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The default implementation is to do the following:
|
||
|
|
||
|
```javascript
|
||
|
res.status(err.status);
|
||
|
res.json({
|
||
|
message: err.message,
|
||
|
errors: err.errors
|
||
|
});
|
||
|
```
|
||
|
|
||
|
All Sequelize ValidationError objects are wrapped in a BadRequestError and are accessed on error.cause.
|
||
|
Any EpilogueError object are passed as-is and any other errors are wrapped in an EpilogueError and can be accessed on
|
||
|
error.cause
|