Hapi für die Kleinen

Hapi.js ist ein Framework zum Erstellen von Webanwendungen. Dieser Beitrag enthält alle wichtigen Informationen für einen heißen Start. Leider ist der Autor überhaupt kein Schriftsteller, daher wird es viel Code und wenige Wörter geben.





MVP


Setzen Sie eine Reihe von Abhängigkeiten:


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

  • hapi / hapi - eigentlich unser server
  • Hapi / Boom - Modul zur Generierung von Standardantworten
  • Hapi-Boom-Dekorateure - Helfer für Hapi / Boom
  • Dateipfade - ein Dienstprogramm, das Ordner rekursiv liest

Erstellen Sie eine Ordnerstruktur und eine Reihe von Startdateien:





In ./src/routes/ fügen wir eine Beschreibung der API-Endpunkte hinzu, 1 Datei - 1 Endpunkt:


 // ./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 - das Modul, das den Server selbst exportiert.


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

In ./server.js rufen wir nur createServer () auf


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

Wir starten


 node server.js 

Und überprüfen Sie:


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


In freier Wildbahn


In einem realen Projekt benötigen wir mindestens eine Datenbank, einen Logger, eine Autorisierung, eine Fehlerbehandlung und vieles mehr.



Sequelize hinzufügen


ORM Sequelize ist als Modul verbunden:


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

Die Datenbank wird innerhalb der Route durch einen Anruf verfügbar:


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


Fügen Sie zusätzliche Module in die Anforderung ein


Es ist notwendig, das "onRequest" -Ereignis abzufangen, in das wir die Konfiguration und den Logger in das Anforderungsobjekt einfügen:


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


Danach haben wir im Request-Handler Zugriff auf die Konfiguration, den Logger und die Datenbank, ohne dass zusätzlich etwas in den Modulkörper aufgenommen werden muss:


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

Somit erhält der Anforderungshandler an der Eingabe alles, was für seine Verarbeitung erforderlich ist, und es ist nicht erforderlich, von Zeit zu Zeit dieselben Module einzuschließen.



Login


Die Autorisierung in Hapi erfolgt in Form von Modulen.


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

Außerdem müssen Sie innerhalb der Route angeben, welche Art von Autorisierung verwendet werden soll:


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

Wenn mehrere Arten von Berechtigungen verwendet werden:


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


Fehlerbehandlung


Standardmäßig generiert boom Fehler auf standardmäßige Weise. Oft müssen diese Antworten in einem eigenen Format verpackt werden.


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


Datenschemata


Dies ist ein kleines, aber sehr wichtiges Thema. Mithilfe von Datenschemata können Sie die Gültigkeit der Anforderung und die Richtigkeit der Antwort überprüfen. Wie gut Sie diese Schemata beschreiben, werden auch Prahlereien und Autotests von solcher Qualität sein.


Alle Datenschemata werden durch joi beschrieben. Machen wir ein Beispiel für die Benutzerautorisierung:


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

Testen:


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




Jetzt senden statt mailen, nur anmelden:


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




Wenn die Antwort nicht mit dem Antwortschema übereinstimmt, wird der Server ebenfalls in einen 500-Fehler geraten.


Wenn das Projekt mehr als eine Anfrage pro Stunde verarbeitet hat, muss möglicherweise die Überprüfung der Antworten eingeschränkt werden Die Überprüfung ist eine ressourcenintensive Operation. Hierfür gibt es einen Parameter: "sample"


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

Daher werden nur 50% der Anfragen validierte Antworten sein.


Es ist sehr wichtig, die Standardwerte und Beispiele zu beschreiben. In Zukunft werden sie zum Generieren von Dokumentation und Autotests verwendet.



Swagger / OpenAPI


Wir brauchen eine Reihe zusätzlicher Module:


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

Wir verbinden sie mit 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 }, ... ]); ... }); 

Und in jeder Route müssen Sie das Tag "api" setzen:


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

Jetzt steht unter http: // localhost: 3030 / documentation ein Webface mit Dokumentation zur Verfügung und unter http: // localhost: 3030 / documentation.json .json eine Beschreibung.





Automatische Testgenerierung


Wenn wir die Anforderungs- und Antwortschemata qualitativ beschrieben und eine Startdatenbank erstellt haben, die den in den Anforderungsbeispielen beschriebenen Beispielen entspricht, können Sie nach bekannten Schemata automatisch Anforderungen generieren und Server-Antwortcodes überprüfen.


Wenn beispielsweise in GET: / auth Login- und Kennwortparameter erwartet werden, werden diese den Beispielen entnommen, die wir im Diagramm angegeben haben:


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

Wenn der Server mit HTTP-200-OK antwortet, gehen wir davon aus, dass der Test bestanden wurde.


Leider gab es kein vorgefertigtes geeignetes Modul, man muss ein wenig reden:


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

Vergessen Sie nicht die Abhängigkeiten:


 npm i request-promise mocha sync-request 

Und über package.json


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

Wir prüfen:


 npm test 



Und wenn die Sperre eine Art Datenschema ist oder die Antwort nicht mit dem Schema übereinstimmt:





Und vergessen Sie nicht, dass die Tests früher oder später empfindlich auf die Daten in der Datenbank reagieren. Bevor Sie die Tests ausführen, müssen Sie mindestens die Datenbank löschen.


Ganze Quellen

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


All Articles