DEV Community

Cover image for Angular 10 | MEAN, Google auth, JWT, Lazyload, upload de archivos, Guards, Pipes, Zona admin, dashboard y mucho más.
Dennys José Márquez Reyes
Dennys José Márquez Reyes

Posted on • Updated on

Angular 10 | MEAN, Google auth, JWT, Lazyload, upload de archivos, Guards, Pipes, Zona admin, dashboard y mucho más.

2020-2022

Este es un challenge de un curso de Fernando herrera.

Sistema de hospitales — para controlar médicos, hospitales y usuarios

Les comento que me he disfrutado este curso ☕ si señor fueron casi 2 años mientras iba echando códigos, aprendiendo dentro de mi día a día y mis responsabilidades laborales, vean todos los Repositorios.

Me divertí, realicé este curso para refrescar conocimientos y obtener nuevos.

Mi perfil en LinkedIn: 💡Dennys Jose Marquez Reyes 🧠 | LinkedIn 👍

Demo: https://adminpro-system-hospitals.onrender.com/

Código fuentes

Cliente: https://github.com/dennysjmarquez/angular-adv-adminpro

Server: https://github.com/dennysjmarquez/angular-adv-adminpro-backend

Bien, comencemos a describir todo lo que hice utilice y aprendí de este maravilloso curso:

MEAN Stack

Mongo, Express, Angular, Node.js.


Sesión 1 — Front-End


Google SignIn protegido por token desde el Front-End hasta el Backend

Uso de librerías de terceros en proyectos de Angular, gapi Google Sign-In, JQuery, etc.

Rutas con configuraciones.

Control de versiones y releases.

Manejo de módulos, Servicios, Lazyload.

Rutas hijas — ForChild( )@inputs@Outputs y @ViewChild — Referencia a elementos en el HTML.

Implementación de Charts (Gráficas) de ng2-charts.

Reactive Forms, Validaciones del formulario, uso de SweetAlert, Guardar información en el LocalStorage

Rxjs Observables, pipes: RetryTakefilter, map

El uso de interval, Observable, Observer.

returnObservable(): Observable<number> {
   let i = 0;

   const ob$ = new Observable((observer: Observer<number>) => {

      const interval = setInterval(() => {
         observer.next(i);

         if (i === 4) {
            clearInterval(interval);
            observer.complete();
         }

         if (i === 2) {
            i = 0;

            observer.error('i llego al valor 2');
         }

         ++i;
      }, 1000);
   });

   return ob$;
}
Enter fullscreen mode Exit fullscreen mode

this._intervalSubs = this.returnInterval()
      .pipe(
         // Especifica cuantas veces se va a ejecutar el Observable
         take(10),

         // Sirve para filtrar los valores y en este caso solo se muestran
         // los números pares
         filter((value) => value % 2 === 0),

         // Este operador recibe la información y la muta
         map((value) => {
            return 'Hola mundo ' + (value + 1);
         })
      )
      .subscribe(
         (valor) => console.log('[returnInterval] valor', valor),
         (error) => console.warn('[returnInterval] Error', error),
         () => console.log('[returnInterval] Terminado')
      );
}
returnInterval() {
   return interval(100);
}
Enter fullscreen mode Exit fullscreen mode

Pipe de Angular para mostrar una imagen de una URL o desde el server

import { Pipe, PipeTransform } from '@angular/core';
import { environment } from '@env';

const baseUrl = environment.baseUrl;

@Pipe({
  name: 'getImage',
})
export class GetImagePipe implements PipeTransform {
  transform(value: any, type: 'users' | 'medicos' | 'hospitals'): any {
    return value && value.includes('://') ? value : `${baseUrl}/upload/${type}/${value || 'no-imagen'}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Implantación de Lazyload con protección de rutas y carga de componentes

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { PagesComponent } from './pages.component';

// Guards
import { AuthGuard } from '../guards/auth.guard';

const APP_ROUTES: Routes = [
  // Template principal
  {
    path: 'dashboard',
    component: PagesComponent,
      canLoad: [AuthGuard],
      canActivate: [AuthGuard],
    loadChildren: () => import('./pages-child-router.module').then(module => module.PagesChildRouterModule),

    // Las rutas hijas se cargan con lazyload
    // children: [],
  },
];

const APP_ROUTING = RouterModule.forChild(APP_ROUTES);

@NgModule({
  declarations: [],
  imports: [CommonModule, APP_ROUTING],
  exports: [RouterModule],
})
export class PagesRouter {}
Enter fullscreen mode Exit fullscreen mode

Todo organizado en módulos, buenas prácticas 🤜🏻🤛🏻

Image description

Use ngZone.run() para notificar a Angular que refresque la vista, ya que algo sucedió fuera de los ciclos de vida de Angular y no lo detecta como se espera que fuese un proceso de Angular, porque es una librería externa que hizo un cambio fuera del control de cambio de Angular.

Image description

Más información sobre ngZone aquí

👇

https://dennysjmarquez.dev/magazine/ngzone-como-runoutsideangular-podria-reducir-las-llamadas-de-deteccion-de-cambios-H41jASdhwzMReJe2JUZk/

Image description

Sistema completo para identificar al usuario tanto en Google auth como con una cuenta normal en el back

google-auth.service.ts

import {Injectable} from '@angular/core';
import Swal, {SweetAlertIcon} from 'sweetalert2';

import {AuthService} from './auth.service';

declare var gapi: any;

@Injectable({
    providedIn: 'root'
})
export class GoogleAuthService {

    constructor(private _authService: AuthService) {
    }

    makertGoogleLoginBtn(options: {
        // Id del botón de Google en el HTML
        btnSignin: string,
        // Parámetros para el mensaje de Error si algo falla al iniciar la App para el login
        errors?: {
            title?: string,
            text?: string,
            icon?: SweetAlertIcon,
            confirmButtonText?: string
        },
        // Función de se llama luego de un inicio exitoso
        callbackStartApp: Function
    }) {


        // Renderiza el botón de Google
        gapi.signin2.render(options.btnSignin, {
            'scope': 'profile email',
            'width': 240,
            'height': 50,
            'longtitle': false,
            'onsuccess': (googleUser) => {},
            'onfailure': console.log
        });


        // Inicia el login con Google
        this._authService.google.startApp('goole-signin').then((profile: any) => {

            options.callbackStartApp(profile);


        }).catch(error => {
            Swal.fire({
                title: options?.errors?.title || 'Error!',
                text: options?.errors?.text || error?.error?.msg || 'Error desconocido',
                icon: options?.errors?.icon || 'error',
                confirmButtonText: options?.errors?.confirmButtonText || 'Ok'
            });
        });

    }

}
Enter fullscreen mode Exit fullscreen mode

auth.service.ts

import { Injectable, NgZone } from '@angular/core';
import { LoginGoogleData } from '../interfaces/login-google-data.interface';
import { tap } from 'rxjs/operators';
import { Observable, throwError } from 'rxjs';
import { UserModel } from '../models/user.model';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { environment } from '@env';
import { LoginForm } from '../interfaces/login-form.interface';

declare var gapi: any;
declare var $: any;

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  baseURL = environment.baseUrl;
   google_id = environment.GOOGLE_ID

  public currentUser: UserModel;

  constructor(private http: HttpClient, private _router: Router, private _ngZone: NgZone) {}

  google = {
    /**
     *
     * Obtiene una sesión de Google
     *
     */
    initGoogleAuth: () => {
      return new Promise((resolve) => {
        gapi.load('auth2', () => {
          this.google.startApp['gapiAuth2'] = gapi.auth2;

          // Retrieve the singleton for the GoogleAuth library and set up the client.
          const auth2Init = gapi.auth2.init({
            client_id: this.google_id,
            cookiepolicy: 'single_host_origin',
            // Request scopes in addition to 'profile' and 'email'
            //scope: 'additional_scope'
          });

          resolve(auth2Init);
        });
      });
    },

    /**
     *
     * Obtiene una sesión de Google y se coloca él escucha del evento clic sobre el botón de Google
     *
     * @param btnSignin {string} Id del botón de Google en el HTML
     */
    startApp: (btnSignin: string) =>
      new Promise(async (resolve, reject) => {
        // Se obtiene una sesión de Google
        const auth2Init: any = await this.google.initGoogleAuth();
        const element = document.getElementById(btnSignin);

        // Se captura el evento clic en el botón de Google
        auth2Init.attachClickHandler(
          element,
          {},
          (googleUser) => {
            const profile = googleUser.getBasicProfile();
            const token = googleUser.getAuthResponse().id_token;
                  $(".preloader").fadeIn();
            this.google.login({ token }).subscribe(
              (resp) => {
                resolve(profile);
              },
              (error) => {
                        $(".preloader").fadeOut();
                reject(error);
              }
            );
          },
          function (error) {
            alert(JSON.stringify(error, undefined, 2));
          }
        );
      }),

    /**
     *
     * Se intensifica en el servidor de la App
     *
     * @param gToken {string} Token devuelto por Google
     */
    login: (gToken: LoginGoogleData) => {

         this.resetCurrentUser();

      return this.http.post(`${this.baseURL}/login/google`, gToken).pipe(
        tap(({ token = '' }: any) => {
          localStorage.setItem('token', token);
        }),
        tap((data: any) => this.setCurrentUser(data))
      );
    },

    /**
     *
     * Lleva a cabo el logOut de la App
     *
     * @param callback {Function} Función anónima que es llamada luego que se haya hecho el logOut
     */
    logOut: (callback?: Function) => {
      const logOut = () => {
        this.resetCurrentUser();
        const auth2 = this.google.startApp['gapiAuth2'].getAuthInstance();

        auth2.signOut().then(() => {
          typeof callback === 'function' && this._ngZone.run(() => callback());
        });
      };

      // Por si se pierde la sesión porque se refresca la pagina
      if (!this.google.startApp['gapiAuth2']) {
        this.google.initGoogleAuth().then(() => logOut());
      } else {
        logOut();
      }
    },
  };

  /**
   *
   * Obtiene el Token y lo almacena localmente
   *
   */
  get token(): string {
    return localStorage.getItem('token') || '';
  }

  /**
   *
   * Valida el token este método se usa en auth.guard para conceder el acceso o deniegarlo
   * en ciertas zonas o paginas también almacena información sensible del usuario
   * en este servicio, tales como: name, email, img, google, role, uid
   *
   * En la prop public user: UserModel de la class
   *
   */
  validateToken(): Observable<any> {
    // Obtiene el Token almacenado localmente
    const token = this.token;

    // Se chequea primero si el token existe antes de ser enviado al servidor para su validación
    if (!token) {
      return throwError('Usuario no logeado');
    }

    return this.http
      .get(`${this.baseURL}/login/tokenrenew`, { headers: { Authorization: token } })

      .pipe(
        tap(({ token = '' }: any) => {
          // Almacena el nuevo token
          localStorage.setItem('token', token);
        }),
        tap((data: any) => this.setCurrentUser(data))
      );
  }

  loginUser(formData: LoginForm): Observable<any> {

      this.resetCurrentUser();

    return this.http.post(`${this.baseURL}/login`, formData).pipe(
      tap(({ token = '' }: any) => {
        localStorage.setItem('token', token);
      }),
      tap((data: any) => this.setCurrentUser(data))
    );
  }

  resetCurrentUser() {
    this.currentUser = new UserModel(null, null, null, null, null, null, null);
      localStorage.removeItem('token');
  }

  private setCurrentUser({ usuario: { name, email, img, google, role, uid } }) {
    this.currentUser = new UserModel(name, email, '', img, google, role, uid);
  }
}
Enter fullscreen mode Exit fullscreen mode

Models

Image description

Interfaces

Image description

Image description

Al respecto, de sí usar Class o Interfaces, les dejo este artículo para más información

Usar Modelos, clases e Interfaces en Angular

👇

https://dennysjmarquez.dev/magazine/usar-modelos-clases-e-interfaces-en-angular-lH8cmIS9YrgW0nONyoQe/

Uso de import { FormBuilder, FormGroup, Validators } from ‘@angular/forms’;


Custom validator o validaciones a medida

Image description

Image description

Mantenimientos de Hospitales, usuarios y médicos

Image description


Image description

Image description

Usuarios

Image description

Image description

Image description

Hospitales

Image description

Image description

Image description

Médicos

Image description

Image description

Image description

Profile

Image description


Sesión 2 — Back-End


Node — Express — MongoDB

Demo: https://adminpro-system-hospitals.onrender.com/

Código fuente Server: https://github.com/dennysjmarquez/angular-adv-adminpro-backend


Image description

Uso de MongoDb compassMongo Atlas para alojamiento de la dB y configuraciones.

Configuaciones como ejemplo: añadir la IP 0.0.0.0/0, en Network Access de MongoDB Atlas con lo que abriríamos nuestra dB para que cualquier dirección IP pueda conectarse. 🤘🏻

Conectar el Back con Mongo Atlas usando Mongoosejs

database/config.js

Image description

index.js

Image description

Creación de modelos para interactuar con la dB de MongoDB Atlas CRUD

Modelos para los Hospitales, Usuarios y Médicos

Image description

Schema con referencias y el uso de populate, para agregar información extra o necesaria al esquema en cuestión.

models/hospital.model.js

Image description

models/medico.model.js

Image description

controllers/hospitals.controller.js

Image description

controllers/medicos.controller.js

Image description

Manejo de los nombres de los esquemas a medida, con { collection: ‘hospitales’ } lo podemos personalizar 🤟🏻

Image description

Image description

models/hospital.model.js

const { Schema, model } = require('mongoose');

const hospitalSchema = Schema(
  {
    name: {
      type: String,
      required: true,
    },
    img: {
      type: String,
    },
    user: {
      required: true,
      type: Schema.Types.ObjectID,
      ref: 'User',
    },
  },

  // Por defecto mongoose le agrega al los modelos una s al final del nombre del modelo,
  // y en este caso sería por defecto “Hospitals” y con esta opción le damos un
  // nombre personalizado “hospitales” y así va a aparecer en la Db de mongoose
  { collection: 'hospitales' }
);

// Esto para modificar los nombres de los campos retornados de la Db
hospitalSchema.method('toJSON', function () {
  // Al extraer los campos dejan de ser regresados, como por ejemplo
  // el Password no conviene que se muestre ese valor por seguridad y
  // por lo tanto no se regresa , igual se extrae el __v por pura estetica

  const { __v, _id, ...object } = this.toObject();

  object.uid = _id;

  return object;
});

module.exports = model('Hospital', hospitalSchema);
Enter fullscreen mode Exit fullscreen mode

models/medico.model.js

const { Schema, model } = require('mongoose');

const medicoSchema = Schema({
  user: {
    type: Schema.Types.ObjectID,
    ref: 'User',
    required: true,
  },
  name: {
    type: String,
    required: true,
  },
  img: {
    type: String,
  },
  hospital: {
    type: Schema.Types.ObjectID,
    ref: 'Hospital',
    required: true,
  },
});

// Esto para modificar los nombres de los campos que retornados de la Db
medicoSchema.method('toJSON', function () {
  // Al extraer los campos dejan de ser regresados, como por ejemplo
  // el Password, no conviene que se muestre ese valor por seguridad,
  // por lo tanto, no se regresa.

  // Igual se puede extraer el __v por pura estética, también se puede
  // cambiar si se necesita el _id por uid, se retornaría el object
  // con los campos modificados.

  const { __v, _id, ...object } = this.toObject();
  object.uid = _id;

  return object;
});

module.exports = model('Medico', medicoSchema);
Enter fullscreen mode Exit fullscreen mode

models/usuario.model.js

const {ROLES} = require('../constant');
const {Schema, model} = require('mongoose');

const userSchema = Schema({
   name: {
      type: String,
      required: true,
   },
   email: {
      type: String,
      required: true,
      unique: true,
   },
   password: {
      type: String,
      required: true,
   },
   img: {
      type: String,
   },
   role: {
      type: String,
      required: true,
      default: ROLES.USER_ROLE,
   },
   google: {
      type: Boolean,
      default: false,
   },
});

// Esto para modificar los nombres de los campos retornados de la Db
userSchema.method('toJSON', function () {

   const {__v, _id, password, ...object} = this.toObject();

   object.uid = _id;

   return object;
});

module.exports = model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

Validación del JWT

Haciendo uso del Middleware — middlewares/validate-jwt.middleware.js

Image description

El uso de express-validator para _v_alidar los datos enviados al servidor en el body

routes/auth.route.js

Es usado en las rutas como un Middleware

Image description

Validar un MongoID

.isMongoId()

check('hospital', 'El id del hospital no es válido').isMongoId(),

Enter fullscreen mode Exit fullscreen mode

CRUD de médicos, usuarios y hospitales

Uso de los modelos de mongoose para obtener los datos, buscar, actualizar y borrar información en la dB

Con el uso de los Schema se crea el modelo

El Modelo de Hospitales como ejemplo:

models/hospital.model.js

Obtener todos los hospitales data guardada en la collection.

Image description

Guardar información.

Image description

Actualizar la información.

Image description

Borrar información un hospital.

Image description

Búsqueda de un Hospital haciendo uso de las expresiones regular.

controllers/search.controller.js

Image description

Image description

Si se usa find({}) sin parámetros devuelve toda la collection.

Búsquedas en varias colecciones a la vez.

controllers/search.controller.js

Image description

Paginación de los datos haciendo uso de .skip y .limit

Image description

Protección de rutas basadas en JWT y sistema de Roles

constant.js

/**
 *
 * Global constants file
 *
 */
module.exports = {
   ROLES: {
      ADMIN_ROLE: 'ADMIN_ROLE',
      USER_ROLE: 'USER_ROLE'
   }
}
Enter fullscreen mode Exit fullscreen mode

middlewares/validate-role.middleware.js

const { request, response } = require('express');
const UsersModel = require('../models/usuario.model');

const validateRole =
  (roles = [], paramsUID = false) =>
  async (req = request, res = response, next) => {
    try {
      let getParamsUID;
      const { uid } = req.usuario;

      // Obtener el usuario del uid
      const usuario = await UsersModel.findById(uid);

      if (paramsUID) {
        getParamsUID = req.params.id;
      }

      if (!usuario || !roles.includes(usuario.role) && !(paramsUID && getParamsUID === uid)) {
        return res.status(403).json({
          msg: 'Acceso denegado',
        });
      }

      next();
    } catch (e) {
      return res.status(403).json({
        msg: 'Acceso denegado',
      });
    }
  };

module.exports = {
  validateRole,
};
Enter fullscreen mode Exit fullscreen mode

middlewares/validate-jwt.middleware.js

const { response, request } = require('express');
const jwt = require('jsonwebtoken');

const validateJWT = async (req = request, res = response, next) => {
  const { authorization: token } = req.headers;

  try {
    if (!token) {
      return res.status(401).json({
        msg: 'Token no definido',
      });
    }

    // Leer Token
    const data = await jwt.verify(token, process.env.JWT_SECRET);
    const { uid, role } = data.payLoad;

    // se pasa al controlador el uid
    req.usuario = { uid, role };

    next();
  } catch (e) {
    res.status(500).json({
      msg: 'Token no valido',
    });
  }
};

module.exports = {
  validateJWT,
};
Enter fullscreen mode Exit fullscreen mode

Login con Google y verificación de su Token

helpers/googleVerifyIdToken.helper.js

const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_ID);

const googleVerifyIdToken = async (token) => {

  const ticket = await client.verifyIdToken({
    idToken: token,
    audience: process.env.GOOGLE_ID,  // Specify the CLIENT_ID of the app that accesses the backend
  });

  return { email, name, picture } = ticket.getPayload();

}


module.exports = {
  googleVerifyIdToken
}
Enter fullscreen mode Exit fullscreen mode

controllers/auth.controller.js

const loginGoogle = async (req = request, res = response) => {
  const { token: G_token } = req.body;

  try {
    const { email, name, picture } = await googleVerifyIdToken(G_token);

    // Se chequea si el usuario existe o se va a crear uno nuevo
    const userDB = await UsersModel.findOne({ email });

    let userNew;

    if (!userDB) {
      userNew = new UsersModel({
        password: '123456',
        name,
        email,
        google: true,
        img: picture,
      });

      // Guarda en la Db el user
      await userNew.save();
    } else {
      userNew = userDB;
      userNew.google = true;

      // Guarda en la Db el user
      await userNew.save();
    }

    const payLoad = {
      uid: userNew.id,
      role: userNew.role,
    };

    // Genera un Token de JWT
    const token = await generateJWT(payLoad);

    res.json({
      token,
      usuario: userNew
    });
  } catch (e) {
    console.log(e);

    res.status(500).json({
      msg: 'El Token no es correcto ',
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Login normal.

controllers/auth.controller.js

Uso de findOne para devolver la primera conciencien en la collection.

const login = async (req = request, res = response) => {
  const { email, password } = req.body;

  try {
    // Verifica el Email
    const userDb = await UsersModel.findOne({ email });
    if (!userDb) {
      return res.status(404).json({
        msg: 'No se ha podido encontrar tu cuenta',
      });
    }

    // Verifica el Password
    const validPass = bcrypt.compareSync(password, userDb.password);

    if (!validPass) {
      return res.status(400).json({
        msg: 'Contraseña incorrecta',
      });
    }

    const payLoad = {
      uid: userDb.id,
      role: userDb.role,
    };

    // Genera un Token de JWT
    const token = await generateJWT(payLoad);

    res.json({
      token,
      usuario: userDb
    });
  } catch (e) {

    res.status(500).json({
      msg: 'Error inesperado… revisar logs',
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Uso de express-fileupload para subir archivos

routes/upload.route.js

const { Router } = require('express');
const router = Router();
const { ROLES } = require('../constant');

// Middlewares
const { validateJWT } = require('../middlewares/validate-jwt.middleware');
const { validateUploads } = require('../middlewares/validate-uploads.middleware');

const fileUpload = require('express-fileupload');
router.use(fileUpload());

// Controllers
const { upLoad, returnImg } = require('../controllers/upload.controller');
const { validateRole } = require('../middlewares/validate-role.middleware');


router.put('/:type/:id', [validateJWT, validateRole([ROLES.ADMIN_ROLE], true), validateUploads], upLoad);

router.get('/:type/:photo', [validateUploads], returnImg);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

controllers/upload.controller.js

const { request, response } = require('express');
const { v4: uuidv4 } = require('uuid');
const { upDateImage } = require('../helpers/upDate-image.helper');
const path = require('path');
const fs = require('fs');


const upLoad = async (req = request, res = response) => {

  try {

    const { id, type } = req.params;

    // Valida que se haya mandado un archivo
    if (!req.files || Object.keys(req.files).length === 0) {
      return res.status(400).json({
        msg: 'Error: No se ha mandado ningún archivo'
      });
    }

    // Se procesa la imagen

    const file = req.files.image;
    const nameSplit = file.name.split('.');
    const extFile = nameSplit[nameSplit.length -1].toLowerCase();

    // Extensiones Validas permitidas
    const mimeTypeValid = [
      'image/jpeg',
      'image/png',
      'image/gif'
    ];

    // Verifica que lo que se envié sea del tipo permitido
    if(!mimeTypeValid.includes(file.mimetype)){

      return res.status(400).json({
        msg: 'Error: No es un archivo permitido'
      });

    }

    // Genera el nuevo nombre del archivo
    const nameFile = `${ uuidv4() }.${ extFile }`;

    // Path para guardar el archivo

    const path = `./uploads/${type}/${nameFile}`;

    // Mueve la imagen
    await file.mv(path, (err) =>{

      if (err){

        console.log(err);

        res.status(500).json({
          msg: 'Error inesperado no se pudo subir la imagen… revisar logs'
        });

      }

      // Actualizar base de datos
      upDateImage(type, id, nameFile);


      res.json({
        upLoad: true,
        nameFile
      });

    });

  }catch (e) {

    console.log(e)

    res.status(500).json({
      msg: 'Error inesperado… revisar logs'
    });

  }

}

const returnImg = async (req = request, res = response) => {

  try {

    const {photo, type} = req.params;
    let pathImg = path.join(__dirname, `../uploads/${type}/${photo}`);

    // Si no existe la imagen se manda una por defecto
    if(!fs.existsSync(pathImg)){

      pathImg = path.join(__dirname, `../uploads/no-img.jpg`);

    }

    return res.sendFile(pathImg);

  } catch (e) {

    console.log(e)

    res.status(500).json({
      msg: 'Error inesperado… revisar logs'
    });

  }

}

module.exports = {

  upLoad,
  returnImg

};
Enter fullscreen mode Exit fullscreen mode

helpers/upDate-image.helper.js

const fs = require('fs');

const UsersModel = require('../models/usuario.model');
const HospitalsModel = require('../models/hospital.model');
const MedicosModel = require('../models/medico.model');

const deleteImg = (path) => {
  if (fs.existsSync(path)) {
    try {
      fs.unlinkSync(path);
    } catch (e) {
      return false;
    }
  }
};

const upDateImage = async (type, id, nameFile) => {
  switch (type) {
    case 'hospitals': {
      const hospital = await HospitalsModel.findById(id);

      if (!hospital) {
        console.log('El id del hospital no existe');

        return false;
      }

      if (hospital.img) {
        const oldPath = `./uploads/${type}/${hospital.img}`;

        // Borrar la imagen anterior
        deleteImg(oldPath);
      }

      hospital.img = nameFile;

      try {
        await hospital.save();

        return true;
      } catch (e) {
        return false;
      }
    }

    case 'medicos': {
      const medico = await MedicosModel.findById(id);

      if (!medico) {
        console.log('El id del medico no existe');

        return false;
      }

      if (medico.img) {
        const oldPath = `./uploads/${type}/${medico.img}`;

        // Borrar la imagen anterior
        deleteImg(oldPath);
      }

      medico.img = nameFile;

      try {
        await medico.save();

        return true;
      } catch (e) {
        return false;
      }
    }

    case 'users': {
      const user = await UsersModel.findById(id);

      if (!user) {
        console.log('El id del hospital no existe');

        return false;
      }

      if (user.img) {
        const oldPath = `./uploads/${type}/${user.img}`;

        // Borrar la imagen anterior
        deleteImg(oldPath);
      }

      user.img = nameFile;

      try {
        await user.save();

        return true;
      } catch (e) {
        return false;
      }
    }
  }
};

module.exports = {
   upDateImage
}

Enter fullscreen mode Exit fullscreen mode

Habilitación de una carpeta pública para servir el proyecto de Angular compilado

index.js

Image description


Sesión 3 — Pruebas unitarias y de integración


Demo: https://replit.com/@dennysjmarquez/angular-13-unit-test-and-integration-demo

Código fuente: https://github.com/dennysjmarquez/angular-13-unit-test-and-integration

Las pruebas están separadas en 4 categorías:

Básicas

En estas pruebas verán la comprobación de Arrays, La comprobación de los booleans y las diferentes formas de hacer esto

Ej. expect(resp).toBe(true) expect(resp).toBeTrue() expect(resp).toBeTruthy()

// la Negación puede ser asi o usar uno que evalué un false

expect(resp).not.toBeTruthy()

También muestro el cómo hacer un test de funciones que están dentro de una class, probando el return de la misma, Pruebas con números usando toBe, string uso de toContain expect(typeof resp).toBe('string') familiarización con la evaluación de expect, siclos de vida del describe de Jasmine, tales como beforeAll, beforeEach, afterAll, afterEach y en que caso usar cada uno de ellos.

Intermedias

Esta sección trabaja con pruebas un poco más complejas y reales:

  1. Pruebas sobre Event Emitter
  2. Formularios
  3. Validaciones
  4. Saltar pruebas
  5. Espías
  6. Simular retornos de servicios
  7. Simular llamado de funciones

Esta sección da fundamentos muy valiosos para realizar pruebas unitarias y de integración Se hacen comprobaciones simples de un componente haciendo usos de cosas simples como estas component = new Form(new FormBuilder()), aquí ya se empieza a ver los spyOn() para espiar algunos métodos de algunos servicios y hacer a las pruebas en relación con los resultados de estos métodos.

Intermedias 2

Esta sección se enfoca en las pruebas de integración:

  1. Aprender la configuración básica de una prueba de integración
  2. Comprobación básica de un componente
  3. TestingModule
  4. Archivos SPEC generados automáticamente por el AngularCLI
  5. Pruebas en el HTML
  6. Revisar inputs y elementos HTML
  7. Separación entre pruebas unitarias y pruebas de integración

Ya aquí empiezo a usar a TestBedComponentFixtureconfigureTestingModule que es una copia limitada de lo que sería el @NgModule, pero para las pruebas y donde se va a poder insertar módulos componentes y servicios, también controlo ya aquí lo que es el siclo de control de cambios de Angular mediante el uso de detectChanges para que se puedan hacer pruebas de integración, ya que con esto se actualiza el HTML.

En esta sesión ya empiezo a usar a debugElement.query() y By.css para acceder al HTML y hacer las comprobaciones necesarias en una prueba de integración.

Avanzadas

Esta sección es un verdadero reto, especialmente entre más te vas acercando al final de la misma. Aquí veremos temas como:

  1. Revisar la existencia de una ruta
  2. Confirmar una directiva de Angular (router-outlet y routerLink)
  3. Errores por selectores desconocidos
  4. Reemplazar servicios de Angular por servicios falsos controlados por nosotros
  5. Comprobar parámetros de elementos que retornen observables
  6. Subject
  7. Gets

En estas pruebas haremos comprobaciones de los params del ActivatedRoute, y comprobaremos la navegación del Router, con toHaveBeenCalledWith Verificando que se llame con los parámetros indicados para la ruta ruta en cuestion.

-FIN-

Top comments (0)