lunes, 22 de abril de 2013

Primeras pruebas: node + mongodb + express

El siguiente post es una continuación de una serie de Documentos para las pruebas, donde he creado y manipulado unas colecciones en mongodb para probar node.js con express.

Esto no es un tutorial. Son los datos que voy recopilando y las pruebas que voy haciendo para ver si me merece la pena pasar a este sistema. Así que puede ser que me equivoque y no vaya por el camino correcto (se agradecen los comentarios al respecto... me gustaría saber si voy encaminado)

Por lo que he visto hasta ahora, me gusta y me intriga. Y creo que realmente acabaré pasando a él. Pero prácticamente no he visto nada. Así que al lío.

Comenzaré creando el directorio para la aplicación. En este caso, crearé una carpeta llamada 'bar' en la raíz de mi disco C:

Una vez creada, voy a instalar express en esa carpeta. 

Para ello guardaré en la carpeta de mi aplicación el archivo package.json con el siguiente contenido:

{
"name": "bar",
"description": "Programa para restaurantes",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "3.x"
}
}

Desde una shell, entro a la carpeta de mi aplicación, 'bar', para instalar express:

npm install

También instalaré MongoJS:

npm install mongojs

Hace falta una carpeta 'routes' para guardar los distintos archivos que serán los encargados de manipular los datos que recibe nuestra app. La estructura de carpetas queda de la siguiente forma:

bar
|-node_modules
|     |-.bin
|     |-express
|     |-mongojs
|-routes


La base para el script que hará las funciones de servidor, app.js:

var express = require('express');
var app = express();

app.listen(3000);
console.log('Servidor escuchando en el puerto 3000...');

La app ahora mismo no hace nada, sólo crea un servidor que escuchará por el puerto 3000.

Para el primer ejemplo, crearé una función que devuelva los artículos existentes en la colección 'articulos'. Pero esto lo definiré en un archivo dentro de la carpeta 'routes', para luego simplemente hacer una llamada desde app y que devuelva lo que necesitamos. El archivo se llamará articulos.js:

var databaseUrl = "km"; // "username:password@example.com/km"
var collections = ["articulos"]
var db = require("mongojs").connect(databaseUrl, collections);

He introducido el código necesario para conectar a mi base de datos 'km', usando la colección 'articulos'. Necesito una función que devuelva los documentos de esa colección:

exports.ver_articulos = function(req, res) {
db.articulos.find({}, function(err, articulos) {
 if( err || !articulos) res.send("No hay articulos");
 else res.send(articulos);
});
};

Modificaré el archivo app para que pueda usar la función ver_articulos del archivo articulos.js:

var express = require('express');
articulos = require('./routes/articulos');
var app = express();

app.get('/articulos', articulos.ver_articulos);

app.listen(3000);
console.log('Servidor escuchando en el puerto 3000...');

Es el momento de ejecutar mi app y para ver si me devuelve los artículos.

node app.js

Abro un navegador y accedo a:

localhost:3000/articulos

A la primera. Esta vez no me ha fallado nada. Ni yo me lo creo. En mi navegador aparecen los artículos tal y como le he pedido.


Pues manos a la obra. Necesito una función que me devuelva un artículo por su '_id'. Así que me voy a articulos.js y la creo:

exports.ver_articulo = function(req, res) {
var id = req.params.id;
db.articulos.findOne({'_id': id}, function(err, articulo) {
 if( err || !articulo) res.send("No existe el artículo con la id: " + id);
 else res.send(articulo);
});
};

Añado la siguiente línea en app.js para  que pueda usar la nueva función:

app.get('/articulo/:id', articulos.ver_articulo);

Y lo pruebo... Este código lo único que me devuelve es:

'No existe el articulo con la id: X'

Para probar si estoy fallando en la consulta o si hay que formatear la variable id de alguna manera, la sustituiré por la id de algún artículo:

db.articulos.findOne({'_id': 1}, function(err, articulo)

Esto SÍ me devuelve el artículo. Por lo tanto, id debe ser formateado de alguna forma.

La solución está en pasar la _id como int, ya que se creó como tal:

db.articulos.findOne({_id: parseInt(id)}, function(err, articulo)

Si apunto mi navegador, por ejemplo al artículo con la id 2:

Ahora que he probado un par de funciones, pensaré sobre como debe aparecer la información al usuario. Para las pruebas que quiero realizar me basta con los siguientes pasos:

1 - Muestra las zonas del bar: el camarero selecciona una zona para seleccionar una mesa
2 - Muestra las mesas: Se selecciona una mesa donde añadiremos los pedidos
3 - Familias: Se muestran las distintas familias para seleccionar una y que aparezcan los artículos.
4 - Artículos: Se muestran los artículos para seleccionar uno y que se añada al pedido
5 - Se inserta el pedido en el historial
6 - Se vuelve a la pantalla familias para volver a seleccionar otro artículo 

Voy a crear una archivo 'mesas.js' en 'routes' para meter ahí las funciones relacionadas con la colección 'mesas':

var databaseUrl = "km"; // "username:password@example.com/km"
var collections = ["mesas"]
var db = require("mongojs").connect(databaseUrl, collections);

exports.ver_mesas = function(req, res) {
db.mesas.find({}, function(err, mesas) {
 if( err || !mesas) res.send("No hay mesas");
 else res.send(mesas);
});
};

Por lo pronto le he añadido una función para ver todas las mesas. También  he modificado el archivo 'app.js' para que pueda usar las funciones de 'mesa.js'.

Pero lo más importante es empezar por el punto 1, mostrar las zonas.

¿Cómo sería la consulta para mostrar las zonas existentes en nuestro restaurante? Probare desde la shell de mongo lo siguiente:

> db.mesas.distinct('zona')
[ "Salon", "Terraza" ]


Eso me devuelve lo que necesito. Sabiendo eso, la función 'ver_zonas' quedaría así:

exports.ver_zonas = function(req, res) {
db.mesas.distinct('zona', function(err, zonas) {
 if( err || !zonas) res.send("No hay zonas");
 else res.send(zonas);
});
};



Modifico 'app.js' para que acceda a la nueva función y pruebo:

Una vez que muestra las zonas, suponiendo que he seleccionado una de ellas, deben aparecer las mesas existentes en dicha zona:

exports.mesas_x_zona = function(req, res) {
var _zona = req.params.zona;
db.mesas.find({zona: _zona}, function(err, mesas) {
 if( err || !mesas) res.send("No hay zonas");
 else res.send(mesas);
});
};

De nuevo modifico 'app.js' para incluir la nueva función y pruebo:


Para el ejemplo, selecciono la mesa '1' y debería aparecer una pantalla con las familias de artículos para que pueda seleccionar uno y añadirlo al pedido, tal y como dije en el punto 3. 

Así que añado la función correspondiente al archivo 'articulos.js':

exports.ver_familias = function(req, res) {
db.articulos.distinct('familia', function(err, familias){
if(err || !familias) res.send("No hay familias");
else res.send(familias);
});
};

Pruebo de nuevo para comprobar si hay algún error:



Seguimos en el punto 4. Al seleccionar una familia aparecen los artículos correspondientes:

exports.articulos_x_familia = function(req, res) {
var _familia = req.params.familia;
db.articulos.find({familia: _familia}, function(err, articulos){
if(err || !articulos) res.send("No hay artículos en la familia " + _familia);
else res.send(articulos);
});
};



Si selecciono 'Carnes':


Hasta aquí bien todo, pero ahora viene lo bueno. Se supone que cuando el usuario pulse un artículo, debe añadirse al pedido, es decir, a la colección historial.

Para ello será necesario crear otro archivo, 'historial.js', que se encargue de crear documentos nuevos para añadir los pedidos.

Este archivo debería contener dos funciones:

- nuevo_historial, crea un nuevo documento en historial pero dependiendo de la _id del último documento.
- inserta_pedido, añade la clave 'descrip' con todos los datos del pedido en el documento creado.

Para nuevo_historial necesito saber cual es la id del último documento. Pruebo desde la shell:

> db.historial.find().sort({_id:-1}).limit(1).pretty();
{
        "_id" : 2,
        "descrip" : [
                {
                        "linea" : "1",
                        "articulo" : "Fanta Naranja",
                        "precio" : 1,
                        "visores" : [
                                "barra"
                        ]
                }
        ],
        "fecha" : ISODate("2013-04-22T16:11:15.500Z"),
        "mesa" : "4"
}


Me devuelve el documento completo. Yo sólo necesito la '_id':

> db.historial.find({},{_id:1}).sort({_id:-1}).limit(1);
{ "_id" : 2 }

Para comprobar (como siempre)  antes de sumar, compruebo que el resultado sea correcto, que me devuelva la última id de documento:

exports.nuevo_historial = function(req, res){
db.historial.find({},{_id:1}).sort({_id:-1}).limit((1), function(err, id){
if(err || !id) res.send("Error o no hay ningun documento");
else res.send(id);
});
};


Sabiendo que el resultado es el deseado, debería asignarlo a una variable y sumarle uno, para prepararlo como la nueva id del documento a crear. La función 'nuevo_historial' quedaría más o menos así:


exports.nuevo_historial = function(req, res){
var x = 0;
db.historial.find({},{_id:1}).sort({_id:-1}).limit((1), function(err, id){
if(err || !id) res.send("Error o no hay ningun documento");
else {
x = id[0]._id + 1;
res.send("Próxima id: " + x);
}
});
};



Hasta donde llego, es la única forma que he encontrado para recoger el valor que necesito. Ya he dicho que soy un novato con esto. Desconozco si hay una forma mejor o más adecuada (por favor, agregar a comentarios).

Una vez que tengo mi próximo indice preparado, el segundo campo que necesito es la fecha. Modifico 'nuevo_historial':


y = new Date();
res.send("Próxima id: " + x + "<br> Fecha: " + y);

El último dato que hace falta para crear el nuevo historial es el nombre de la mesa, el  cual debería venir en el POST desde alguna página que lo envíe.

Para probar a recoger valores desde un POST, voy a crear un pequeño formulario que envíe el número de la mesa elegida, además del usuario que está utilizando el sistema:


<html>
<head>
   <title>Seleccione la mesa</title>
</head>
<body>
<form method="post" action="http://localhost:3000/nuevo_historial">
    Usuario:<input type="text" name="usuario" value="luismips"><br>
    Mesa:<input type="text" name="mesa" value="2"><br>
    <input type="submit" value="Enviar">
</form>
</body>
</html>



En la app, debo configurar express para que use la función bodyParser():


app.configure(function(){
  app.use(express.bodyParser());
});


Modifico la llamada a la función 'nuevo_historial' desde app.js. La cambiaré de get a post:

app.post('/nuevo_historial', historial.nuevo_historial);

La función 'nuevo_historial' también será modificada. Se recogerá el valor del POST y se mostrará en el navegador:


exports.nuevo_historial = function(req, res){
var x = 0;
var usuario = req.body.usuario;
var mesa = req.body.mesa;

db.historial.find({},{_id:1}).sort({_id:-1}).limit((1), function(err, id){
if(err || !id) res.send("Error o no hay ningun documento");
else {
x = id[0]._id + 1;
y = new Date();
res.send("Próxima id: " + x + "<br> Fecha: " + y + "<br>Mesa: " + mesa + "<br>Usuario: " + usuario);
}
});
};


Es el momento de probar si todo envía y recibe correctamente. Abro mi pequeño formulario en el navegador:


Pulso Enviar:










Finalmente tengo los datos que necesito para crear el nuevo documento que contendrá el pedido de la mesa. El dato 'Usuario' no me sirve para las pruebas. Lo he incluido simplemente por probar.

Lo siguiente será crear el nuevo documento e insertar el pedido al pulsar (enviar en este caso) un artículo, para posteriormente, simular el visor de barra o cocina y ver los pedidos que van entrando en tiempo real (que es una de las cosas por las que quiero usar node... ver en tiempo real).

Pero eso en el siguiente post (2ª parte). Este se me ha hecho muy largo, así que continuaré en el próximo.

Hasta entonces, saludos,

Luismips


















No hay comentarios:

Publicar un comentario