sails-hook-api-utils
v0.2.1
Published
A set of utilities for backend restful sails applications: policies, services, authentication, count in blueprint methods, standard responses, etc
Downloads
5
Maintainers
Readme
sails-hook-api-utils
A Sails.js hook with some useful utilites for design a restful api backend.
Features
- Standard json responses with a basic structure
- JWT authentication with passport.js using local and jwt strategies
- Policies for check autheticated user and for model validation with hapi/joi
- Pagination support for blueprint find method.
Standard json responses with basic structure
Some basic standard api restful responses made with my own judgment and using some useful examples from https://medium.com/@bojanmajed/standard-json-api-response-format-c6c1aabcaa6d
- If you really like standars, take a look to the future: https://www.hydra-cg.com/
Error Response (res.errorResponse)
A base and basic error response to be used in all your backend
{
"success": false,
"message" "A custom error message",
"data": {}
}
Sails use:
// status: Any 5xx or 4xx error
return res.errorResponse({
status: 400,
message: 'Oh no, you mess it up',
data: {
aditionalData: 'Try again'
}
})
Success Response (res.errorResponse)
A base and basic success response to be used in all your backend
{
"success": true,
"message" "A custom info message",
"data": {}
}
Sails use:
// status: Any valid and successful http code
return res.successResponse({
status: 200,
message: 'Oh yes, you rock',
data: {
aditionalData: 'I am proud of you'
}
})
Bad Request Response (res.badRequest) [Sails.js default badRequest override]
An override of sails badRequest using errorResponse as base
Sails use:
return res.badRequest()
// OUTPUT -> Hardcoded code: 400 (Sorry bro, spanish for now)
{
"success": false,
"message": "Solicitud inválida, revisar datos enviados",
"data": {}
}
return res.badRequest(new Error('A nice error message'))
// OUTPUT in PRODUCTION -> Hardcoded code: 400
{
"success": false,
"message": "A nice error message",
"data": {}
}
// OUTPUT in DEVELOPMENT -> Hardcoded code: 400
{
"success": false,
"message": "A nice error message",
// Stack error [nice for debug ;)], for example
"data": "t Object.module.exports [as someFile] (/your/nice/project/src/your/well/formed/response/someFile.js:67:20)"
}
Forbidden Response (res.forbidden) [Sails.js default forbidden override]
An override of sails forbidden using errorResponse as base
Sails use:
return res.forbidden()
// OUTPUT -> Hardcoded code: 403
{
"success": false,
"message": "Acceso prohibido",
"data": {}
}
return res.forbidden(new Error('Ups, someone is trying to make bad things'))
// OUTPUT in PRODUCTION -> Hardcoded code: 403
{
"success": false,
"message": "Acceso prohibido",
"data": {}
}
// OUTPUT in DEVELOPMENT -> Hardcoded code: 403
{
"success": false,
"message": "Acceso prohibido",
// Stack error [nice for debug ;)], for example
"data": "t Object.module.exports [as someFile] (/your/nice/project/src/your/well/formed/response/someFile.js:67:20)"
}
Not Found Response (res.notFound) [Sails.js default notFound override]
An override of sails notFound using errorResponse as base
Sails use:
return res.notFound()
// OUTPUT -> Hardcoded code: 404
{
"success": false,
"message": "Recurso no encontrado",
"data": {}
}
return res.notFound(new Error('Ups, someone is trying to access /wordpress/admin.php'))
// OUTPUT in PRODUCTION -> Hardcoded code: 404
{
"success": false,
"message": "Recurso no encontrado",
"data": {}
}
// OUTPUT in DEVELOPMENT -> Hardcoded code: 404
{
"success": false,
"message": "Recurso no encontrado",
// Stack error [nice for debug ;)], for example
"data": "t Object.module.exports [as someFile] (/your/nice/project/src/your/well/formed/response/someFile.js:67:20)"
}
OK Response (res.ok) [Sails.js default ok override]
An override of sails ok response, using successResponse as base. In this hook, this response is intended to be used to override the use in blueprints actions. All methods are considered, please read: https://sailsjs.com/documentation/concepts/blueprints/blueprint-routes#?restful-blueprint-routes
To be more specific, i created this override to not make a use of the ok response in other places, just to make blueprints responses more standard.
I strongly recommend you to read the ./responses/ok.js
file to make it clear.
Sails blueprints output examples:
// OUTPUT for findOne -> GET /boat/1 -> Hardcoded code: 200
{
"success": true,
"message": "Operacion 'findOne' sobre la entidad 'boat'",
"data": {
"result": {
"id": 1,
"name": "Malbec",
"city": "Santa Fe, Argentina"
"drivers": [1, 2]
}
}
}
// OUTPUT for remove -> DELETE /boat/1/drivers/2 -> Hardcoded code: 200
{
"success": true,
"message": "Operacion 'remove' sobre la entidad 'boat' y la relation 'drivers'",
"data": {
"result": {
"id": 1,
"name": "Malbec",
"city": "Santa Fe, Argentina"
"drivers": [1]
}
}
}
// All other variants are the same
Get Collection Response (res.getCollection) [custom response]
A custom response for restful Get Collection endpoints
Sails use:
let responseData = {
results: myEntityCollectionArrayEtc,
pagination: {
totalPages: 1,
perPage: 20,
totalEntries: 2
}
}
return res.getCollection(responseData)
// OUTPUT -> Hardcoded code: 200
{
"success": false,
"message": "Collection resuelta correctamente",
"data": {
"results": [
{
"id": 1,
"name": "Malbec",
"city": "Santa Fe, Argentina"
"drivers": [1, 2]
},
{
"id": 2,
"name": "Lecth",
"city": "Santa Fe, Argentina"
"drivers": [3]
}
],
"pagination": {
"totalPages": 1,
"perPage": 20,
"totalEntries": 2
}
}
}
JWT authentication with passport.js
JWT authentication ready with:
- One method to use in logIn actions
- One policy to check if user isAuthenticated (see next section)
To use passport you need to:
Config hook variables
- Create a file
your-project/config/apiUtils.js
with
module.exports.apiUtils = {
passport: {
userModelName: 'user', // Your User model name, please use 'lower case'
jwt: {
privateKey: 'your-4096-bits-aes256-private-key'
publicKey: 'your-public-key',
passphrase: 'your-passphrase',
expiresIn: '2 days' // expression of expires time of the token otherExamples: '60 minutes', '12 hours', etc.
}
}
}
- Generate keys with:
$ openssl genpkey -out private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
$ openssl pkey -in private.pem -out config/jwt/public.pem -pubout
- Inside your login action use it like this:
// UserController
module.exports = {
login: (req, res) => {
sails.hooks.apiUtils.passport.authenticate('local', sails.hooks.apiUtils.passport.handleLogin(req, res))(req, res)
}
}
Seems to easy? passport.authenticate()
recieves two parameters: first, the strategy, in this case 'local'; second, a function to handle that authentication that recieves (error, user). Seems noisy couse handleLogin is a high order function that recieves req and res and return that function passport needs (simple and random explanation https://dev.to/damcosset/higher-order-functions-in-javascript-4j8b). You can check both implementations at ./passport/passport.js
.
Policies for check autheticated user and for model validation with hapi/joi
You have the next custom policies to be used with this hook:
isAuthenticated
To protect endpoints that must, at least, have a logged in user.
Important: in many situations yo will need to add more than this policy, for example isAuthenticated and isAdmin(you must implement your owm isAdmin policy)
Validations
- Check if JWT token is present in the authorization header of the request
- Check if the JWT token is valid
- Check if the user inside the JWT payload is a valid user
- PLUS: If the user is present in the request header, add the user object related in the req express object.
- Too hard to understand this last point? Easy -> if you use this policy in one of your actions, inside that action you can access req.user and you will have the user logged in :)
modelValidation
This policy has been created to be used in blueprints actions for create and update, but you can use it in any custom action.
IMPORTANT: this policy works only if you adjust your sails model to use hapi/joi validations
How to use
- Blueprint Variant: You can add this policy to any blueprint action that requires validation, you online need to add 'modelValidation' as policy.
- Custom Action Variant: Another action that recibes a req where the req.body corresponds to a model. For example if you create your own create (or update) action to create (or update) a model entity, you have to:
- Add 'modelValidation' policy to that route (/config/policies.js)
- Add a 'model' property to the bind route-action ( for example inside your routes.js file
'GET /api/v1/cat': { controller: 'CatController', action:'create', model:'youlowercasecatmodel' }
)
Validations
Checks whatever joi schema validation you add to your sails model and outputs a res.badRequest response with the text of the first error of the schema, otherwise proceed with the action. You just need to do something like this:
/**
* Usuario.js
*
* @description :: A model definition represents a database table/collection.
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/
// ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗ ╦╔═╗╦
// ║║║║╠═╝║ ║╠╦╝ ║ ║║ ║║
// ╩╩ ╩╩ ╚═╝╩╚═ ╩ ╚╝╚═╝╩
const Joi = require('@hapi/joi')
// ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗ ╔═╗╦ ╦╔═╗╔╦╗╔═╗╔╦╗ ╦ ╔═╗╔╗╔╔═╗
// ║║║║╠═╝║ ║╠╦╝ ║ ║ ║ ║╚═╗ ║ ║ ║║║║ ║ ╠═╣║║║║ ╦
// ╩╩ ╩╩ ╚═╝╩╚═ ╩ ╚═╝╚═╝╚═╝ ╩ ╚═╝╩ ╩ ╩═╝╩ ╩╝╚╝╚═╝
// ╔╦╗╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗ ╦╔═╗ ╦ ╦╔═╗╦ ╦ ╦ ╦╔═╗╔╗╔╔╦╗
// ║║║║╣ ╚═╗╚═╗╠═╣║ ╦║╣ ╚═╗ ║╠╣ ╚╦╝║ ║║ ║ ║║║╠═╣║║║ ║
// ╩ ╩╚═╝╚═╝╚═╝╩ ╩╚═╝╚═╝╚═╝ ╩╚ ╩ ╚═╝╚═╝ ╚╩╝╩ ╩╝╚╝ ╩
// Spanish source: https://gist.github.com/mrtnzagustin/a11823c32ba4957f68df52952f62d415
const spanishErrors = require('../../config/locales/joi_es_ES.json')
module.exports = {
attributes: {
id: {
columnName: 'id',
columnType: 'bigint(20)',
type: 'number',
autoIncrement: true
},
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
username: {
type: 'string',
columnType: 'varchar(50)',
minLength: 6,
required: true
},
password: {
type: 'string',
columnType: 'varchar(255)',
required: true
},
nombre: {
type: 'string',
columnType: 'varchar(255)',
required: true
},
apellido: {
type: 'string',
columnType: 'varchar(255)',
required: true
},
email: {
type: 'string',
columnType: 'varchar(255)',
required: true
},
telefono: {
type: 'string',
columnType: 'varchar(255)',
required: true
}
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
},
// ╦╔═╗╦ ╔═╗╔═╗╦ ╦╔═╗╔╦╗╔═╗ ╔╦╗╔═╗╔═╗╦╔╗╔╦╔╦╗╦╔═╗╔╗╔
// ║║ ║║ ╚═╗║ ╠═╣║╣ ║║║╠═╣ ║║║╣ ╠╣ ║║║║║ ║ ║║ ║║║║
// ╚╝╚═╝╩ ╚═╝╚═╝╩ ╩╚═╝╩ ╩╩ ╩ ═╩╝╚═╝╚ ╩╝╚╝╩ ╩ ╩╚═╝╝╚╝
joiSchema: function () {
return Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(50)
.required()
.messages(spanishErrors),
email: Joi.string()
.required()
.email({ minDomainSegments: 2 })
.messages(spanishErrors),
password: Joi.string()
.required()
.messages(spanishErrors),
nombre: Joi.string()
.required()
.messages(spanishErrors),
apellido: Joi.string()
.required()
.messages(spanishErrors),
telefono: Joi.string()
.required()
.messages(spanishErrors)
})
},
customToJSON: function () {
return _.omit(this, ['password'])
},
beforeCreate: function (valuesToSet, proceed) {
// Hash password
// This helper is included with sails-hook-organics
sails.helpers.passwords.hashPassword(valuesToSet.password).exec((err, hashedPassword) => {
if (err) { return proceed(err) }
valuesToSet.password = hashedPassword
return proceed()
})
}
}
Example success response:
{
"success": true,
"message": "Operacion 'create' sobre la entidad 'cat'",
"data": {
"result": {
"id": 1,
"name": "Pompón",
"owner": "Juan Agustín Martínez",
"favoriteSong": "Soft kitty. Warm kitty. Little ball of fur."
}
}
}
Example error (validation working) response:
{
"success": false,
"message": "Error en campo 'apellido': es requerido",
"data": {}
}
Pagination support for blueprint find method.
Theres is a simple but powerful find.js re-implementation to add pagination capatilties. Its just a expand of the original find.js you can found at https://github.com/balderdashy/sails/tree/master/lib/hooks/blueprints/actions/find.js
How to use it
Just call it from your action:
/**
* CatController
*
* @description :: Server-side actions for handling incoming requests.
* @help :: See https://sailsjs.com/docs/concepts/actions
*/
module.exports = {
find: (req, res) => {
sails.hooks.apiUtils.customBlueprint.find(req, res)
}
}
Also you can set wich response should the method use, to customize this set this variable inside your hook config.
module.exports.apiUtils = {
blueprints: {
// UPDATE: This variable is related to DEFAULT_LIMIT of the blueprints options
// https://github.com/balderdashy/sails/blob/master/lib/hooks/blueprints/parse-blueprint-options.js
// this variable is used inside custom find method, just in case the request ommit the 'limit' param
defaultLimit: 30,
// INFO: Here you can edit each one of the customBlueprints response to be used
customMethodsResponses: {
find: 'yourCustomResponse' // by default, uses a getCollection response -> see /responses/getCollection
}
}
}
the find method will end sending the next object to your response:
let object = {
results: matchingRecords,
pagination
}
return res.yourCustomResponse(object)