Hapi untuk anak kecil

Hapi.js adalah kerangka kerja untuk membangun aplikasi web. Posting ini berisi semua hal penting untuk memulai yang baru. Sayangnya, penulisnya sama sekali bukan penulis, jadi akan ada banyak kode dan beberapa kata.





MVP


Taruh banyak dependensi:


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

  • hapi / hapi - sebenarnya, server kami
  • hapi / boom - modul untuk menghasilkan jawaban standar
  • hapi-boom-dekorator - pembantu untuk hapi / boom
  • filepaths - sebuah utilitas yang secara rekursif membaca folder

Buat struktur folder dan banyak file mulai:





Dalam ./src/routes/ kami menambahkan deskripsi titik akhir api, 1 file - 1 titik akhir:


 // ./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 - modul yang mengekspor server itu sendiri.


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

Di ./server.js yang kita lakukan adalah memanggil createServer ()


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

Kami meluncurkan


 node server.js 

Dan periksa:


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


Di alam liar


Dalam proyek nyata, minimal, kita membutuhkan database, logger, otorisasi, penanganan kesalahan, dan banyak lagi.



Tambahkan sekuel


Sequelize ORM terhubung sebagai modul:


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

Basis data menjadi tersedia di dalam rute melalui panggilan:


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


Suntikkan modul tambahan ke dalam permintaan


Hal ini diperlukan untuk mencegat acara "onRequest", di mana kami akan menyuntikkan konfigurasi dan logger ke objek permintaan:


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


Setelah itu, di dalam penangan permintaan, kita akan memiliki akses ke config, logger, dan basis data, tanpa perlu menambahkan sesuatu ke dalam modul 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    } }; 

Dengan demikian, penangan permintaan pada input akan menerima semua yang diperlukan untuk pemrosesan, dan tidak perlu menyertakan modul yang sama setiap waktu dari waktu ke waktu.



Login


Otorisasi dalam hapi dibuat dalam bentuk modul.


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

Dan juga di dalam rute Anda perlu menentukan jenis otorisasi yang digunakan:


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

Jika beberapa jenis otorisasi digunakan:


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


Menangani kesalahan


Secara default, boom menghasilkan kesalahan dengan cara standar, seringkali jawaban ini perlu dibungkus dalam format mereka sendiri.


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


Skema data


Ini adalah topik kecil tapi sangat penting. Skema data memungkinkan Anda untuk memeriksa validitas permintaan dan kebenaran tanggapan. Seberapa baik Anda menggambarkan skema ini, demikian juga kesombongan dan autotest akan berkualitas seperti itu.


Semua skema data dijelaskan melalui joi. Mari kita buat contoh untuk otorisasi pengguna:


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

Pengujian:


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




Sekarang kirim alih-alih surat, hanya login:


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




Jika jawabannya tidak cocok dengan skema respons, maka server juga akan jatuh ke dalam kesalahan 500.


Jika proyek mulai memproses lebih dari 1 permintaan per jam, mungkin perlu membatasi verifikasi tanggapan, seperti verifikasi adalah operasi intensif sumber daya. Ada parameter untuk ini: "sampel"


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

Dengan demikian, hanya 50% permintaan yang akan divalidasi.


Sangat penting untuk menggambarkan nilai dan contoh default, di masa depan mereka akan digunakan untuk menghasilkan dokumentasi dan autotest.



Sombong / OpenAPI


Kami membutuhkan banyak modul tambahan:


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

Kami menghubungkan mereka ke 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 }, ... ]); ... }); 

Dan di setiap rute Anda harus meletakkan tag "api":


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

Sekarang di http: // localhost: 3030 / dokumentasi web-face dengan dokumentasi akan tersedia, dan di http: // localhost: 3030 / dokumentasi.json .json sebuah deskripsi.





Pembuatan Tes Otomatis


Jika kami telah secara kualitatif menggambarkan skema permintaan dan respons, menyiapkan basis data seed yang sesuai dengan contoh-contoh yang dijelaskan dalam contoh permintaan, maka, menggunakan skema terkenal, Anda dapat secara otomatis menghasilkan permintaan dan memeriksa kode respons server.


Misalnya, dalam GET: / auth login dan kata sandi parameter diharapkan, kami akan mengambilnya dari contoh yang kami tentukan dalam diagram:


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

Dan jika server merespons dengan HTTP-200-OK, maka kami akan menganggap bahwa tes telah berlalu.


Sayangnya, tidak ada modul yang cocok yang siap pakai; Anda harus berbicara sedikit:


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

Jangan lupa tentang dependensi:


 npm i request-promise mocha sync-request 

Dan tentang package.json


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

Kami memeriksa:


 npm test 



Dan jika kuncinya adalah semacam skema data, atau jawabannya tidak cocok dengan skema tersebut:





Dan jangan lupa bahwa cepat atau lambat tes akan menjadi peka terhadap data dalam database. Sebelum menjalankan tes, Anda harus setidaknya menghapus database.


Seluruh Sumber

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


All Articles