Hapi para los pequeños.

Hapi.js es un marco para crear aplicaciones web. Esta publicación contiene todos los elementos esenciales para un arranque en caliente. Desafortunadamente, el autor no es escritor en absoluto, por lo que habrá mucho código y pocas palabras.





MVP


Pon un montón de dependencias:


npm i @hapi/hapi @hapi/boom filepaths hapi-boom-decorators 

  • hapi / hapi - en realidad, nuestro servidor
  • hapi / boom - módulo para generar respuestas estándar
  • decoradores hapi-boom - ayudante para hapi / boom
  • filepaths: una utilidad que lee carpetas de forma recursiva

Cree una estructura de carpetas y un montón de archivos de inicio:





En ./src/routes/ agregamos una descripción de puntos finales api, 1 archivo - 1 punto final:


 // ./src/routes/home.js async function response() { // content-type            return { result: 'ok', message: 'Hello World!' }; } module.exports = { method: 'GET', //  path: '/', //  options: { handler: response // ,  ,  hapi > 17    } }; 

./src/server.js: el módulo que exporta el servidor en sí.


 // ./src/server.js 'use strict'; const Hapi = require('@hapi/hapi'); const filepaths = require('filepaths'); const hapiBoomDecorators = require('hapi-boom-decorators'); const config = require('../config'); async function createServer() { //   const server = await new Hapi.Server(config.server); //   await server.register([ hapiBoomDecorators ]); //      ./src/routes/ let routes = filepaths.getSync(__dirname + '/routes/'); for(let route of routes) server.route( require(route) ); //   try { await server.start(); console.log(`Server running at: ${server.info.uri}`); } catch(err) { //    ,   console.log(JSON.stringify(err)); } //      return server; } module.exports = createServer; 

En ./server.js todo lo que hacemos es llamar a createServer ()


 #!/usr/bin/env node const createServer = require('./src/server'); createServer(); 

Lanzamos


 node server.js 

Y verifica:


 curl http://127.0.0.1:3030/ {"result":"ok","message":"Hello World!"} curl http://127.0.0.1:3030/test {"statusCode":404,"error":"Not Found","message":"Not Found"} 


En la naturaleza


En un proyecto real, como mínimo, necesitamos una base de datos, un registrador, autorización, manejo de errores y mucho más.



Agregar secuencializar


ORM sequelize está conectado como un módulo:


 ... const Sequelize = require('sequelize'); ... await server.register([ ... { plugin: require('hapi-sequelizejs'), options: [ { name: config.db.database, // identifier models: [__dirname + '/models/*.js'], //    //ignoredModels: [__dirname + '/server/models/**/*.js'], //  -     sequelize: new Sequelize(config.db), //  sync: true, // default false forceSync: false, // force sync (drops tables) - default false }, ] } ... ]); 

La base de datos está disponible dentro de la ruta a través de una llamada:


 async function response(request) { const model = request.getModel('_', '_'); } 


Inyecte módulos adicionales en la solicitud


Es necesario interceptar el evento "onRequest", dentro del cual inyectaremos la configuración y el registrador en el objeto de solicitud:


 ... const Logger = require('./libs/Logger'); ... async function createServer(logLVL=config.logLVL) { ... const logger = new Logger(logLVL, 'my-hapi-app'); ... server.ext({ type: 'onRequest', method: async function (request, h) { request.server.config = Object.assign({}, config); request.server.logger = logger; return h.continue; } }); ... } 


Después de eso, dentro del controlador de solicitudes, tendremos acceso a la configuración, el registrador y la base de datos, sin la necesidad de incluir algo adicional en el cuerpo del módulo:


 // ./src/routes/home.js async function response(request) { //  request.server.logger.error('request error', 'something went wrong'); //  console.log(request.server.config); //   const messages = request.getModel(request.server.config.db.database, '_'); return { result: 'ok', message: 'Hello World!' }; } module.exports = { method: 'GET', //  path: '/', //  options: { handler: response // ,  ,  hapi > 17    } }; 

Por lo tanto, el controlador de solicitudes en la entrada recibirá todo lo necesario para su procesamiento, y no será necesario incluir los mismos módulos cada vez.



Iniciar sesión


La autorización en hapi se realiza en forma de módulos.


 ... const AuthBearer = require('hapi-auth-bearer-token'); ... async function createServer(logLVL=config.logLVL) { ... await server.register([ AuthBearer, ... ]); server.auth.strategy('token', 'bearer-access-token', {// 'token' -   ,  allowQueryToken: false, unauthorized: function() { //  ,  validate  isValid=false throw Boom.unauthorized(); }, validate: function(request, token) { if( token == 'asd' ) { return { //    isValid: true, credentials: {} }; } else { return { //   isValid: false, credentials: {} }; } } }); server.auth.default('token'); //    ... } 

Y también dentro de la ruta debe especificar qué tipo de autorización usar:


 module.exports = { method: 'GET', path: '/', auth: 'token', //  false,     options: { handler: response } }; 

Si se utilizan varios tipos de autorización:


 auth: { strategies: ['token1', 'token2', 'something_else'] }, 


Manejo de errores


Por defecto, boom genera errores de forma estándar, a menudo estas respuestas deben envolverse en su propio formato.


 server.ext('onPreResponse', function (request, h) { //      Boom,     if ( !request.response.isBoom ) { return h.continue; } //  -     let responseObj = { message: request.response.output.statusCode === 401 ? 'AuthError' : 'ServerError', status: request.response.message } //     logger.error('code: ' + request.response.output.statusCode, request.response.message); return h.response(responseObj).code(request.response.output.statusCode); }); 


Esquemas de datos


Este es un tema pequeño pero muy importante. Los esquemas de datos le permiten verificar la validez de la solicitud y la exactitud de la respuesta. Qué tan bien describe estos esquemas, por lo que la arrogancia y las pruebas automáticas serán de tal calidad.


Todos los esquemas de datos se describen a través de joi. Hagamos un ejemplo para la autorización del usuario:


 const Joi = require('@hapi/joi'); const Boom = require('boom'); async function response(request) { //   const accessTokens = request.getModel(request.server.config.db.database, 'access_tokens'); const users = request.getModel(request.server.config.db.database, 'users'); //     let userRecord = await users.findOne({ where: { email: request.query.login } }); //   ,     if ( !userRecord ) { throw Boom.unauthorized(); } // ,    if ( !userRecord.verifyPassword(request.query.password) ) { throw Boom.unauthorized();//  ,    ,    } // ,    let token = await accessTokens.createAccessToken(userRecord); //    return { meta: { total: 1 }, data: [ token.dataValues ] }; } //   ,      const tokenScheme = Joi.object({ id: Joi.number().integer().example(1), user_id: Joi.number().integer().example(2), expires_at: Joi.date().example('2019-02-16T15:38:48.243Z'), token: Joi.string().example('4443655c28b42a4349809accb3f5bc71'), updatedAt: Joi.date().example('2019-02-16T15:38:48.243Z'), createdAt: Joi.date().example('2019-02-16T15:38:48.243Z') }); //   const responseScheme = Joi.object({ meta: Joi.object({ total: Joi.number().integer().example(3) }), data: Joi.array().items(tokenScheme) }); //   const requestScheme =Joi.object({ login: Joi.string().email().required().example('pupkin@gmail.com'), password: Joi.string().required().example('12345') }); module.exports = { method: 'GET', path: '/auth', options: { handler: response, validate: { query: requestScheme }, response: { schema: responseScheme } } }; 

Prueba:


 curl -X GET "http://localhost:3030/auth?login=pupkin@gmail.com&password=12345" 




Ahora envíe en lugar de correo, solo inicie sesión:


 curl -X GET "http://localhost:3030/auth?login=pupkin&password=12345" 




Si la respuesta no coincide con el esquema de respuesta, entonces el servidor también caerá en un error 500.


Si el proyecto comenzó a procesar más de 1 solicitud por hora, puede ser necesario limitar la verificación de respuestas, ya que La verificación es una operación que requiere muchos recursos. Hay un parámetro para esto: "muestra"


 module.exports = { method: 'GET', path: '/auth', options: { handler: response, validate: { query: requestScheme }, response: { sample: 50, schema: responseScheme } } }; 

Como tal, solo el 50% de las solicitudes serán respuestas validadas.


Es muy importante describir los valores y ejemplos predeterminados, en el futuro se utilizarán para generar documentación y pruebas automáticas.



Swagger / OpenAPI


Necesitamos un montón de módulos adicionales:


 npm i hapi-swagger @hapi/inert @hapi/vision 

Los conectamos a server.js


 ... const Inert = require('@hapi/inert'); const Vision = require('@hapi/vision'); const HapiSwagger = require('hapi-swagger'); const Package = require('../package'); ... const swaggerOptions = { info: { title: Package.name + ' API Documentation', description: Package.description }, jsonPath: '/documentation.json', documentationPath: '/documentation', schemes: ['https', 'http'], host: config.swaggerHost, debug: true }; ... async function createServer(logLVL=config.logLVL) { ... await server.register([ ... Inert, Vision, { plugin: HapiSwagger, options: swaggerOptions }, ... ]); ... }); 

Y en cada ruta debes poner la etiqueta "api":


 module.exports = { method: 'GET', path: '/auth', options: { handler: response, tags: [ 'api' ], //    swagger'     validate: { query: requestScheme }, response: { sample: 50, schema: responseScheme } } }; 

Ahora en http: // localhost: 3030 / documentation , estará disponible una cara web con documentación , y en http: // localhost: 3030 / documentation.json .json una descripción.





Auto Test Generation


Si hemos descrito cualitativamente los esquemas de solicitud y respuesta, hemos preparado una base de datos inicial correspondiente a los ejemplos descritos en los ejemplos de solicitud, luego, utilizando esquemas conocidos, puede generar automáticamente solicitudes y verificar los códigos de respuesta del servidor.


Por ejemplo, en GET: / auth se esperan parámetros de inicio de sesión y contraseña, los tomaremos de los ejemplos que especificamos en el diagrama:


 const requestScheme =Joi.object({ login: Joi.string().email().required().example('pupkin@gmail.com'), password: Joi.string().required().example('12345') }); 

Y si el servidor responde con HTTP-200-OK, entonces asumimos que la prueba ha pasado.


Desafortunadamente, no había un módulo adecuado listo para usar; tienes que hablar un poco:


 // ./test/autogenerate.js const assert = require('assert'); const rp = require('request-promise'); const filepaths = require('filepaths'); const rsync = require('sync-request'); const config = require('./../config'); const createServer = require('../src/server'); const API_URL = 'http://0.0.0.0:3030'; const AUTH_USER = { login: 'pupkin@gmail.com', pass: '12345' }; const customExamples = { 'string': 'abc', 'number': 2, 'boolean': true, 'any': null, 'date': new Date() }; const allowedStatusCodes = { 200: true, 404: true }; function getExampleValue(joiObj) { if( joiObj == null ) // if joi is null return joiObj; if( typeof(joiObj) != 'object' ) //If it's not joi object return joiObj; if( typeof(joiObj._examples) == 'undefined' ) return customExamples[ joiObj._type ]; if( joiObj._examples.length <= 0 ) return customExamples[ joiObj._type ]; return joiObj._examples[ 0 ].value; } function generateJOIObject(schema) { if( schema._type == 'object' ) return generateJOIObject(schema._inner.children); if( schema._type == 'string' ) return getExampleValue(schema); let result = {}; let _schema; if( Array.isArray(schema) ) { _schema = {}; for(let item of schema) { _schema[ item.key ] = item.schema; } } else { _schema = schema; } for(let fieldName in _schema) { if( _schema[ fieldName ]._type == 'array' ) { result[ fieldName ] = [ generateJOIObject(_schema[ fieldName ]._inner.items[ 0 ]) ]; } else { if( Array.isArray(_schema[ fieldName ]) ) { result[ fieldName ] = getExampleValue(_schema[ fieldName ][ 0 ]); } else if( _schema[ fieldName ]._type == 'object' ) { result[ fieldName ] = generateJOIObject(_schema[ fieldName ]._inner); } else { result[ fieldName ] = getExampleValue(_schema[ fieldName ]); } } } return result } function generateQuiryParams(queryObject) { let queryArray = []; for(let name in queryObject) queryArray.push(`${name}=${queryObject[name]}`); return queryArray.join('&'); } function generatePath(basicPath, paramsScheme) { let result = basicPath; if( !paramsScheme ) return result; let replaces = generateJOIObject(paramsScheme); for(let key in replaces) result = result.replace(`{${key}}`, replaces[ key ]); return result; } function genAuthHeaders() { let result = {}; let respToken = rsync('GET', API_URL + `/auth?login=${AUTH_USER.login}&password=${AUTH_USER.pass}`); let respTokenBody = JSON.parse(respToken.getBody('utf8')); result[ 'token' ] = { Authorization: 'Bearer ' + respTokenBody.data[ 0 ].token }; return result; } function generateRequest(route, authKeys) { if( !route.options.validate ) { return false; } let options = { method: route.method, url: API_URL + generatePath(route.path, route.options.validate.params) + '?' + generateQuiryParams( generateJOIObject(route.options.validate.query || {}) ), headers: authKeys[ route.options.auth ] ? authKeys[ route.options.auth ] : {}, body: generateJOIObject(route.options.validate.payload || {}), json: true, timeout: 15000 } return options; } let authKeys = genAuthHeaders(); let testSec = [ 'POST', 'PUT', 'GET', 'DELETE' ]; let routeList = []; for(let route of filepaths.getSync(__dirname + '/../src/routes/')) routeList.push(require(route)); describe('Autogenerate Hapi Routes TEST', async () => { for(let metod of testSec) for(let testRoute of routeList) { if( testRoute.method != metod ) { continue; } it(`TESTING: ${testRoute.method} ${testRoute.path}`, async function () { let options = generateRequest(testRoute, authKeys); if( !options ) return false; let statusCode = 0; try { let result = await rp( options ); statusCode = 200; } catch(err) { statusCode = err.statusCode; } if( !allowedStatusCodes[ statusCode ] ) { console.log('*** TEST STACK FOR:', `${testRoute.method} ${testRoute.path}`); console.log('options:', options); console.log('StatusCode:', statusCode); } return assert.ok(allowedStatusCodes[ statusCode ]); }); } }); 

No te olvides de las dependencias:


 npm i request-promise mocha sync-request 

Y sobre package.json


 ... "scripts": { "test": "mocha", "dbinit": "node ./scripts/dbInit.js" }, ... 

Comprobamos:


 npm test 



Y si el bloqueo es algún tipo de esquema de datos, o la respuesta no coincide con el esquema:





Y no olvide que tarde o temprano las pruebas se volverán sensibles a los datos de la base de datos. Antes de ejecutar las pruebas, debe al menos borrar la base de datos.


Fuentes enteras

Source: https://habr.com/ru/post/467099/


All Articles