Hapi pour les petits

Hapi.js est un cadre pour la création d'applications Web. Ce message contient tous les éléments essentiels pour un démarrage à chaud. Malheureusement, l'auteur n'est pas du tout un écrivain, il y aura beaucoup de code et peu de mots.





MVP


Mettez un tas de dépendances:


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

  • hapi / hapi - en fait, notre serveur
  • hapi / boom - module pour gĂ©nĂ©rer des rĂ©ponses standard
  • hapi-boom-decorators - assistant pour hapi / boom
  • filepaths - un utilitaire qui lit rĂ©cursivement les dossiers

Créez une structure de dossiers et un tas de fichiers de démarrage:





Dans ./src/routes/, nous ajoutons une description des points finaux api, 1 fichier - 1 point 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 - le module qui exporte le serveur lui-mĂȘme.


 // ./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; 

Dans ./server.js, tout ce que nous faisons est d'appeler createServer ()


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

Nous lançons


 node server.js 

Et vérifiez:


 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"} 


Dans la nature


Dans un vrai projet, au minimum, nous avons besoin d'une base de données, d'un enregistreur, d'une autorisation, d'une gestion des erreurs et bien plus encore.



Ajouter séquelle


ORM sequelize est connecté en tant que module:


 ... 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 données devient disponible à l'intérieur de l'itinéraire via un appel:


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


Injecter des modules supplémentaires dans la demande


Il est nécessaire d'intercepter l'événement "onRequest", à l'intérieur duquel nous injecterons la config et le logger dans l'objet request:


 ... 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; } }); ... } 


AprĂšs cela, Ă  l'intĂ©rieur du gestionnaire de requĂȘtes, nous aurons accĂšs Ă  la configuration, Ă  l'enregistreur et Ă  la base de donnĂ©es, sans avoir besoin d'inclure en outre quelque chose dans le corps du module:


 // ./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    } }; 

Ainsi, le gestionnaire de requĂȘtes en entrĂ©e recevra tout le nĂ©cessaire pour son traitement, et il ne sera pas nĂ©cessaire d'inclure Ă  chaque fois les mĂȘmes modules.



Se connecter


L'autorisation en hapi se fait sous forme de modules.


 ... 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'); //    ... } 

Et également à l'intérieur de l'itinéraire, vous devez spécifier le type d'autorisation à utiliser:


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

Si plusieurs types d'autorisation sont utilisés:


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


Gestion des erreurs


Par dĂ©faut, boom gĂ©nĂšre des erreurs de maniĂšre standard, souvent ces rĂ©ponses doivent ĂȘtre emballĂ©es dans leur propre format.


 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); }); 


Schémas de données


C'est un sujet petit mais trÚs important. Les schémas de données vous permettent de vérifier la validité de la demande et l'exactitude de la réponse. Dans quelle mesure vous décrivez ces schémas, les tests et les autotests seront donc d'une telle qualité.


Tous les schémas de données sont décrits via joi. Faisons un exemple d'autorisation d'utilisateur:


 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 } } }; 

Test:


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




Maintenant, envoyez au lieu du courrier, connectez-vous uniquement:


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




Si la réponse ne correspond pas au schéma de réponse, le serveur tombera également dans une erreur 500.


Si le projet a commencĂ© Ă  traiter plus d'une demande par heure, il peut ĂȘtre nĂ©cessaire de limiter la vĂ©rification des rĂ©ponses, car la vĂ©rification est une opĂ©ration gourmande en ressources. Il y a un paramĂštre pour cela: "Ă©chantillon"


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

Ainsi, seulement 50% des demandes seront des réponses validées.


Il est trÚs important de décrire les valeurs par défaut et les exemples, à l'avenir, ils seront utilisés pour générer de la documentation et des autotests.



Swagger / OpenAPI


Nous avons besoin d'un tas de modules supplémentaires:


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

Nous les connectons Ă  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 }, ... ]); ... }); 

Et dans chaque itinéraire, vous devez mettre la balise "api":


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

Maintenant, sur http: // localhost: 3030 / documentation, une page Web avec documentation sera disponible, et sur http: // localhost: 3030 / documentation.json .json, une description.





Génération de test automatique


Si nous avons décrit qualitativement les schémas de demande et de réponse, préparé une base de données de départ correspondant aux exemples décrits dans les exemples de demande, puis, à l'aide de schémas bien connus, vous pouvez générer automatiquement des demandes et vérifier les codes de réponse du serveur.


Par exemple, dans GET: / les paramÚtres de connexion et de mot de passe sont attendus, nous les prendrons à partir des exemples que nous avons spécifiés dans le diagramme:


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

Et si le serveur répond avec HTTP-200-OK, nous supposerons que le test a réussi.


Malheureusement, il n'y avait pas de module appropriĂ© prĂȘt Ă  l'emploi; vous devez parler un peu:


 // ./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 ]); }); } }); 

N'oubliez pas les dépendances:


 npm i request-promise mocha sync-request 

Et Ă  propos de package.json


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

Nous vérifions:


 npm test 



Et si le verrou est une sorte de schéma de données, ou si la réponse ne correspond pas au schéma:





Et n'oubliez pas que tÎt ou tard les tests deviendront sensibles aux données de la base de données. Avant d'exécuter les tests, vous devez au moins effacer la base de données.


Sources entiĂšres

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


All Articles