Differences

This shows you the differences between two versions of the page.

Link to this comparison view

moby:backend:03 [2020/05/31 19:56]
alexandru.hogea created
moby:backend:03 [2020/07/27 15:14] (current)
alexandru.hogea [Resurse]
Line 1: Line 1:
-===== Sesiuna 3 - Implementarea ​serviciului de autentificare si autorizare ​=====+===== Sesiuna 3 - Implementarea ​unui serviciu CRUD simplu ​=====
  
-Vom incepe sa implementam serviciile propuse ​in arhitecturaPrima oara vom realiza serviciul ​de autentificare ​si autorizareNe vom lega la PostgreSQL. ​Vom folosi ​Docker ​pentru ​PostgresInteractiunea ​cu baza de date se va realiza ​in partea a doua.+Scopul sedintei: 
 +  * Realizarea unui REST API in NodeJS 
 +  * Interactiunea cu o baza de date PostgreSQL 
 +  * Realizarea actiunilor CRUD peste un set de date 
 + 
 +==== Resurse ==== 
 + 
 +Filmuletul cu prezentarea poate fi gasit la acest [[https://​drive.google.com/​drive/​folders/​1BJcQlCYE2WN5vA0J1ZQjV4tp0Avd1x21?​usp=sharing|link]] 
 + 
 +Un REST API poate fi structurat, cel mai des, in doua moduri: 
 + 
 +  - in functie ​de context 
 +  - in functie de functionalitate 
 + 
 +Noi am ales o impartire bazata pe context. Fiecare context din cadrul serviciului are propriul lui folder (de exemplu, folderul **authors**). O impartire pe functionalitate implica foldere dedicate functionaltiatilor (de exemplu, folder **controllers**,​ folder **services**,​ etc...). 
 + 
 +Puteti pleca de la urmatorul [[https://​gitlab.com/​mobyworkshop/​library-microservice.git|schelet de cod]]. Sunt implementate urmatoarele:​ 
 + 
 +=== Scriptul de start (start.js) === 
 + 
 +In scriptul de start se initializeaza serverul. Deoarece express se bazeaza pe conceptul de **middlewares**,​ adica lanturi de functii, initializarea nu inseamna nimic mai mult decat mai multe functii ale unor pachete puse impreuna, la inceput. 
 + 
 +<code javascript>​ 
 +const express = require('​express'​);​ // pachet folosit pentru partea de REST API server 
 +const morgan = require('​morgan'​);​ // pachet folosit pentru formatarea log-urilor 
 +const helmet = require('​helmet'​);​ // pachet folosit pentru adaugarea unor headere de securitate 
 +const createError = require('​http-errors'​);​ // pachet folosit pentru trimiterea de erori 
 + 
 +require('​express-async-errors'​);​ // pachet folosit pentru captarea erorilor pe rute. In spate, de fapt, face un try {} catch {} pe fiecare ruta definita de noi si apeleaza next() in cazul in care intalneste o eroareEste util pentru a nu scrie try...catch de fiecare data 
 +require('​log-timestamp'​);​ // pachet folosit pentru injectarea timpului in momenul in care se da console.log() 
 + 
 +const routes = require('​./​routes.js'​);​ // fisierul (scris de noi) care are configurate toate rutele 
 + 
 +const app = express(); // instantierea serverului efectiv 
 + 
 +app.use(helmet());​ // adaugarea primului middleware, cel oferit de pachetul helmet 
 +app.use(morgan(':​remote-addr - :​remote-user [:​date[web]] ":​method :url HTTP/:​http-version"​ :status :​res[content-length]'​));​ // adaugarea celui de-al doilea middleware, cel oferit de pachetul morgan 
 +app.use(express.json());​ // adaugarea celui de-al treilea middleware, cel care extrage obiecte JSON din corpul cererilor. Util pentru POST si PUT 
 +app.use(express.urlencoded({ extended: false })); // adaugarea celui de-al patrulea middleware, cel care extrage obiecte x-www-urlencoded din corpul cererilor. Util pentru POST si PUT, daca nu se foloseste JSON 
 + 
 +app.use('/​api',​ routes); // adaugarea rutelor configurate de noi in lantul de rute, cu radacina /api 
 + 
 +app.use((err,​ req, res, next) => { 
 +    console.error(err);​ 
 +    let status = 500; 
 +    let message = '​Something Bad Happened';​ 
 +    if (err.httpStatus) { 
 +        status = err.httpStatus;​ 
 +        message = err.message;​ 
 +    }  
 +    return next(createError(status,​ message));​ 
 +}); // adaugarea unui middleware ce intercepteaza erorile si foloseste createError pentru incapsularea lor 
 + 
 + 
 +const port = process.env.PORT || 80; // stabilirea portului pe care va rula serverul in functie de variabila de mediu. Daca nu exista, este implicit 80 
 + 
 +app.listen(port,​ () => { 
 +    console.log(`App is listening on ${port} and running on ${process.env.NODE_ENV} mode`); 
 +}); // deschiderea serverului pe portul stabilit 
 + 
 +</​code>​ 
 + 
 +=== Fisierul de configurare al rutelor (routes.js) === 
 + 
 +Acest fisier are rol de a centraliza toate **controllerele** folosite in aplicatie si de a le servi sub forma unui obiect **Router** care este, apoi, consumat in start.js. 
 + 
 +<code javascript>​ 
 +const Router = require('​express'​).Router();​ // instantierea obiectului Router din biblioteca express. Ajuta la rutare modulara 
 + 
 +const authorsControllers = require('​./​authors/​controllers.js'​);​ // controllerul folosit pentru autori 
 + 
 +Router.use('/​authors',​ authorsControllers);​ // adaugarea controllerului de autori in rutele obiectului Router, cu radacina /authors 
 + 
 +module.exports = Router; // expunerea obiectului Router pentru a putea fi folosit de alte fisiere. Bineinteles,​ dupa cum ati vazut, este folosit in start.js 
 +</​code>​ 
 + 
 +=== Interactiunea cu baza de date (data/​index.js) === 
 + 
 +Baza de date este PostgreSQL, asa ca, pentru interactiune,​ am utilizat un driver facut pentru NodeJS, care se numeste **pg**PG ofera posibilitatea scrierii de cereri SQL ad-hoc. Se puteau ​folosi ​si alte pachete, care abstractizeaza acest proces, precum Knex.js, insa, pentru ​simplitate, am optat pe SQL "de mana". 
 + 
 +Scopul acestui fisier este de a initializa conexiunea ​cu baza de date si de a oferi, catre public, o functie (query) prin care se pot trimite cereri SQL catre baza de date. 
 + 
 +<code javascript>​ 
 +const { 
 +    Pool 
 +  } = require('​pg'​);​ // preluarea obiectului Pool din pachetul pg care initializeaza conexiunea cu baza de date 
 +   
 +  const { 
 +    getSecret 
 +  } = require('​docker-secret'​);​ // folosit mai tarziu, o sa vedeti intr-o sedinta viitoare ;) 
 +   
 +  const options = { 
 +    host: process.env.PGHOST,​ 
 +    database: process.env.PGDATABASE,​ 
 +    port: process.env.PGPORT,​ 
 +    user: process.env.NODE_ENV === '​development'​ ? process.env.PGUSER : getSecret(process.env.PGUSER_FILE),​ 
 +    password: process.env.NODE_ENV === '​development'​ ? process.env.PGPASSWORD : getSecret(process.env.PGPASSWORD_FILE) 
 +  } // preluarea informatiilor de conectare din variabilele de mediu 
 + 
 +  const pool = new Pool(options);​ // realizarea conexiunii folosind informatiile de conectare si obiectul Pool 
 +   
 +  const query = async (text, params) => { 
 +    const { 
 +      rows, 
 +    } = await pool.query(text,​ params); 
 +    return rows; 
 +  }; // crearea functiei de interactiune cu baza de date. Functia are doi parametri, un string, care reprezinta cererea SQL si un vector de valori, din care sunt preluate valorile si injectate in cerere. Atentie, functia ​va returna **intotdeauna un vector**. 
 +   
 +  module.exports = { 
 +    query, 
 +  }; // expunerea functiei 
 +</​code>​ 
 + 
 +=== Obiect custom de erori (errors/​index.js) === 
 +Cateodata dorim sa avem anumite campuri adaugate ​in plus, in obiectele de eroare. Pentru acest lucru, trebuie extinsa clasa de erori din js (Error) si creata o clasa noua ce contine campurile dorite. 
 + 
 +<code javascript>​ 
 +class ServerError extends Error { // extinderea clasei Error 
 +    constructor(message,​ httpStatus) { // cele doua campuri pe care le va contine clasa 
 +      super(message);​ // campul message este continut si de Error, asa ca se poate trimite mai sus, pe lantul de mostenire, prin apelul super() 
 +      this.name = this.constructor.name;​ 
 +      this.httpStatus = httpStatus; // campul httpStatus este un camp nou, asa ca il vom initaliza aici 
 +      Error.captureStackTrace(this,​ this.constructor);​ // util pentru debug 
 +    } 
 +  } 
 +   
 +  module.exports = { 
 +    ServerError,​ 
 +  }; //expunerea clasei public 
 +   
 +</​code>​ 
 + 
 +=== Controllerele pentru Authors (authors/​controllers.js) === 
 + 
 +Deoarece am ales o structura bazata pe context, fiecare context are un set de **controllere** si **servicii**. 
 + 
 +Controllerele au rolul de a descrie logica de rutare si verificare a parametrilor. 
 + 
 +Pentru cod modular, am folosit si aici obiectul Router. 
 + 
 +<note tip>​Obiectele **req** si **res** sunt obiecte ce vor fi folosite pentru orice cerere. Req vine de la **request** si contine informatii despre cerere, precum url, parametrii, corp, etc..., in timp ce Res vine de la **response** si are metode pentru stabilirea statusului, a tipului de raspuns returnat precum si pentru returnarea (si, deci, terminarea actiunii cerere-raspuns) unui raspuns </​note>​ 
 + 
 +<code javascript>​ 
 +const Router = require('​express'​).Router();​ // initializarea obiectului Router, ce permite crearea de rute in mod modular 
 + 
 +const { 
 +    ServerError 
 +} = require('​../​errors'​);​ // clasa de erori custom, definita mai sus 
 + 
 +const { 
 +    getAll, 
 +    getById, 
 +    add, 
 +    update, 
 +    remove 
 +} = require('​./​services.js'​);​ // functiile din servicii 
 +  
 +Router.get('/:​id',​ async (req, res) => { // ruta de GET /​api/​authors/:​id. Returneaza informatiile despre autorul cu id-ul dat in parametri 
 +    const { 
 +        id 
 +    } = req.params; // preluarea parametrului id din parametrii de cerere. Observati propritatea **.params** a obiectului **req** 
 + 
 +    const author = await getById(id);​ // se apeleaza functia respectiva din servicii 
 + 
 +    res.json(author);​ // se returneaza autorul, sub forma de obiect json. Express face acest lucru usor, prin metoda **.json()** a obiectului **res** 
 + 
 +}); 
 + 
 +Router.get('/',​ async (req, res) => { // ruta de GET /​api/​authors. Returneaza toti autorii 
 + 
 +    const authors = await getAll(); // se apeleaza functia din servicii 
 + 
 +    res.json(authors);​ // se returneaza autorii sub forma de obiect json 
 + 
 +}); 
 + 
 +Router.post('/',​ async (req, res) => { // ruta de POST /​api/​authors. Ruta pentru adaugarea unui autor 
 +    const { 
 +        nume 
 +    } = req.body; // se preia campul **nume** din corpul cererii 
 + 
 +    if (typeof nume !== '​string'​) { 
 +        throw new ServerError('​Nume invalid!',​ 400); 
 +    } // se verifica daca este de tip string. Altfel, se arunca o eroare custom. Eroarea este interceptata de pachetul **express-async-errors** si redirectionata prin lantul de middlewares catre handler-ul creat de noi in start.js.  
 + 
 +    const id = await add(nume); // se apeleaza functia din servicii 
 + 
 +    res.json({id,​ nume}); // se returneaza autorul nou creat, sub forma id-ului sau din baza de date si a numelui. 
 +}); 
 + 
 +Router.put('/:​id',​ async (req, res) => { // ruta de PUT /​api/​authors/:​id. Ruta pentru modificarea informatiilor unui autor cu id-ul dat in parametri 
 +    const { 
 +        id 
 +    } = req.params; // se preia id-ul din parametrii cererii 
 + 
 +    const { 
 +        nume 
 +    } = req.body; // se preia noul nume din corpul cererii 
 + 
 +    await update(id, nume); // se apeleaza functia din servicii 
 +     
 +    res.json({id,​ nume}); // se returneaza autorul sub forma unui obiect ce contine id-ul sau si noul nume 
 + 
 +}); 
 + 
 +Router.delete('/:​id',​ async (req, res) => { // ruta de DELETE /​api/​authors/:​id. Ruta pentru stergerea unui autor cu id-ul dat in parametri 
 +    const { 
 +        id 
 +    } = req.params; // se preia id-ul din parametri cererii 
 + 
 +    await remove(id); // se apeleaza functia din servicii 
 + 
 +    res.status(200).end();​ // de data aceasta nu se mai intoarce nimic, asa ca se seteaza statusul http de succes (200) prin apelul metodei **.status()** din obiectul **res** si apoi se apeleaza metoda **.end()** care raspunde cererii si, deci, inchide fluxul cerere-raspuns 
 +}); 
 + 
 +module.exports = Router; // se expune obiectul Router in afara pentru a putea fi folosit in fisierul routes.js 
 +</​code>​ 
 + 
 +=== Serviciile pentru Authors (authors/​services.js) === 
 +Daca controllerele se ocupa de partea ​de rutare, serviciile se ocupa de partea de computatie si interactiune cu alte module. In cazul nostru, vom interactiona cu functia **query** din data/​index.js 
 + 
 +<code javascript>​ 
 +const { 
 +    query 
 +} = require('​../​data'​);​ // preluarea functiei de acces baza de date 
 + 
 +const { ServerError } = require('​../​errors'​);​ // clasa de erori custom, definita mai sus 
 + 
 +const getAll = async () => { 
 +    console.info('​Getting all authors...'​);​ // indicat sa logati fiecare actiune, in special daca faceti debug 
 + 
 +    const authors = await query('​SELECT * FROM authors'​);​ // obtinem toti autorii din baza de date printr-un SELECT 
 + 
 +    return authors; // returnam autorii 
 +}; 
 + 
 +const getById = async (id) => { 
 +    console.info(`Getting author ${id}...`);​ 
 + 
 +    const authors = await query('​SELECT * FROM authors WHERE id=$1',​ [id]); // obtinem autorul (sub forma de vector) care are id-ul dat ca parametru. Pentru securitate, in apelul query variabilele sunt inserate la executie, asa ca in locul valorii efective se pune $1. Valoarea este preluata din vectorul de valori trimis ca parametru secundar functiei query. 
 +     
 +    if (authors.length !== 1) { 
 +        throw new ServerError('​Author does not exist!',​ 404); 
 +    } // se verifica dimensiunea vectorului de raspuns rezultat. Ar trebui sa fie 1 (autorul cautat de noi). Daca nu este 1, inseamna ca nu exista si returnam un mesaj corespunzator. 
 + 
 +    return authors[0]; // returnam autorul (adica primul - si singurul - element din vectorul de raspuns) 
 +}; 
 + 
 +const add = async (nume) => { 
 +    console.info(`Adaug un autor cu numele ${nume}`);​ 
 + 
 +    try { 
 +        const authors = await query('​INSERT INTO authors (nume) VALUES ($1) RETURNING id', [nume]); // adaugam un autor in tabela folosind un apel SQL de INSERT 
 +        return authors[0].id;​ // returnam id-ul sau. Din nou, query returneaza un vector, asa ca preluam primul (si singurul) element din vector 
 + 
 +    } catch (e) { // postgres va returna o eroare cu codul 23505 in cazul in care se adauga o informatie duplicat. Acest lucru trebuie tratat manual, printr-un try...catch 
 +        if (e.code === '​23505'​) { 
 +            throw new ServerError('​Author already exists!',​ 409); 
 +        } 
 +        throw e; 
 +    } 
 +}; 
 + 
 +const update = async (id, nume) => { 
 +    console.info(`Modific numele autorului ${id} in ${nume}`);​ 
 + 
 +    await query('​UPDATE authors SET nume=$1 WHERE id=$2',​ [nume, id]); // actualizam numele autorului, printr-un apel INSERT. Observam ca se pune si $2, pentru ca sunt 2 valori in vectorul de parametri dat ca parametru secundar functiei query 
 +}; 
 + 
 +const remove = async (id) => { 
 +    console.info(`Sterg autorul ${id}...`);​ 
 + 
 +    await query('​DELETE FROM authors WHERE id=$1',​ [id]); // stergerea autorului cu id specificat printr-un apel DELETE. 
 +
 + 
 +module.exports = { 
 +    getAll, 
 +    getById, 
 +    add, 
 +    update, 
 +    remove 
 +} // expunerea functiilor public 
 +</​code>​ 
 + 
 +=== Variabilele de mediu (.env)=== 
 + 
 +Variabilele de mediu pot fi injectate prin mai multe moduri, insa **cel mai uzual** este utilizarea unui fisier **.env** in care sunt trecute numele variabilelor si valorile lor. In cadrul acestui mic server, avem nevoie de urmatoarele variabile de mediu: 
 +  * PGHOST - folosit in data/​index.js -> retine ip-ul unde ruleaza baza de date 
 +  * PGUSER - folosit in data/​index.js -> retine usernameul de autentificare cu baza de date 
 +  * PGPASSWORD - folosit in data/​index.js -> retine parola de autentificare cu baza de date 
 +  * PGPORT - folosit in data/​index.js -> retine portul de conectare cu baza de date 
 +  * PGDATABASE - folosit in data/​index.js -> retine numele bazei de date 
 +  * PORT - folosit in start.js -> retine portul pe care va rula serverul 
 +  * NODE_ENV - folosit in mai multe locuri -> retine modul in care va rula serverul (development sau production) 
 + 
 +<code bash> 
 +PGHOST=localhost 
 +PGUSER=test 
 +PGPASSWORD=test 
 +PGPORT=5555 
 +PGDATABASE=workshop-library 
 + 
 +PORT=3000 
 +NODE_ENV=development 
 +</​code>​ 
 + 
 +=== Package.json === 
 +Pe langa informatiile uzuale trecute in package.json,​ ce tin de pachetele folosite, am introdus 3 scripturi, la sectiunea scripts. Rolul lor este sa ne ajute sa diferentiam modurile de rulare ale serverului. 
 + 
 +<code json> 
 +
 +  "​name":​ "​practic",​ 
 +  "​version":​ "​1.0.0",​ 
 +  "​description":​ "",​ 
 +  "​main":​ "​start.js",​ 
 +  "​scripts":​ { 
 +    "​start-dev-docker":​ "​nodemon start.js",​ // comanda de rulare in interiorul dockerului, rol de development 
 +    "​start-dev":​ "​nodemon -r dotenv/​config start.js",​ // comanda de rulare local, folosind fisierul de variabile de mediu .env si pachetul dotenv pentru injectarea lor 
 +    "​start":​ "node start.js"​ // comanda de rulare in interiorul dockerului, in productie 
 +  }, 
 +  "​author":​ "",​ 
 +  "​license":​ "​ISC",​ 
 +  "​dependencies":​ { 
 +    "​docker-secret":​ "​^1.1.2",​ 
 +    "​dotenv":​ "​^8.2.0",​ 
 +    "​express":​ "​^4.17.1",​ 
 +    "​express-async-errors":​ "​^3.1.1",​ 
 +    "​helmet":​ "​^3.23.3",​ 
 +    "​http-errors":​ "​^1.8.0",​ 
 +    "​log-timestamp":​ "​^0.3.0",​ 
 +    "​morgan":​ "​^1.10.0",​ 
 +    "​nodemon":​ "​^2.0.4",​ 
 +    "​pg":​ "​^8.3.0"​ 
 +  } 
 +
 + 
 +</​code>​ 
 + 
 +==== Baza de date ==== 
 + 
 +Pentru ​nu va pune sa instalati PostgreSQL pe sistemul vostru, si pentru ca traim in 2020, am ales sa folosim **docker** si **docker-compose** pentru rularea postgres localChiar daca inca nu stiti ce inseamna, nu este nicio problema, peste 3 workshop-uri veti cunoaste toate aceste lucruri. 
 + 
 +Pentru rularea bazei de date, trebuie sa dati comanda de mai jos in folderul **database-deploy** 
 + 
 +<code bash> 
 +docker-compose up 
 +</​code>​  
 + 
 +Structura bazei de date pentru partea de biblioteca o puteti regasi [[https://​dbdiagram.io/​d/​5da063faff5115114db5261e|aici]],​ urmarind tabelele care sunt prefixate cu **Library**. 
 +==== Exercitii ==== 
 + 
 +  - Recapitulare javascript 
 +  - Recapitulare NodeJS 
 +  - Realizati controllerele si serviciile pentru interactiunea cu Books 
 + 
 +<note important>​Pentru Books, va trebui sa dati si id-ul utilizatorului care a introdus cartea. Pentru ca, momentan, nu avem serviciul de autentificare si autorizare scris, ignorati acest lucru si puneti orice valoare de tip int. Vom modifica acest lucru in momentul in care realizam serviciul de auth si, deci, vom avea utilizatori</​note>​
moby/backend/03.1590944185.txt.gz · Last modified: 2020/05/31 19:56 by alexandru.hogea
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0