Differences

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

Link to this comparison view

moby:backend:03 [2020/07/23 14:58]
alexandru.hogea [Resurse]
moby:backend:03 [2020/07/27 15:14] (current)
alexandru.hogea [Resurse]
Line 8: Line 8:
 ==== Resurse ==== ==== Resurse ====
  
-Puteti pleca de la urmatorul ​[[https://gitlab.com/mobyworkshop/library-microservice.git|schelet de cod]]. Sunt implementate urmatoarele:​+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: Un REST API poate fi structurat, cel mai des, in doua moduri:
Line 16: Line 16:
  
 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...). 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) === === Scriptul de start (start.js) ===
  
Line 26: Line 29:
 const createError = require('​http-errors'​);​ // pachet folosit pentru trimiterea de erori const createError = require('​http-errors'​);​ // pachet folosit pentru trimiterea de erori
  
-require('​express-async-errors'​);​ // pachet folosit pentru captarea erorilor pe rute+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 eroare. Este 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() require('​log-timestamp'​);​ // pachet folosit pentru injectarea timpului in momenul in care se da console.log()
  
Line 104: Line 107:
     } = await pool.query(text,​ params);     } = await pool.query(text,​ params);
     return rows;     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.+  }; // 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 = {   module.exports = {
Line 130: Line 133:
 </​code>​ </​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 a 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 local. Chiar 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.1595505513.txt.gz · Last modified: 2020/07/23 14:58 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