Bienvenidos sean a este post, hoy si trabajaremos de verdad con las notas.
Si vienen siguiendo esta saga de posts donde comenzamos con nuestra aplicacion Notas en Express hasta ahora siempre hemos almacenado nuestras notas en la memoria por lo tanto estas existian mientras la aplicacion no terminara, al momento de volver a ejecutar nuevamente debiamos volver a crearlas, a partir de este post vamos a trabajar con distintas formas de almacenarlas y en este primer caso lo veremos mediante el filesystem, es decir como un archivo en nuestro PC y si bien no dispondremos de muchas de las herramientas para realiza nuestras consultas o queries dada la simplicidad de nuestra aplicacion podemos utilizar sin mayor complejidad, para ello necesitaremos nuestra aplicacion sino tienen su codigo les dejo un link para descargarlo:
Una vez descargado simplemente extraigan el directorio en el PC y ya tenemos nuestra aplicacion funcional, nuestro primer paso sera ir al archivo Notas.mjs en el directorio models, en este iremos a la clase Notas y le agregaremos dos funciones:
export class Nota {
... instrucciones anteriores se mantienen iguales
get JSON() {
return JSON.stringify({
clave: this.clave,
titulo: this.titulo,
cuerpo: this.cuerpo
});
}
static desdeJSON(json) {
const datos = JSON.parse(json);
if (typeof datos !== 'object'
|| !datos.hasOwnProperty('clave')
|| typeof datos.clave !== 'string'
|| !datos.hasOwnProperty('titulo')
|| typeof datos.titulo !== 'string'
|| !datos.hasOwnProperty('cuerpo')
|| typeof datos.cuerpo !== 'string') {
throw new Error(`No es una nota: ${json}`);
}
const nota = new Nota(datos.clave, datos.titulo, datos.cuerpo);
return nota;
}
}
La primera funcion sera obtener los datos de nuestro archivo de tipo JSON, simplemente lo toma y obtiene las propiedades de nuestras notas, en cambio la segunda sera la encargada de crear la nota en si donde primero creara el objeto llamado datos desde el archivo que informemos, despues tendremos un condicional donde verificara primero si datos no es un objeto, si no posee algunas de las propiedades de la nota o si algunas de ellas no es de tipo string, si se cumple algunas de estas procede a crear una excepcion informando que no es una nota, en caso de no cumplirse alguna sigue con la siguiente instruccion donde crea el objeto que es la nota con cada una de las propiedades de la misma y devuelve este objeto, nuestro siguiente paso sera crear el modulo que se encargara de crear las notas en el filesystem, para ello en el directorio models deben crear un nuevo archivo con el nombre de notas-fs.mjs y agregar el siguiente codigo:
models/notas-fs.mjs
import fs from 'fs-extra';
import path from 'path';
import util from 'util';
import { approotdir } from '../approotdir.mjs';
import { Nota, AlmacenNotasAbs } from './Notas.mjs';
import { default as DBG } from 'debug';
const debug = DBG('notas:notas-fs');
const error = DBG('notas:error-fs');
Este sera la seccion donde importaremos todos los modulos que necesitaremos para nuestro modulo, salvo el primero que es el nuevo sobre el resto ya hemos hablado y/o utilizado, donde path es para manejar todo lo relacionado a los paths en el PC, util nos brindara la funcion inspect para trabajar, approotdir nos brinda el directorio raiz de nuestra aplicacion, despues tenemos el archivo con las clases de las notas, las siguientes tres lineas son las encargadas de la depuracion, sobre esto hablamos en este post, ahora hablemos sobre el primer paquete que es muy similar a fs pero fs-extra trae algunas herramientas utiles extras, en nuestro caso sera ensureDir, con esto comentado agregamos el siguiente bloque de codigo:
export default class AlmacenNotasFS extends AlmacenNotasAbs {
async cerrar() {}
async update(clave, titulo, cuerpo) {
return crupdate(clave, titulo, cuerpo);
}
async create(clave, titulo, cuerpo) {
return crupdate(clave, titulo, cuerpo);
}
async read(clave) {
const notasdir = await notasDir();
const lanota = await leerJSON(notasdir, clave);
return lanota;
}
async delete(clave) {
const notasdir = await notasDir();
await fs.unlink(filePath(notasdir, clave));
}
async listarClaves() {
const notasdir = await notasDir();
let archivoz = await fs.readdir(notasdir);
if (!archivoz || typeof archivoz === 'undefined')
archivoz = [];
const lasnotas = archivoz.map(async fname => {
const clave = path.basename(fname, '.json');
const lanota = await leerJSON(notasdir, clave);
return lanota.clave;
});
return Promise.all(lasnotas);
}
async contar() {
const notasdir = await notasDir();
const archivoz = await fs.readdir(notasdir);
return archivoz.length;
}
}
En este caso definimos una nueva clase que se encargara del CRUD de las notas en el FileSystem, al igual que lo hicimos en el caso de almacenar en memoria a esta clase la hacemos heredera de AlmacenNotasAbs, y en esta nueva clase definiremos cada uno de los prototipos en la clase madre, en este caso no definimos a cerrar pero si al resto, esta funcion por el tipo de almacenamiento que usaremos no sera necesario pero proximamente si, la funcion update sera para actualizar la nota que le pasemos, la siguiente funcion llamada create se usa para crear una nueva nota, en ambos caso reciben los mismos datos y en ambos casos los pasaremos a la misma funcion que aun no definimos pero en ambos casos devolveremos el resultado de esta, luego tenemos la funcion read que se encarga de leer la nota que le pasemos mediante su clave, para obtener la nota usaremos la funcion leerJSON y lo obtenido lo devolveremos, y lo siguiente sera la funcion delete que usaremos para borrar las notas, aqui usaremos a filePath junto a unlink para desvincularlo, sobre la funcion filePath ya la definiremos en un momento.
La siguiente definicion es la funcion para listar las claves, primero leeremos el contenido del directorio de las notas, despues tendremos un condicional donde verifica si el resultado o el tipo son iguales a undefined, en caso de ser verdadero genera un archivoz como un array vacio, despues generamos un objeto llamado lasnotas, en esta mapearemos el resultado devuelto en archivoz, en este almacenaremos como clave el nombre del archivo y el valor asociado a la clave de la nota, una vez finalizado devolvemos el objeto final mediante una promesa, por ultimo tenemos una funcion llamada contar la cual contara la cantidad de notas que disponemos, generamos un objeto de tipo coleccion mediante readdir y devolveremos el valor que obtendremos mediante length, con esto ya tenemos nuestra clase definida, ahora en el mismo archivo agreguemos el siguiente codigo:
async function notasDir() {
const dir = process.env.NOTAS_FS_DIR
|| path.join(approotdir, 'notas-fs-datos');
await fs.ensureDir(dir);
return dir;
}
const filePath = (notasdir, clave) => path.join(notasdir, `${clave}.json`);
async function leerJSON(notasdir, clave) {
const leerDesde = filePath(notasdir, clave);
const datos = await fs.readFile(leerDesde, 'utf8');
return Nota.desdeJSON(datos);
}
async function crupdate(clave, titulo, cuerpo) {
const notasdir = await notasDir();
if (clave.indexOf('/') >= 0) {
throw new Error(`clave ${clave} no puede tener '/'`);
}
const nota = new Nota(clave, titulo, cuerpo);
const escribirA = filePath(notasdir, clave);
const escribirJSON = nota.JSON;
await fs.writeFile(escribirA, escribirJSON, 'utf8');
return nota;
}
Estas son las funciones ayudantes que utilizamos en la clase anterior, la primera funcion sera para obtener el directorio de las notas, observen que tenemos un objeto donde verifica si pasamos una variable de entorno y en caso contrario pasamos a crear todo un path donde estaran nuestas notas, despues usaremos un await para el ensureDir donde verifica si el directorio existe sea cual sea, para finalmente devolverlo, la siguiente sera la encargada de obtener el path del archivo, para ello usaremos el valor generado con la funcion anterior y el nombre del archivo sera la clave, la siguiente sera la encargada de leer el archivo JSON, se encarga de leerlo pero antes de enviarlo usaremos a desdeJSON para verificar que el archivo sea de este tipo, por ultimo tenemos la funcion crupdate que sera la encargada de crear/actualizar las notas, primero generamos el directorio de las notas, despues tenemos el condicional donde verifica si la barra esta ubicada en el valor de clave, en caso de ser verdadero devuelve un error informando que no puede tenerlo,, despues creara un nueva nota mediante la clase Nota, definimos una variable donde almacena el path y el nombre del archivo a escribir, despues definimos un objeto de tipo JSON donde estaran todos los valores de la nota, para finalmente escribir el archivo, recuerden que utf8 es el charset a escribir, por ultimo devolvemos la nota creada, con esto tenemos un 70% realizado pero todavia nos falta.
Antes de continuar con otros archivos necesitamos instalar el modulo fs-extra, para ello dentro del directorio notas ejecuten el siguiente comando:
$ npm install fs-extra --save-dev
Con esto ya tenemos dos formas de trabajar con las notas pero necesitamos alguna forma de poder diferenciarlas, una buena opcion seria poder pasar una variable la cual establezca cual debe utilizarse sin tener la necesidad de cambiar nuestro codigo fuente cada vez que necesitemos trabajar con otro metodo de almacenamiento (⚠Spoiler⚠ al final Notas tendra multiples formas de almacenar informacion), si utilizaramos a CommonJS no habria inconvenientes porque nos permite pasar una variable a la funcion require pero import del ES6 no permite esto por lo tanto debemos entrar en accion con otro tema.
Import dinamico
Como dijimos el import que estuvimos usando hasta ahora no nos sirve para lo que necesitamos pero este tiene una opcion llamada dynamic, esta caracteristica nos permite computar dinamicamente el nombre de un modulo a cargar, para ver como trabaja vamos a implementarlo en nuestra aplicacion Notas y para ello en el directorio models debemos crear un archivo con el nombre de notas-almacen.mjs y le agregaremos el siguiente codigo:
models/notas-almacen.mjs
import { default as DBG } from 'debug';
const debug = DBG('notas:notas-almacen');
const error = DBG('notas:error-almacen');
var _AlmacenNotas;
export async function usarModelo(modelo) {
try {
let ModuloNotasAlmac = await import(`./notas-${modelo}.mjs`);
let ClaseNotasAlmac = ModuloNotasAlmac.default;
_AlmacenNotas = new ClaseNotasAlmac();
return _AlmacenNotas;
} catch(err) {
throw new Error(`No encuentro AlmacenNotas en ${modelo} porque ${err}`);
}
}
export { _AlmacenNotas as AlmacenNotas }
Al comienzo impotaremos el bloque que podemos usar para la depuracion, despues declaramos simplemente una variable, y finalmente tenemos una funcion que nos permite establecer un modelo para usar, utilizaremos un try/catch, en el bloque del try primero definiremos un objeto donde almacenaremos el resultado de utilizar a import con el informado como argumento, despues otro objeto que almacenara la clase predeterminada del modulo cargado, lo siguiente sera definir la variable que declaramos y esta se convertira en un objeto de la clase que cargamos anteriormente, para finalmente devolver este objeto, esto como lo ven es lo denominado como fabrica (factory) para que sin importar cual tipo de almacenamiento dispongamos y seleccionamos podamos usarlo de manera dinamica, y el bloque del catch es para simplemente devolver una notificacion en caso de error, y al final del archivo permitiremos que el objeto creado de la clase para almacenar sera exportado, con esto ya tenemos nuestro modulo terminado ahora debemos hacer las modificaciones pequeñas para poder usarlo, nuestra primera parada sera en el arcihvo notas-memoria.mjs en el directorio models donde buscaremos la siguiente linea:
export class NotasEnMemoria extends AlmacenNotasAbs {
Y la modificaremos de la siguiente manera:
export default class NotasEnMemoria extends AlmacenNotasAbs {
Esto debemos hacerlo asi porque recuerden que nuestra fabrica usara la clase predeterminada de cada almacenamiento, si repasan el de notas-fs notaran que ya lo hicimos, nuestro siguiente paso sera en app.mjs donde buscaremos las siguientes lineas:
import { NotasEnMemoria } from './models/notas-memoria.mjs';
export const NotasMem = new NotasEnMemoria();
Estas dos lineas pueden comentarlas o eliminarlas pero deben ser anuladas y luego agregaremos el siguiente segmento de codigo:
import { usarModelo as usarModeloNotas } from './models/notas-almacen.mjs';
usarModeloNotas(process.env.MODELO_NOTAS
? process.env.MODELO_NOTAS
: "memoria")
.then(almacen => {})
.catch(error => {onError({ code: 'ENOTESSTORE', error}); });
Primero importaremos a la funcion usarModelo de nuestra «fabrica» en notas-almacen pero lo renombraremos a usarModeloNotas, lo siguiente sera pasarle el modelo y para ello usaremos la variable de entorno MODELO_NOTAS, en caso de existir un valor se lo pasamos de lo contrario pasaremos la predeterminada que es memoria, es decir que utilizara el modelo que usaba antes, despues usaremos a then y catch, para tomar los promesas. Con then al no necesitar procesarlas mas alla le enviaremos a una funcion vacia para que no devuelva ningun error y en el catch utilizaremos a onError para que procese al error que surja, pero noten que utilizamos un nuevo codigo de error y este sera nuestro siguiente paso porque ahora debemos ir al archivo appsupport.mjs y en el switch de la funcion onError agregaremos el siguiente case:
case 'ENOTESSTORE':
console.error('Fallo inicio de almacenamiento porque ',
error.error);
process.exit(1);
break;
Este sera para indicar porque fallo el almacenamiento pero su conducta es similar a los anteriores donde despues del mensaje saldra de la aplicacion, y ahora debemos hacer una modificacion en dos archivos, en ambos es similar, en ambos casos debemos buscar la siguiente linea:
import { NotasMem as notas } from '../app.mjs';
Esta deben eliminarla o comentarla para luego agregar la siguiente:
import { AlmacenNotas as notas } from '../models/notas-almacen.mjs';
Esto importara a nuestra clase encargada de las notas que hemos trabajado ahora, sera tanto para las de memoria o filesystem o las proximas que veremos, seguimos renombrandola a notas para que el resto del codigo no se entere y trabaje como siempre y la importaremos desde el archivo que generamos anteriormente, esta modificacion tanto en index.mjs como en notas.mjs del directorio routes, con esto tenemos nuestra aplicacion al 99% solo nos resta una modificacion mas y es realizar la siguiente modificacion en la seccion de scripts en package.json:
"scripts": {
"start": "cross-env DEBUG=* node ./app.mjs",
"server1": "cross-env DEBUG=* PORT=3001 node ./app.mjs",
"server2": "cross-env DEBUG=* PORT=3002 node ./app.mjs",
"fs-start": "cross-env DEBUG=notas:* PORT=3000 MODELO_NOTAS=fs node ./app.mjs",
"fs-server1": "cross-env MODELO_NOTAS=fs PORT=3001 node ./app.mjs",
"fs-server2": "cross-env MODELO_NOTAS=fs PORT=3002 node ./app.mjs"
},
Como pueden ver agregamos tres scripts nuevos para ejecutar con el nuevo modelo de notas mediante la variable, y seguimos incluyendo el tema del debug, observen que las tres lineas son similares pero lo que varia es el port para ejecutar varias instancias del servidor tal como vimos en este post, pero en esta ocasion vamos a hacer una prueba simple donde ejecutaremos una instancia e ingresaremos un par de notas veamos como trabaja mediante el sigueinte video
En el video pueden ver como reiniciamos el servidor y a diferencia de otros momentos las notas no desaparecen en ningun momento sino que se mantienen, y al final les muestro como no solo creo los archivos sino el directorio que proveemos de manera predeterminada, les dejo el contenido del directorio de las notas:
$ ls -l notas-fs-datos/
total 8
-rw-r--r-- 1 tinchicus tinchicus 69 dic 3 21:17 id0001.json
-rw-r--r-- 1 tinchicus tinchicus 73 dic 3 21:18 id0002.json
$
Observen como uso el id de la nota como nombre del archivo, por esta razon no permitimos que se utilice la barra (/) como parte del identificador, pueden hacer varias pruebas entre ellas pasar la variable NOTAS_FS_DIR para especificar un nuevo directorio donde iran las notas asi como tambien probar varias instancias para almacenar las notas, con esto ya tenemos nuestra primer forma de almacenamiento pero antes de finalizar les dejo un link con todos los archivos del proyecto asi como los codigos trabajados en este post:
En resumen, hoy hemos visto como almacenar informacion mediante el FileSystem, en este caso lo haremos en archivos JSON, hemos visto el paso a paso para permitirlo, despues una fabrica (factory) para poder implementar tanto los archivos como las memorias, para no perder lo trabajado anteriormente, y por ultimo hemos visto como trabaja, espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.50
