sails

Imagen

Guía de aprendizaje

v.0.11 (2ª Edición)

(Sails.js) Guía Aprendizaje

Por:  Josué J. Rodríguez

Esta guía no pretende enseñarte a programar con Node.js ni con JavaScript. Esta guía  te ayudará a construir Aplicaciones Web Personalizadas. Sails es uno de los frameworks MVC más populares de Node.js. La construcción de una aplicación con Sails, será cuestión de semanas en lugar de meses.

___________________________________________________________

1ª Edición Publicado el 12 de Noviembre 2014 (v.0.10)

2ª Edición Publicado el 3 de Febrero 2015 (v.0.11)

Tabla de Contenidos 

 

  1. Sails.js
  1. Visión
  2. Instalación node
  3. Instalación sails.js
  1. Assets
  1. Visión del conjunto
  2. Tareas por defecto
  3. Desactivando tareas  automatizadas
  4. Tareas automatizadas
  1. Configuración
  1. Usando el archivo .sailsrc
  1. Globales
  2. Rutas
  1. Rutas personalizadas
  2. URL’s
  1. Controladores
  2. Respuestas Personalizadas
  3. Modelos y ORM (Object-Relational mapping)
  1. Asociaciones
  2. Dominio
  3. Many to Many (muchos a muchos)
  4. One to Many (uno a muchos)
  5. One to One (uno a uno)
  6. Asociación única
  7. Accediendo a tablas relacionadas
  8. Atributos
  9. Ciclo de vida Callbacks
  10. Modelos
  11. Lenguaje de consulta
  12. Validaciones
  13. Configuración de modelos
  1. Vistas
  1. Plantillas
  2. Locales
  3. Parciales
  4. Manejadores de vistas
  1. Servicios
  2. Seguridad
  1. Cross-Origin Resource Sharing (CORS) 
  2. Cross-Site Request Forgery (CSRF)
  3. Cickjacking
  4. Política de seguridad
  5. DDos
  6. P3P
  7. Robo de sockets
  8. Seguridad de transporte HTTP stricta (HSTS)
  9. XSS
  1. Internacionalización
  2. Políticas
  3. Middleware
  4. Cargar / Subir archivos al servidor
  5. Logs
  1. sails.log()
  1. Testeo
  2. Desplegando la aplicación                

Características Principales

Sails es un framework ligero. El conjunto de pequeños módulos trabajan juntos para proporcionar simplicidad, facilidad de mantenimiento y robustez en nuestras aplicaciones web.

icon_js_large.png

100% JavaScript

Al igual que otros frameworks MVC, Sails se centra en el desarrollador, intentando hacerle la vida más fácil manteniendo la filosofía de convención antes que configuración.

La construcción de una aplicación en Sails está escrita completamente en JavaScript, el lenguaje que estamos acostumbrados a escribir en la parte del frontend. De este modo el desarrollador es capaz de escribir código consistente y productivo.

icon_database_large.png

Cualquier Base de Datos

Sails trae consigo un ORM, llamado Waterline, proporciona una capa de acceso a los datos que simplemente funciona, no importa la base de datos que esté utilizando.

Aparte de la gran cantidad de proyectos que existen en la comunidad, existen adaptadores con soporte oficial para MySQL, MongoDB, PostgreSQL, Redis y Disco Local.

icon_associations_large.png

Asociaciones con gran potencial

Sails ofrece una nueva forma de relacionar modelos, con el propósito de realizar un modelo de datos más práctico. A parte de lleva a cabo las relaciones más usuales (many-to-many, one-to-many, ...), también se puede asignar varias asociaciones por modelo. (Por ejemplo un pastel podría tener dos colecciones: “gente” y “comedores”). O mejor aún puedes asignar diferentes modelos a diferentes bases de datos.

icon_api_large.png

Generar automáticamente API Rest

Sails cuenta con blueprints para ayudarnos a construir un backend sin escribir apenas código.

Tan fácil como ejecutar sails generate api dentista, y poder realizar búsquedas, paginar, filtrar, crear, eliminar, actualizar y relacionar. Todo esto gracias a las acciones de blueprint.

icon_websocket_large.png

Soporte para WebSockets sin código adicional

Sails interpreta automáticamente los mensajes socket entrantes, son compatibles con las rutas explícitas de nuestra aplicación.

Tanto la normalización de parámetros, sesión y la transmisión de interfaz serán atendidos por sails.

icon_security_large.png

Políticas seguras, reusables y declarativas

Sails ofrece seguridad básica y control de acceso basado en roles, para ello haremos uso de las políticas, funciones reusables que se ejecutan antes que los controladores y acciones.

Al hacer uso de las políticas simplificamos nuestra lógica de negocio y reducimos consideradamente la cantidad total de código. Las políticas también pueden ser un middleware de Express, esto significa que podemos conectar módulos a través de npm como passport.

icon_frontend_large.png

Front-end agnóstico

Sails es compatible con cualquier estrategia que queramos implementar en el front-end, Angular, Backbone, iOS/ObjC, Android/Java, Windows Phone o cualquier otro que todavía no se haya inventado.

Además es fácil servir una API para ser consumido por otro servicio web o comunidad de desarrolladores.

icon_asset_large.png

Assets flexibles

Si estás construyendo una aplicación de navegador, estás de suerte. Sails junto con Grunt hacen que todo el flujo de trabajo activo para el desarrollador sea totalmente personalizable y con soporte para la mayoría de los módulos de Grunt.

Cuando tu aplicación esté lista para llevarla a producción, todos los assets serán comprimidos y minificados automáticamente.

icon_foundation_large.png

Fundación sólida

Sails está construido sobre Node.js, una tecnología popular, rápida en el lado del servidor, que permite al desarrollador llevar a cabo rápidamente aplicaciones en la red con JavaScript.

Sails usa Express para gestionar las peticiones HTTP y socket.io para gestionar WebSockets.

1. sails.js

Visión

Sails es un framework web. La mayoría de las veces, cuando hacemos referencia a la “web”, nos viene a la mente conceptos como HTML5 o CSS3. Cuando oímos o leemos la palabra framework, lo primero en que pensamos es en Backbone, Angular o jQuery.

Sails no es de ese tipo de frameworks. sails trabaja juntamente con Angular o Backbone, pero nunca debes usar sails como sustitución de estas librerías.

Por otro lado, cuando hablamos de “frameworks web”, también nos referimos a la parte del back-end, pensamos en conceptos como REST, HTTP o WebSockets y tecnologías como Java, Ruby o Node.js.

 

Un framework para el back-end (como Sails) nos ayuda a construir APIs, servir archivos HTML y a manejar cientos de miles de usuarios conectados simultáneamente. Para llevar a cabo todo esto, sails, sí es el rey!  

 

Convención antes que configuración

Sails cumple con los muchos de los objetivos que otros entornos de desarrollo de aplicaciones web, usando las mismas metodologías MVC (Modelo, Vista, Controlador).  

Cualquiera que haya  trabajado antes con Zend, Laravel, CodeIgniter, Cake, Django, ASP .NET o Rails, se encontrará con un entorno bastante familiar. También le resultará fácil de entender la anatomía y la distribución del código de un proyecto en sails.

 

 

Instalación de node.js 

Aunque quizás sea obvio, hay que decir que sails es un framework de Node.js. Así que para empezar a utilizar sails, primero debemos tener instalado Node.js. A continuación, mostramos cómo efectuar la instalación en Windows, OSX y Linux.

[ OSX ]

Puedes optar por descargar el paquete desde la página oficial: http://www.nodejs.orj/download/ 

También a través de homebrew:  brew install node

O quizás quieras hacerlo por medio de macports: port install nodejs

 

Instalación node.js en Linux

[ Ubuntu, Mint ]

$ sudo apt-get install python-software-properties python g++ make
$ sudo add-apt-repository ppa:chris-lea/node.js
$ sudo apt-get update
$ sudo apt-get install nodejs

Los usuarios que tengan la versión Quantal (12.10), necesitarán instalar software-properties-common

$ sudo apt-get install software-properties-common

[ Fedora ]

Node.js y npm están disponibles en el repositorio desde la versión 18 y posteriores

$ sudo yum install npm

[ RHEL / CentOS / Scientific Linux 6 ]

$ su -c 'yum --enablerepo=epel-testing install npm'

[ Arch Linux ]

$ sudo pacman -S nodejs

[ Gentoo ]

# emerge -aqv --autounmask-write nodejs
# etc-update
# emerge -aqv nodejs

[ OpenSUSE & SLE ]

sudo zypper ar http://download.opensuse.org/repositories/devel:/languages:/nodejs/openSUSE_12.1/ NodeJSBuildService
sudo zypper in nodejs nodejs-devel

[ Windows ]

Puedes optar por descargar el paquete desde la página oficial: http://www.nodejs.orj/download/

O bien con chocolatey

cinst nodejs.install

Instalación de Sails

Bien, ahora que tenemos Node.js instalado, seguiremos con la instalación de sails, la instalación es mucho más sencilla, y la realizaremos de forma global con el parámetro -g, de esta forma podemos crear proyectos con sails sin tener que descargar el código fuente cada vez que iniciemos un nuevo proyecto.

$ sudo npm -g install sails

Así de sencillo, ya tenemos sails instalado en nuestro sistema.

Otro requisito para utilizar sails aunque opcional, es tener instalado en nuestro sistema el gestor de tareas Grunt.

Grunt, es herramienta muy potente que nos ahorra mucho tiempo cuando estamos desarrollando. Se encarga de realizar tareas que son repetitivas, como minificar código, compilación, pruebas unitarias, etc. en definitiva hace nuestro trabajo más fácil.

Grunt cuenta con un gran número de plugins para realizar las tareas rutinarias de nuestra aplicación. Podemos automatizar cualquier cosa con muy poco esfuerzo.

Más información en www.gruntjs.com

La instalación de Grunt es como la de sails, utilizaremos el parámetro -g para que la instalación se efectúe de forma global.

$ sudo npm -g install grunt grunt-cli

 

Para identificar los archivos y carpetas que mencionaremos a lo largo de esta guía, es necesario o más bien recomendable, crear un proyecto nuevo con sails. Así que vamos a ello.

Creando un nuevo proyecto Sails

Para crear un nuevo proyecto Sails, desde la consola nos dirigimos hasta el directorio donde queramos  guardar nuestro proyecto. Una vez dentro del directorio introducimos lo siguiente:

$ sails new el_nombre_que_le_queramos_dar

Automáticamente se creará una carpeta con el nombre que le hayamos dado al proyecto, accedemos a dicha carpeta mediante el comando:

$ cd el_nombre_que_le_queramos_dar

Y a continuación iniciamos el servidor web con el siguiente comando:

$ sails lift

Por consola nos saldrá algo como esto:

Selección_001.png

Nos muestra como el servidor es levantado en un entorno de desarrollo y es escuchado por el puerto 1337. Pues bien comprobemos los resultados, en nuestro navegador web preferido, introducimos lo siguiente en la barra de navegación, http://localhost:1337/, y esto es lo que veremos:

Selección_002.png

Comprobado que nuestro servidor funciona, vamos a pasar a materia y explicar como trabajar con sails

2. Assets

Visión de conjunto

“Assets” ¿Que son? nos referimos a los archivos estáticos ( js, css, imágenes, etc ) que son accesibles para cualquiera. En sails, estos archivos están ubicados en el directorio assets/ de nuestro proyecto, donde estos son procesados en un directorio temporal ( ./temp/public ) en el momento que el servidor es iniciado. El contenido de ./temp/public es el equivalente a la carpeta “public” que usamos en  express, o la carpeta “www” que tienen otros servidores web como Apache. Este paso permite a Sails preparar o pre-compilar estos archivos  para su uso en el frontend (lado del cliente).

Middleware Estáticos 

“Middleware: es un software que asiste a una aplicación para interactuar o comunicarse con otras aplicaciones, o paquetes de programas

Sails usa el middleware estático express para servir los assets. Podemos configurar este middleware (más adelante veremos cómo configurar middlewares de terceros o personalizados por nosotros) en /config/http.js.

index.html

Como la mayoría de los servidores web, Sails hace honor a index.html. Por ejemplo si creas assets/foo.html en un nuevo proyecto, este será accesible desde http://localhost:1337/foo.html, pero si creaste  assets/foo/index.html, entonces estará disponible desde http://localhost:1337/foo/index.html y también desde http://localhost:1337/foo.

Prioridades

Esto es importante a tener en cuenta, ya que los middleware estáticos corren después del enrutador de sails

(más adelante explicamos qué es esto de del enrutador). Ahora por el momento Imagina que tenemos definida una ruta personalizada, y en nuestro directorio “assets”  tenemos un archivo con la misma ruta, en el momento que hay una petición a esta ruta, es la ruta personalizada la que intercepta la petición  antes que el middleware estático. Por ejemplo, creamos assets/index.html, sin definir ninguna ruta en el archivo config/routes.js, esta será servida en tu página de inicio, pero si definimos una ruta personalizada en el archivo config/routes.js  como podria ser “ / ” : “FooController.bar”, entonces a esta se le dará prioridad ya que las dos rutas apuntan al mismo sitio.

Recuerda: sails siempre dará preferencia a las rutas personalizadas antes que a un archivo estático, aunque apunten a la misma ruta.

 

Tareas Predeterminadas

Visión de conjunto

El paquete Assets incluido en sails establece un conjunto de tareas configuradas con Grunt, diseñadas por defecto para crear un proyecto coherente y productivo. Todo el flujo de trabajo en la parte del frontend es completamente personalizable, aunque sails proporciona por defecto un conjunto de tareas predefinidas.

Con sails le resultará fácil configurar nuevas tareas que no vienen por defecto y que quizás necesite.

Aquí algunas de las tareas que tenemos por defecto al crear un proyecto en Sails que nos pueden ayudar:

Tareas de Comportamiento por Defecto en Grunt

A continuación se presentan las tareas de Grunt que están incluidas en un proyecto en sails, así como una pequeña descripción del trabajo que llevan a cabo..

clean

Esta tarea está configurada para limpiar todo el contenido de la carpeta .tmp/public/ de nuestro proyecto en Sails.

coffee

Esta tarea está configurada para compilar archivos cooffeeScript desde assets/js, y ubicarlos dentro del directorio  .tmp/public/js

concat

Esta tarea está configurada para concatenar los archivos CSS y JavaScript, y los guarda en .tmp/public/concat/

copy

dev task config Copia todos los directorios y archivos, excepto los archivos coffeeScript y LESS, dentro del directorio .tmp/public

build task config Copia todos los directorios y archivos desde .tmp/public al directorio www

cssmin

Minimiza los archivos CSS y los ubica en el directorio .tmp/public/min

less

Compila LESS dentro de archivos  CSS. Solamente los que están en assets/styles/importer.less serán compilados.  Esto permite el control del mismo, importando dependencias, variables, resets, etc. antes que otras hojas de estilo.

sails-linker

Automáticamente inyecta archivos javascript y CSS. añadiendo las etiquetas <script> y <link>. La inyección de estos archivos se hará en el archivo que contenga las etiquetas <!--SCRIPTS--> <!--SCRIPTS END--> y <!--STYLES--> <!--STYLES END-->

Estas etiquetas están incluidas por defecto en el archivo views/layout.ejs

sync

Esta tarea es muy similar a grunt-copy, con la diferencia que esta tarea copia solamente los archivos que han sufrido algún cambio recientemente. Sincroniza los archivos desde la carpeta assets/ a la carpeta .tmp/public/ , sobrescribiendo cualquiera de ellos.

uglify

Minifica los archivos JavaScript

watch

Ejecuta tareas predefinidas cuando un archivo se agrega, cambia o elimina. Vigila estos cambios en la carpeta assets/ cuando surge algún cambio ejecuta la tarea less, jst.  Esto permite ver los cambios reflejados en nuestra aplicación sin tener que reiniciar el servidor Sails.

Desactivando Grunt

Tanto si el proyecto a realizar es una API Rest, o una aplicación donde el servidor de nuestro proyecto no ha de incluir ficheros estáticos, es recomendable que desactive la integración de Grunt en nuestro proyecto sails, simplemente borraremos el archivo Gruntfile.js y la carpeta tasks/. También puede desactivarlo mediante la propiedad grunt, estableciendo dicha propiedad a false en el archivo .sailsrc :

{
   
"hooks": {
       
"grunt": false
   }
}

También es posible añadir tareas para personalizar SASS, AngularJS, templates, etc. Basta con sustituir la tarea relevante de Grunt o añadir una nueva tarea.

3. Configuración

Visión del conjunto

Como hemos mencionado anteriormente sails mantiene su filosofía de “convención antes que configuración”, esto es importante a la hora de cómo personalizamos los valores predeterminados. Cada convención en sails, está acompañada de un set de opciones de configuración que podemos ajustar o sobre escribir según nuestras necesidades. Esta documentación incluye una completa referencia de las opciones configurables que están disponibles en Sails.

Disponemos de dos formas de configuración, a través de las variables de entorno o mediante argumentos desde la línea de comandos, usando los archivos de configuración que se encuentran en el directorio config/ de nuestro proyecto. 

Configuración Estándar (config/*)

Un número de archivos de configuración están incluidos por defecto en un proyecto sails cuando lo instalamos. Estos archivos de configuración poseen comentarios los cuales están diseñados para proporcionarnos ayuda.

En la mayoría de los casos, las claves de nivel superior en el objeto sails.config corresponden a un archivo de configuración en particular de nuestra aplicación (por ejemplo: sails.config.views). Sin embargo los valores de configuración se pueden organizar como queramos a través de los archivos en tu directorio config/. Lo importante es el nombre de la key (clave o llave) no confundir con el nombre del archivo.

Por ejemplo, digamos que agrega un archivo nuevo en el directorio config/ con el nombre config/foo.js :

// config/foo.js
//  El objeto se fusiona  con  `sails.config.blueprints`:
module.exports.blueprints
= {
 shortcuts:
false
};

Veremos más acerca de esto en la “Guía de Desarrollo con Sails”, donde se hará referencia a las configuraciones individuales, y como residen estos archivos de forma predeterminada.

Archivos Específicos de Entorno (config/env*)

Los ajustes específicos en los archivos de configuraciön generalmente están disponibles en cualquier entorno de trabajo (Desarrollo, Producción, Test, etc.). Si lo que quiere es tener su configuración solamente en ciertos entornos, entonces debe utilizar los archivos de las carpetas especificadas para ello, config/env/developent.js o config/env/production.js

Accediendo a los valores de configuración de nuestro proyecto

El objeto config está disponible en una instancia de aplicación de sails. Por defecto, este objeto está expuesto en un entorno de ámbito global, mientras sails esté corriendo, esto quiere decir que podemos acceder a él desde cualquier parte de nuestra aplicación. Veamos un ejemplo.

// Este ejemplo comprueba si Sails está corriendo en modo producción y si la protección    CSRF está activada.
// Generamos error si no se cumple la condición.
if (sails.config.environment === 'production' && !sails.config.csrf) {
 
throw new Error('STOP IMMEDIATELY ! CSRF should always be enabled in a production deployment!');
}

Configuración Personalizada

Sails reconoce varios entornos, nombres de espacio en diferentes claves de nivel superior (ej. sails.config.sockets y sails.config.blueprints). Sin embargo también puede utilizar sails.config para su propia configuración personalizada (ej. sails.config.cualquierProveedorApi.secret).

Ejemplo

// config/linkedin.js
module.exports.linkedin
= {
 apiKey:
'...',
 apiSecret:
'...'
};

//  En su controlador, servicio, modelo u otra
// ...
var apiKey = sails.config.linkedin.apiKey;
var apiSecret = sails.config.linkedin.apiSecret;
// ...

Configuración desde línea de comandos

Cuando se trata la configuración, la mayor parte del tiempo estará centrado en la configuración de su aplicación (el puerto, las conexiones a la base de datos, etc.). Sin embargo puede ser útil personalizar Sails CLI, de esta manera simplificamos el flujo de trabajo, reducimos esas tediosas tareas repetitivas, la construcción personalizada de automatización, etc. Afortunadamente sails, desde su versión 0.10, añade una poderosa herramienta que nos ayudará precisamente a esto.

El archivo .sailsrc es diferente a cualquier otra fuente de configuración que encontremos en sails. Podemos usar este archivo para configurar todo el sistema, para un grupo de directorios o solo para un directorio determinado. La principal razón para hacer esto es personalizar los generadores que usa sails cuando se genera un nuevo proyecto o cuando levantamos nuestro servidor con $ sails lift. También es útil para personalizar o instalar nuestros propios generadores personalizados.

En el último capítulo (Extendiendo Sails) de este libro, veremos con más detalle cómo llevar a cabo todo esto.

Cuando generemos un nuevo proyecto, sails buscará el archivo .sailsrc más cercano en directorio actual o en su defecto en los directorios antecesores hasta llegar a la raíz.

Se puede utilizar con total seguridad este archivo para configurar nuestros ajustes, no es recomendable alojar este archivo en la nube.

El archivo config/local.js

El archivo config/local.js, nos resulta útil para la configurar una aplicación sails en un entorno local (Por ejemplo: PC, Portátil, etc). La configuración de este archivo tiene prioridad o preferencia sobre todos los demás archivos de configuración, excepto el archivo .sailsrc, que ya lo vimos anteriormente.

Dado que está destinado sólo para su uso local, no debería ser puesto bajo un control de versiones (GIT, Subversion, etc).

Cuando su aplicación esté lista para desplegarse en un entorno de producción, puede usar este archivo para configurar las opciones que mejor se ajusten en el servidor donde se desplegará la aplicación. Sin embargo, es preferible configurar los archivos de entorno para la implementación en un servidor de producción o un servidor de desarrollo

NOTA:

Este archivo es ignorado por el control de versiones, así que si está usando git como solución para gestionar la versiones de su aplicación, ha de tener en cuenta que este archivo no será controlado, eso significa que puede especificar la configuración de su máquina sin comprometer datos como la conexión a la base de datos, contraseñas, etc.

Además, se evita que otros miembros que estén trabajando en el mismo proyecto, hagan cambios en la configuración local.

Usando el archivo .sailsrc

Como hemos visto anteriormente, además de los métodos que tenemos para configurar nuestra aplicación, disponemos del archivo .sailsrc donde podemos especificar la configuración para uno o más aplicaciones. A través de este archivo podemos aplicar todas las opciones de configuración  a todas las aplicaciones sails que tengamos en nuestro ordenador.

Cuando iniciamos o levantamos el servidor web, busca archivos .sailsrc en la carpeta actual y en la carpeta home (~/.sailsrc).

En realidad, sails busca el archivo .sailsrc en cualquier directorio, siguiendo la convención rc, aunque el mejor lugar para poner el archivo .sailsrc, como bien hemos comentado es en la carpeta actual o en el home.

4. Globales (Globals)

Visión de Conjunto

Para nuestra comodidad como desarrolladores sails pone a nuestra disposición una serie de variables globales. Por defecto, los modelos, servicios y el objeto sails están disponibles en todo el ámbito global de nuestra aplicación. Esto significa que podemos hacer referencia a ellos por su nombre en cualquier parte del código, siempre y cuando el servidor sails esté corriendo.

Nada que esté en el núcleo de sails se basa en estas variables globales. Las variables globales pueden estar desactivadas, las podemos modificar y configurar desde el archivo config.globals.js  

El objeto App (sails)

En la mayoría de los casos, usted querrá mantener el objeto global sails accesible, con el propósito de que el código de la aplicación sea mucho más limpio. Sin embargo, si deseas o necesitas desactivar todas las variables globales, incluyendo sails, puedes obtener acceso a través del objeto (req).

Modelos y Servicios

Los modelos y servicios de tu aplicación están expuestas como variables globales utilizando su globalId. Por ejemplo, el modelo definido en el archivo api/models/Foo.js está accesible globalmente como Foo y el servicio definido en el archivo api/services/Baz.js está accesible globalmente como Baz.

Async (async) y Lodash ( _ )

Sails dispone de una instancia de lodash como _ (guion bajo), y una instancia de async como async. Sails proporciona estas utilidades por defecto, lo que quiere decir que no es necesario instalarlas por medio de npm install. Al igual que cualquier otra variable global, se pueden desactivar.

Deshabilitando Globales

Sails determina qué globales están disponibles para su uso examinando sails.config.globals, que convencionalmente se configura en config/globals.js.

Para deshabilitar todas las variables globales, has de establecer el parámetro con valor false :

// config/globals.js
module.exports.globals
= false;

Si lo que desea es deshabilitar solo una o algunas de las variables globales, ha de establecer a false las que quiera deshabilitar :

// config/globals.js
module.exports.globals
= {
 _:
false,
 async:
false,
 models:
false,
 services:
false
};

Ten en cuenta que ninguna de las variables globales, incluyendo sails, serán accesibles

hasta que sails se haya cargado completamente. En otras palabras, no será capaz de

utilizar sails.models.user o User, fuera de la función, ya que sails no se ha cargado

5. Rutas (Routes)

Visión de Conjunto

La característica más básica de cualquier aplicación web, es la habilidad de interpretar una petición enviada a una URL, y emitir una respuesta. Para ello la aplicación tiene que ser capaz de distinguir una URL de otra.

Sails provee un mecanismo de ruteo que mapea URL’s a controladores y vistas. Las rutas son reglas o normas que le dicen a sails qué hacer cuando reciben una petición.

Existen dos tipos principales de rutas personalizadas o explícitas y las automáticas o implícitas.

Rutas personalizadas (explícitas)

Sails nos permite diseñar la URL’s de nuestra aplicación como queramos, sin restricciones a nivel de framework.

Cada proyecto en sails cuento con el archivo config/routes.js, un simple módulo de node.js que exporta las rutas personalizadas o explícitas. En el siguiente ejemplo se definen seis rutas, donde unas hacen referencia a una acción de un controlador, mientras que otras rutas hacen referencia directamente a una vista.

// config/routes.js

module.exports.routes = {

    ‘get /signup’: {view: ‘coversion/signup’ },

    ‘post /signup’: ‘AuthController.processSignup’,

    ‘get /login’: {view: ‘portal/login’ },

    ‘post /login’: ‘AuthController.processLogin’,

    ‘/logout’: ‘AuthController.logout’,

    ‘get /me’: ‘UserController.profile

}

Cada ruta consiste en una dirección (‘get /me’) y un destino (‘UserController.profile’). A la dirección opcionalmente le podemos especificar el método HTTP. El destino lo podemos definir de varias maneras diferentes (como veremos a continuación), aunque las dos sintaxis que hemos visto en el ejemplo son las más comunes.

Cuando sails recibe una petición, este chekea las direcciones de las rutas personalizadas hasta encontrar una coincidencia. Si encuentra una coincidencia, entonces la petición pasa al destino.

Por ejemplo, si pudiésemos hablar directamente con sails, sería esto lo que le ordenaremos:

“Estate atento, cuando recibas una petición con el método HTTP GET que apunte a la dirección http://localhost/mi_dominio.com/me, ejecuta la acción profile del controlador UserController”.

Notas

El hecho de que una petición coincida con una dirección de ruta, no significa que se pasará directamente al controlador, algunas peticiones HTTP pasarán primero a través de middlewares. Y si la ruta apunta a una acción de controlador, la petición deberá pasar a través de alguna política configurada previamente.

Rutas Automáticas

Además de las rutas personalizadas, sails enlaza muchas rutas de forma automática. Si una ruta no coincide con la ruta personalizada, puede coincidir con una ruta automática y generar una respuesta.

Los principales tipos de rutas automáticas en sails son:

Protocolos soportados

El router de sails sabe como manejar las peticiones entrantes HTTP y mensajes enviados vía WebSockets. Esto se logra mediante la escucha de los mensajes enviados por Socket.io, estos mensajes son enviados a los controladores de eventos en un formato simple, llamado JWR (JSON-WebSocket Requies/Response).

Notas

Los usuarios avanzados pueden optar por eludir el router y enviar mensajes directamente al servidor mediante Socket.io. Puede enlazar los eventos socket directamente en la aplicación con la función onConnect (localizado en el archivo config/socket.js).

Rutas personalizadas

Como veremos a continuación, sails permite explícitamente definir rutas de varias formas diferentes en nuestro archivo config/routes.js. Cada configuración de ruta contiene una dirección URL y un destino a alcanzar. Por ejemplo:

'GET /foo/bar': 'FooController.bar'
^^dirección^^   ^^^^destino^^^^^

Dirección de Ruta

La dirección de ruta indica a qué URL debe responder con el fin de aplicar un controlador y las opciones definidas en el destino. Una ruta puede contener opcionalmente un verbo HTTP (GET, POST, … )

  'POST       /foo/bar'
^verbo^  ^direccion^^

Sí no se especifica un verbo, el destino se aplicará a cualquier petición que coincida con la ruta, independientemente del método HTTP utilizado. Ten en cuenta la barra  ” / ” que indicamos al principio de la dirección, todas las rutas han de comenzar con “ / ” para que trabajen correctamente.

Además de usar una ruta estática como /foo/bar, también podemos utilizar el asterisco (*) 

Esta dirección coincidirá con todas las rutas

'/*'

Esta otra, coincidirá con todas las rutas que empiezan por  /user/foo

'/user/foo/*'

Se pueden capturar partes de la dirección que coinciden con parámetros sustituyendo  *  por los nombres de parámetros

'/user/foo/:name/bar/:age'

Esta también coincidirá con las mismas URLs

'/user/foo/*/bar/*'

Expresiones regulares en las direcciones

Además de utilizar el  *  como ya hemos visto, también podemos utilizar expresiones regulares para definir direcciones URL. La sintaxis para definir direcciones con expresiones regulares es:

r|<expresion regular>|<coma delimitadora listado de nombres de parámetros>

r|^/\\d+/(\\w+)/(\\w+)$|foo,bar": "MessageController.myaction

Una petición URL como esta /123/abc/def ejecutaría la acción “myaction” del controlador “MessageController” y tanto abc como def serían los valores de req.param(‘foo’) y req.param(‘bar’)

NOTA: “La doble barra invertida en  \\d  y  \\w son necesarias para que la expresión regular funcione correctamente.

El orden de rutas importa

Tanto si usamos * , como si usamos expresiones regulares en nuestras rutas, debemos tener muy en cuenta en como las ordenamos, cuáles ponemos antes y cuáles después. Las URLs se comparan con las direcciones de ruta de arriba a abajo, en el momento que encuentra coincidencia, para y no sigue buscando otras coincidencias.

Destinos de ruta

Podríamos decir que las rutas se conforman de dos porciones, la primera es donde especificamos la dirección que debe coincidir con la URL solicitada. La segunda porción es el destino que especifica a sails que debe hacer, a donde ha de ir a parar en el momento que encuentre coincidencia la dirección URL con la petición. Disponemos de varias formas de definir el destino. En algún momento es posible que necesites que una ruta tenga varios destinos colocados en un array, pero en la mayoría de los casos cada ruta tendrá su dirección junto con un solo destino. A continuación vemos los diferentes tipos que hay para definir los destinos, seguido de como se pueden aplicar.

Destino … Controlador / acción

'GET /foo/go': 'FooController.myGoAction',
'GET /foo/go': 'Foo.myGoAction',
'GET /foo/go': {controller: "Foo", action: "myGoAction"},
'GET /foo/go': {controller: "FooController", action:"myGoAction"},

Cada una de las rutas GET /foo/go accede a la acción myGoAction del controlador api/controllers/FooController.js. Cada vez que se realice una petición a GET /foo/go se ejecutará el código de la acción. Si tu controlador o acción no existiera, se producirá un error y no haría caso de la ruta.

Tanto el nombre del controlador como el de la acción, son case-insesitive (es decir, distingue mayúsculas de  minúsculas).

Ten en cuenta de que blueprints añade las siguientes acciones por defecto  a los controladores ( find, create, update y delete), todos ellos están disponibles para el enrutado.

'GET /foo/go: ‘UserController.find’'

Se asume que se ha de tener un archivo controlador api/controllers/UserController.js y un archivo modelo en api/models/User.js.

Destino … una vista

Otro destino bastante común es asociarla a una vista. La forma de definirla es muy sencilla: Es la ruta relativa hacia el archivo que contiene la vista dentro de la carpeta views, pero sin la extensión.

'GET /home': {view: 'home/index'}

Esta ruta GET /home apunta a la vista guardada en views/home/index.ejs (asumiendo que como motor de templates estés usando EJS).

Destino … Blueprint

En algunos casos es posible que quieras asignar una dirección a una acción blueprint de sails. Por ejemplo, si tienes un controlador llamado UserController y un modelo llamado User, sails asigna automáticamente una de las acciones blueprint  “find” a esta dirección (GET /user) retornando un listado de usuarios. Si deseas asignar una dirección diferente para tal acción de blueprint, la sintaxis a llevar a cabo sería algo parecido a esto:

'GET /findAllUsers': {model: 'user', blueprint: 'find'},
'GET
/user/findAll': {blueprint: 'find'}

En la primera ruta establecemos tanto el modelo (model) como la acción blueprint, mientras que en la segunda ruta solamente establecemos blueprint. Esto es porque sails examinará en la dirección asumiendo que el modelo es User. Aunque podemos sobreescribir esto explícitamente estableciendo un modelo.

'GET /user/findAll': {blueprint: 'find', model: 'pet'}

Aunque rara vez o nunca vayas a realizar esto (ya que harías muy confusa tu aplicación), es bueno saber todas las características de que dispone sails a la hora de construir aplicaciones. Nunca se sabe, si en algún caso en concreto tendrás que recurrir a algo parecido.

Si especificamos un modelo que no existe, Sails emitirá un error y no hará caso de la ruta.

Destino … Redireccionando

Puedes tener una ruta que redirija a otra URL ya sea dentro de nuestra aplicación o en otro servidor, solamente hemos de especificar la URL a la que queremos que se dirija.

'/alias' : '/some/other/route',
'GET
/google': 'http://www.google.com'

Ten cuidado de no crear un bucle infinito al redirigir dentro de la aplicación.

Has de saber que al redirigir, lo más probable es que tanto el método HTTP, parámetros y cabeceras se pierdan, y la petición se transforma en una simple petición GET. En el ejemplo una petición a  la ruta /alias daría como resultado una petición GET a /some/other/route.

Destino … Respuesta personalizada

También podemos asignar una dirección directamente a una respuesta personalizada:

'/foo': {response: 'notFound'}

Sólo tienes que especificar el nombre del archivo sin la extensión (.js) que contiene tu respuesta, deberá estar ubicado en api/responses. El nombre de la respuesta que especifiquemos hace diferencia entre mayúsculas y minúsculas. Si una ruta apunta hacia una respuesta inexistente, Sails emitirá un error y no hará caso de la ruta.

Destino … Política

En la mayoría de los casos para aplicar una política a una acción de un controlador, hacemos uso del archivo de configuración config/policies.js. Habrá ocasiones en la que quieras aplicar una política directamente a una ruta, sobre todo cuando estés usando en el destino de la ruta una vista o blueprint.  

'/foo': {policy: 'myPolicy'}

Sin embargo, aparte de aplicar la política, también querrás que tenga otro tipo de destino. Esto lo conseguimos usando un array, quedaría de esta forma:

‘/foo’: [{ policy: ‘miPolicy’ }, { blueprint: ‘find’, model: ‘user’ }]

Esto aplicará la política a la ruta, si la política devuelve true o next(), entonces se ejecuta la acción find.

Opciones de los destinos

Aparte de todas la opciones que hemos visto hasta ahora, cualquier otra propiedad que  quieras introducir en el destino, será transferido al controlador como un objeto req.options. 

Hay varias propiedades reservadas  que se pueden utilizar para alterar el comportamiento de las rutas, estas se enumeran en la siguiente tabla.

Propiedad

Tipos de Destino Aplicables

Tipo de Datos

Detalles

skipAssets

todos

0/1

Establezca en true, si no desea que la ruta coincida con direcciones url en donde la parte final de la dirección sea un archivo estático (ej.: miImagen.jpg). Esto es útil cuando se crean URL’s slugs.

skipRegex

todos

0/1

Si necesitas gestionar rutas que han de ser omitidas en base a diferentes criterios, esta opción, permite especificar expresiones regulares.

locals

controladores, vistas, blueprint, respuestas

{}

Establece por defecto el paso de variables locales a cualquier vista que es renderizada mientras se maneja la petición.

cors

todos

{}  -  0/1  -  “”

Especifica como manejar las peticiones de dicha ruta desde diferentes orígenes.

populate

blueprint

0/1

Indica si los resultados de la acción find o findOne, deben de tener campos de modelos relacionados. Por defecto, el valor se establece en config/blueprints.js

skip, limit, sort, where

blueprint

{}

Establece los criterios para la acción find (blueprint).

URL’s amigables (slugs)

Las URL semánticas o URL amigables son aquellas URLs que son, dentro de lo que cabe, entendibles para el usuario. Lejos de las clásicas URLs de las páginas dinámicas llenas de variables GET y números difíciles de recordar, las URL semánticas están formadas con palabras relacionadas con el contenido de la página y fáciles de recordar. Estas se utilizan en los sitios web dinámicos (no estáticos). Por ello se están utilizando mucho más que las URL extensas.

Un caso de uso muy común para las rutas explícitas es diseñarlas para que sean amigables. Por ejemplo, considera una url de un repositorio de Github (http://www.github.com/balderdashy/sails.js). En sails, podríamos definir esta ruta en la parte inferior de nuestro archivo config/routes.js.

// config/routes.js

‘get /:account/:repo’: {

    controller:’RepoController’,

    action: ‘show’,

    skipAssets: true

}

En la acción show de nuestro RepoController usamos req.para(‘account’) y req.param(‘repo’) para saber los datos del repositorio adecuado, para luego pasarlo tanto a la vista como a las locales (locals), esta parte la veremos más adelante en la sección de vistas.

La opción skipAssets nos asegura de que la ruta no coincide accidentalmente con cualquiera de nuestros assets o archivos estáticos (ej.: /images/logo.png).

6. Controladores

Visión del conjunto

Los controladores son los objetos principales en nuestra aplicación, estos son los responsables para responder a las peticiones que provienen normalmente de un navegador web, aplicaciones móviles u otro sistema capaz de comunicarse con el servidor.  La mayoría de las veces actúan como intermediarios entre nuestros modelos y vistas. Para cualquier aplicación, los controladores contienen la lógica de negocio de nuestro proyecto.

Acciones

Los controladores están compuestos de una colección de métodos llamados acciones (actions).  Los métodos o acciones se pueden unir a las rutas de nuestra aplicación, de tal modo que cuando el cliente solicite una ruta el método o acción se ejecute para realizar la lógica de negocio y en la mayoría de los casos genere una respuesta. Por ejemplo la ruta GET /hola podría estar ligada a un método o acción como se muestra a continuación:

function (req, res) {
 
return res.send("Hola!");
}

Así que cada vez que nuestro navegador apunte a la ruta /hola, este imprimirá en pantalla Hola!.

¿Donde definimos los controladores?

Los controladores están o serán definidos en el directorio api/controllers/. Podemos crear tantos controladores como queramos, pero para que sean cargados como controladores deberán estar dentro de dicho directorio, el nombre del archivo deberá finalizar con Controller.js. Por convención, los controladores usa Pascal-cased, con lo que la primera letra de cada palabra (incluida la primera) la escribiremos en mayúscula, por ejemplo UserController.js, MiController.js o CualquierOtroNombreController.js .

Puedes organizar tus controladores dentro de grupos, guardando estos en sub-carpetas dentro de api/controllers , sin embargo nota que el nombre de la sub-carpeta pasará a formar parte de la identidad del controlador en el momento que se utilice el enrutamiento (aprenderemos más sobre esto en la sección de “Routing” ).

¿Como es un archivo Controlador?

Un archivo controlador se define como un objeto en JavaScript. La llave o clave (key) es el nombre de la acción, y los valores son los métodos de dicha acción. Observa este ejemplo sencillo de un controlador:

module.exports = {
 
hola: function (req, res) {
   
return res.send("Hola!");
 },
 
adios: function (req, res) {
   
return res.redirect("http://www.sayonara.com");
 }
};

Este controlador define dos acciones: por un lado tenemos la acción “hola” que responde a la petición con un mensaje de tipo string (cadena de texto) y la segunda acción “adios” que responde redireccionado a otra página web.

Los objetos “req” y  “res” le resultará familiar si anteriormente ha usado Express.js. Esto es porque Sails usa Express para manejar o manipular el enrutado. Quizás te preguntes acerca del argumento “next” en las acciones, a diferencia de otros métodos en Express, los controladores en Sails siempre deben ser la última parada en la cadena de la petición, es decir, que siempre ha de devolver o una respuesta o un error. Aunque es posible utilizar el argumento “next” en las acciones, no es nada recomendable, en su lugar puedes hacer  uso de este parámetro en las políticas (policies) siempre que sea posible, algo que veremos en su momento.

Enrutado (Routing)

Por defecto, Sails crea rutas blueprint (las rutas blueprint enlazan rutas vinculadas automáticamente, sin tener que definirlas explícitamente en una ruta) para cada acción, de modo que si hacemos una petición GET a /:controlador/:accion se activará la acción. Si tomamos como ejemplo el controlador anterior, imagina que lo hubiésemos guardado como api/controllers/SayController.js, entonces las rutas hacia dichas acciones estarían disponibles como /say/hola  y  /say/adios. Si el controlador lo hubiésemos guardado en una sub-carpeta como por ejemplo api/controllers/we/SayControlller.js entonces las rutas serían /we/say/hola y  /we/say/adios.

Además de las rutas por defecto, Sails te permite enlazar manualmente las rutas a las acciones de los controladores utilizando el archivo config/routes.js. A las rutas definidas en este archivo se les denomina rutas explicitas ¿Cuando debemos utilizar rutas explícitas?

 

Para enlazar manualmente una ruta a una acción, lo haremos siempre desde el archivo config/routes.js, usamos el verbo HTTP y la dirección. La dirección de la ruta es la clave o llave (key) y el nombre_del_controlador.nombre_de_la_acción es el valor.

Por ejemplo, la siguiente ruta hará que la aplicación  active la acción preparar(), siempre que la petición reciba el método POST

        api/controllers/BocadilloController.js

'POST /make/a/bocadillo: 'BocadilloController.preparar'

Para hacerte una idea más completa, revisa la sección rutas, obtendrás una descripción más completa de las opciones disponibles.

Controladores ligeros

La mayoría de los frameworks MVC recomiendan escribir controladores ligeros, sails no iba a ser menos, ni hacer una excepción, ya que es una buena idea mantener los controladores tan simples como sea posible. Mostraremos el porqué es útil mantenerlos lo más simple posible.

El código del controlador depende esencialmente de un evento. En la parte del backend de sails, este evento es casi siempre una solicitud entrante. De modo que si escribimos un montón de código en las acciones del controlador, no es raro pensar que el código se vuelva dependiente del contexto de la petición, lo cual está bien…. hasta que quieras utilizar ese código en otra acción diferente o desde la línea de comandos.

Así que la filosofía de mantener el controlador lo más simple posible o ligero, es fomentar la disociación del código para poderlo hacer reutilizable, de esta forma cuando quieras realizar un cambio no tendrá que hacerlo en todos los controladores. Con sails podemos lograr esto de varias maneras diferentes, pero las más comunes para la extrapolación de código de los controladores son:

Generando Controladores

Puedes utilizar la herramienta que brinda Sails,  desde la línea de comandos puedes generar controladores automáticamente, junto con sus acciones, por ejemplo:

$ sails generate controller <nombre controlador> [nombres de acciones separados por comas...]

Por ejemplo, si ejecutamos el siguiente comando:

$ sails generate controller comentario crear,eliminar,editar,modificar


info: Genera un  
nuevo controlador con nombre `comentario` en api/controllers/ComentarioController.js!

Sails generará un archivo en api/controllers/ComentarioController.js parecido a esto:

/**
* ComentarioController.js
*
* @description :: Server-side logic for managing comentarios.
*/


module.exports
= {

 
/**
  * ComentarioController.crear()
  */

 
crear: function (req, res) {
   
return res.json({
     todo:
'Not implemented yet!'
   });
 },

 
/**
  * ComentarioController.eliminar()
  */

 
eliminar: function (req, res) {
   
return res.json({
     todo:
'Not implemented yet!'
   });
 },

 
/**
  * ComentarioController.editar()
  */

 
editar: function (req, res) {
   
return res.json({
     todo:
'Not implemented yet!'
   });
 },

 
/**
  * ComentarioController.modificar()
  */

 
modificar: function (req, res) {
   
return res.json({
     todo:
'Not implemented yet!'
   });
 }
};

7. Respuestas Personalizadas

Visión del conjunto

Sails nos permite personalizar las respuestas que emite el servidor, por defecto, en la carpeta o directorio api/responses, encontramos las más comunes. Para personalizarlas solamente hemos de editar el archivo .js apropiado para ello.

Veamos un ejemplo, considerando la siguiente acción:

foo: function(req, res) {
 
if (!req.param('id')) {
    res.status(
400);
    res.view(
'400', {message: 'Lo sentimos, necesitamos el ID para mostrarle los datos!'});
  }
  ...
}

Este código maneja una petición, respondiendo con un error de tipo 400 y un mensaje corto que describe el problema. Sin embargo, este código tiene algunos inconvenientes, principalmente:

Ahora considera este cambio:

foo: function(req, res) {
 
if (!req.param('id')) {
    res.badRequest(
'Lo sentimos, necesitamos el ID para mostrarle los datos!');
  }
  ...
}

Ahora este enfoque, contiene las siguientes ventajas:

Métodos de respuesta y archivos

Todos los archivos .js guardados o almacenados en la carpeta /api/responses serán ejecutados por la llamada res.[nombreRespuesta] desde nuestro controlador. Por ejemplo, /api/responses/serverError.js puede ser llamado o ejecutado llamándolo a través de res.serverError(errores). Los objetos request y  response están disponibles dentro del archivo response (serverError.js) cómo this.req y this.res , esto permite que la función de respuesta tome los parámetros arbitrariamente.

Respuestas Por Defecto

Cada una de las respuestas envía un objeto JSON normalizado, si el cliente lo que espera es un JSON, contiene una clave/llave (key) con el código de estado HTTP y claves adicionales con información acerca del error.

res.serverError(errors)

Esta respuesta normaliza el error o errores en un array de objetos. Los errores pueden ser una o varias cadenas de texto o incluso objetos. Sails registra todos los errores normalmente en consola y responde con la vista /500.* si el cliente esta esperando HTML, o en su defecto el cliente obtiene un JSON si es lo que esperaba. En modo desarrollo, la lista de errores se incluye en la respuesta. En modo producción los errores reales se suprimen.

res.badRequest(validationErrors, redirectTo)

Esta respuesta incluye el código de estado 400 y todos los datos relevantes pasados a través de los parámetros validationErrors.

Para los formularios web tradicionales (sin AJAX) este middleware sigue las mejores prácticas para cuando el usuario envía datos del formulario que no son válidos.

res.notFound()

Si lo que el cliente espera es JSON, esta respuesta envía un objeto con código de estado 404.

De lo contrario Sails sirve la vista /views/404.* Si la vista no es encontrada, entonces el cliente recibirá la respuesta en formato JSON.

res.forbiden(message)

Si lo que el cliente espera es JSON, esta respuesta envía un objeto con código de estado 403 junto con el contenido del mensaje.

De lo contrario Sails sirve la vista /views/403.* Si la vista no es encontrada, entonces el cliente recibirá la respuesta en formato JSON.

Respuesta Personalizada

Para añadir tu propio método de respuesta personalizada, solamente has de añadir un archivo en la carpeta /api/responses con el mismo nombre del método que le gustaría crear. El archivo debe exportar la función, y puede obtener tantos parámetros como desees. Mira este ejemplo de una respuesta personalizada:

/**
* api/responses/miRespuesta.js
*
* Esto estará disponible en los controladores como
res.miRespuesta('foo');
*/


module.exports = function(message) {

 
var req = this.req;
 
var res = this.res;

 
var viewFilePath = 'miEspecialVista';
 
var statusCode = 200;

 
var result = {
   status: statusCode
 };

 
// mensaje opcional
 
if (message) {
   result.message
= message;
 }

 
// Si la peticion del usuario es JSON, respondemos con JSON
 
if (req.wantsJSON) {
   
return res.json(result, result.status);
 }

 
// Establecemos el código de estado y la vista local
 res.status(result.status);
 
for (var key in result) {
   res.locals[key]
= result[key];
 }
 
// Renderizamos la vista
 res.render(viewFilePath, result,
function(err) {
   
// Si la vista no existe, o ocurre un error, responde con JSON
   
if (err) {
     
return res.json(result, result.status);
   }

   
// Servimos la vista miEspecialVista.*
   res.render(viewFilePath);
 });

8. Modelos y ORM

Waterline: SQL/noSQL Tratamiento de Datos (ORM/ODM)

Sails lleva incorporado un magnífico ORM/ODM llamado Waterline. Una herramienta que simplifica la interacción de nuestra aplicación con una o más bases de datos. Proporciona una capa de abstracción en la parte superior de la base de datos, que permite consultar y manipular fácilmente los datos, sin necesidad de escribir una gran cantidad de código.

En las bases de datos como Postgres, Oracle y MySQL, los modelos están representados por tablas, en cambio en MongoDB se representa por colecciones. En Redis se representa por medio de pares clave/valor (key/value). Cada base de datos tiene su propio dialecto que hace que consultar sus datos sea totalmente distinta unas de otras, en algunos casos incluso requiere instalación y compilación de algún módulo nativo específico para la conexión al servidor.

La sintaxis de waterline se centra en la lógica de negocio, como la creación de nuevos registros o búsqueda de registro/s existentes, actualización o la destrucción de los mismos. Da lo mismo a la base de datos que estemos conectados, el uso de waterline será el mismo. Por otra parte waterline permite asociaciones entre modelos de una manera sencilla, incluso si los datos de cada modelo reside en una bases de datos diferente. Esto significa que podemos cambiar los modelos de nuestra aplicación de MongoDB a Postgress o MySQL o Redis, sin tener que cambiar ningún código de nuestra aplicación.

Para momentos en los que necesitamos la funcionalidad de una base de datos específica,  waterline proporciona una interfaz de consulta que nos permite interactuar directamente con el controlador de la base de datos.

Escenario

Imaginemos que estamos construyendo una aplicación web que contiene un comercio online (e-commerce), a esta aplicación le acompaña otra que permite acceder desde un dispositivo móvil. Los usuarios podrán navegar por los productos, filtrarlos por categoría o realizar una búsqueda de un artículo en concreto, finalmente el usuario realizará una compra. Algunas partes de la aplicación son bastante comunes. En nuestra API habrá partes que han de controlar la autenticación de usuarios, registro, órdenes de pago, reseteo de contraseñas, etc. Sin embargo, existen todavía algunas características en las que nos tenemos que involucrar aún más.

Flexibilidad

Quizá se pregunte, qué motor de base de datos es la mejor para llevar a cabo la aplicación web de comercio online.

Pues bien, no se apresure, seguro que no le gustará tomar una decisión equivocada.

En la mayoría de los casos elegiremos una sola base de datos para una aplicación API, cabe la posibilidad que se necesite mantener la compatibilidad con una o más bases de datos, ya sea porque así lo deseemos o bien por razones de rendimiento.

Sails  usa sails-disk por defecto, esto significa que puedes empezar a construir tu aplicación sin configurar nada, usando un archivo temporal como almacenamiento de datos.

Compatibilidad

El cliente me pide que integre los productos en la aplicación de comercio online, me dice que los tiene en una ERP con no sé qué de DB2. De todas formas seguro que no es difícil implementarlo.

Cuando desarrollamos aplicaciones empresariales, nos encontramos que a menudo debemos integrarlas con bases de datos que el propio cliente ya tiene. Si tienes suerte, una migración de datos de una sola vez, puede que sea todo lo que necesites, pero en algunas ocasiones es posible que la base de datos existente se vea modificada por otras aplicaciones en tiempo real. Con el fin de construir su aplicación es posible que tenga que convivir con otros sistemas de conjunto de datos  separados en otro lugar. Estos conjuntos de datos podrían incluso estar en cinco servidores diferentes repartidos por el mundo. Una base de datos podría albergar datos relacionales SQL, mientras que otro servidor podría contener colecciones de MongoDB.

Sails/Waterline te permite conectar modelos a diferentes bases de datos, ya sean locales o en cualquier otro sitio en internet. Puedes construir un modelo “Usuario” y que esté asignado a una tabla de MySQL personalizada que herede de otra base de datos. De igual manera para un modelo “Producto” que esté asignado a una tabla de DB2 o lo mismo para una colección de MongoDB. Lo mejor de todo es que podemos a través de .populate() conectarnos a diferentes almacenes de datos y adaptadores y configurar un modelo para que resida en una base de datos diferente. Tu modelo o controlador no sufrirán ningún cambio en el  código.

Adaptadores

Como la mayoría de los frameworks MVC y sails no iba a ser menos, soportan múltiples bases de datos. Esto significa que la sintaxis para manipular o hacer consultas a la base de datos es siempre el mismo, ya sea que esté trabajando con MySQL, MongoDB o cualquier otra base de datos compatible.

Waterline se basa en la flexibilidad de sus adaptadores. Un adaptador es un pedazo de código que asigna métodos como find() y create() para una sintaxis de bajo nivel como SELECT * FROM  y  INSERT INTO. El núcleo de sails contiene adaptadores de código abierto de las bases de datos más populares. También una gran cantidad de adaptadores  que están disponibles en la comunidad.

Conexiones

Una conexión representa la configuración de una base de datos en particular. Este objeto configura un adaptador: el host, puerto, nombre de usuario y contraseña. Las conexiones se definen en config/connections.js

//  config/connections.js
// ...
{
 adapter:
'sails-mysql',
 host:
'localhost',
 port:
3306,
 user:
'root',
 password:
'70m45_7ur64d0'
}
// ...

Por defecto la conexión a la base de datos para una aplicación en sails se encuentra en config/models.js, pero podemos sobreescribir la conexión en un modelo especificando una conexión (connection).

Analogía

Imagine un archivador físico lleno de hojas de facturas. Todas las facturas tienen los mismos campos (ej.: número_de_factura, fecha, nombre_cliente,  … ), pero en cada factura, los valores de estos campos varían. Por ejemplo, una factura puede contener “100”, “2015-05-16T21:16:15.127Z”, “Pedro Manuel”, …

mientras que otra factura contiene “101”, “2015-06-16T21:16:15.127Z”, “Aitor Menta”, …

Ahora imagine que tiene un negocio de venta de perritos calientes, si es una persona organizada, es posible que establezca los archivos de su negocio de la siguiente manera:

En su aplicación, un modelo será como un archivador que uno o muchos archivos, ya sean de empleados, facturas de compras, etc. Los atributos son los campos que contiene cada archivo, todos los archivo de un archivador tienen los mismos campos pero con diferentes valores.

Nota: Esta documentación no será aplicable si reemplazas el ORM que trae consigo Sails, (Waterline).

Asociaciones / Relaciones

Con Sails y Waterline, puede asociar modelos a través de múltiples bases de datos. Esto significa que si los los datos de usuarios residen en una base de datos PostgreSQL y las fotografías de estos usuarios residen en una base de datos MongoDB, puede interactuar con los datos como si la ubicación de los usuarios y las fotos estuvieran en la misma base de datos.

También puede hacer que las asociaciones abarquen diferentes conexiones usando el mismo adaptador. Esto es muy útil, si su aplicación necesita acceso o actualizar los datos almacenados en una base de datos MySQL que tenga físicamente en su empresa o negocio, pero también guardar o recuperar datos desde otra base de datos MySQL pero esta vez ubicada en la nube.

Dominación

Veamos un ejemplo algo peculiar. Donde tenemos un modelo que reside en una base de datos

MySQL y otro modelo en una base de datos Redis. Entre ellos queremos crear una relación de

muchos-a-muchos.

// Usuario.js
module.exports
= {
 connection:
'BD_MySQL',
 attributes: {
   email:
'string',
   wishlist: {
     collection:
'producto',
     via:
'wishlistedBy'
   }
 }
};

// Producto.js
module.exports
= {
 connection:
'BD_Redis',
 attributes: {
   name:
'string',
   wishlistedBy: {
     collection:
'usuario',
     via:
'wishlist'
   }
 }
};

Es fácil saber lo que está pasando en esta relación de muchos-a-muchos (many-to-many) donde

usuarios y productos están relacionados. De hecho, se ha puesto este ejemplo pero podría ser otro

cualquiera.

Ahora surge la pregunta … ¿Dónde reside el recurso de la relación Producto-Usuario? Sabemos

que en un lugar o en otro va a estar, pero qué pasa si queremos controlar la base de datos en la que

va a residir el recurso.

Para una posible solución podríamos especificar una tercera conexión o adaptador para la unión de los

dos modelos. Por ahora, nos centraremos cual de los dos lados sea el que predomine.

Para ello nos centramos en el concepto de “posición dominante”. En cualquier cruce  de modelos

relacionados, uno de los lados ha de dominar. Para entenderlo mejor, imagine un matrimonio donde

el hombre tiene la nacionalidad Española y la mujer la nacionalidad Francesa, al tener un hijo, este,

podrá escoger a qué nacionalidad quiere pertenecer.

Volviendo al ejemplo anterior, pero en esta ocasión estableceremos la base de datos MySQL como la

dominante. Esto significa que la relación Producto-Usuario se almacenará como una tabla MySQL.

// Usuario.js
module.exports
= {
 connection:
'BD_MySQL',
 attributes: {
   email:
'string',
   wishlist: {
     collection:
'producto',
     via:
'wishlistedBy',
     dominant:
true
   }
 }
};

// Producto.js
module.exports
= {
 connection:
'BD_Redis',
 attributes: {
   name:
'string',
   wishlistedBy: {
     collection:
'usuario',
     via:
'wishlist'
   }
 }
};

        La elección de una tabla como “dominante”

        Existen varios factores que influyen a la hora de tomar la decisión de elegir como dominante una

        tabla:

        

¿Que pasa si una colección no tiene la propiedad “via”?

Si una asociación de una colección no tiene la propiedad “via”, automáticamente se establece como dominante.

¿Que ocurre si ninguno de las colecciones tienen la propiedad “via”?

Entonces las colecciones no tienen relación, Ambas son dominante, ya que son tablas de relaciones separadas.

¿Que ocurre con las asociaciones de modelo?

En todos los otros tipos de asociaciones, la propiedad dominante no está permitida. La configuración de un lado como dominante solo es necesaria para asociaciones entre dos modelos que tengan una propiedad o atributo: { via: ‘...’, collection: ‘...’ } en ambos lados.

¿Pueden ser ambos modelos dominante?

No. Si ambos modelos en una conexión cruzada con relación de muchos-a-muchos se establecen como dominantes, se produce un error antes de que empiece a correr el servidor o nuestra aplicación.

¿Que ocurre si ambos modelos no son dominantes?

Si ninguno de los modelos en una conexión cruzada con relación de muchos-a-muchos no tiene establecida la propiedad dominante, se produce una advertencia al iniciar el servidor y automáticamente basándose en las características de la relación establece a un modelo la propiedad dominante, tomando esa decisión por orden alfabético.

Relaciones de Muchos-a-Muchos

Visión de conjunto

Una asociación de muchos-a-muchos establece que un modelo se puede asociar a muchos otros modelos y viceversa. Debido a que ambos modelos pueden tener muchos modelos relacionados, se deberá crear una nueva tabla que realice la unión entre ellos.

Waterline analizará los modelos en busca de asociaciones y automáticamente construirá esa tabla de unión por nosotros.

Veamos un ejemplo, donde construimos un esquema de usuarios y otro de mascotas, en donde un usuario podrá tener muchos registros de mascotas y una mascota varios usuarios.

Ejemplo de muchos-a-muchos

En este ejemplo, iniciamos con un array de usuarios y un array de mascotas. Creamos los registros para cada elemento de cada array, luego asociamos todas las mascotas con todos los usuarios.

Si todo funciona correctamente, nosotros podremos realizar una consulta a cualquier usuario y ver todas las mascotas que tiene. Por otra parte, también se podrá consultar una mascota y ver los usuarios que tienen asociada esta mascota.

        myApp/api/models/Mascota.js

module.exports = {

   attributes: {
       name:
'STRING',
       color:
'STRING',

       
// Añadimos referencia a Usuario
       usuarios: {
           collection:
'usuario',
           via:
'mascotas'
       }
   }
}

myApp/api/models/Usuario.js

module.exports = {

   attributes: {
       name:
'STRING',
       age:
'STRING',

       
// Añadimos referencia a Mascota
       mascotas: {
           collection:
'mascota',
           via:
'usuarios'
       }
   }
}

        

        myApp/config/bootstrap.js

module.exports.bootstrap = function (cb) {

// después de crear los usuarios, almacenamos en esta variable las asociaciones con las mascotas
var usuariosAlmacenados = [];

var usuarios = [{name:'Miguel',age:'16'},{name:'Pedro',age:'25'},{name:'Gabriel',age:'107'}];
var ponis = [{ name: 'Dedo Gordo', color: 'rosa'},{ name: 'Arco Iris',color: 'azul'},{ name: 'Manzana Negra', color: 'naranja'}]

// Construye la asociación.
// A la función se le pasa un  poni, que itera a través del array de los usuarios recientemente creados, uniendo cada uno de estos a una tabla.
var associate = function(unPony, cb){
 
var thisPoni = unPony;
 
var callback = cb;

 usuariosAlmacenados.
forEach(function(thisUsuario, index){
   console.
log('Asociando ', thisPoni.name, 'con', thisUsuario.name);
   thisUsuario.mascotas.add(thisPoni.id);
   thisUsuario.save(console.log);

   
if (index === usuariosAlmacenados.length-1)
     
return callback(thisPoni.name);
 })
};


// Esta devolución de llamada se ejecuta después de que todas las mascotas sean creadas.
// Se envía cada nueva mascota a la función “associate” con los usuarios.  
var afterPony = function(err, nuevoPoni){
 
while (nuevoPoni.length){
   
var thisPoni = nuevoPoni.pop();
   
var callback = function(poniID){
     console.
log(‘Asociacion terminada para el poni: ’,poniID)
   }
   associate(thisPoni, callback)
 }
 console.
log('Todos pertenecen a todos!! Saliendo.');

 
// Esta devolución de llamada nos permite dejar que bootstrap.js continúe ejecutando nuestra aplicación!
 
return cb();
};

// Esta devolución de llamada se ejecuta después de que todos los usuarios sean creados.
// Retorna el usuario y lo guarda en el array usuariosAlmacenados para usarlo posteriormente.
var afterUser = function(err, nuevoUsuario){
 
while (nuevoUsuario.length)
   usuariosAlmacenados.
push(nuevoUsuario.pop());

 Mascota.create(ponis).exec(afterPony)
};


Usuario.create(usuarios).exec(afterUser)

};

A continuación ejecutamos nuestra aplicación con sails console

        

        

Asociando  Manzana Negra con Gabriel

Asociando  Manzana Negra con Pedro

Asociando  Manzana Negra con Miguel

Asociacion terminada para el poni:  Manzana Negra

Asociando  Arco Iris con Gabriel

Asociando  Arco Iris con Pedro

Asociando  Arco Iris con Miguel

Asociacion terminada para el poni:  Arco Iris

Asociando  Dedo Gordo con Gabriel

Asociando  Dedo Gordo con Pedro

Asociando  Dedo Gordo con Miguel

Asociacion terminada para el poni:  Dedo Gordo

Todos pertenecen a todos!! Saliendo.

info: Welcome to the Sails console.

info: ( to exit, type <CTRL>+<C> )

sails> null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Gabriel',

  age: '107',

  createdAt: '2015-06-14T16:58:48.988Z',

  updatedAt: '2015-06-14T16:58:49.042Z',

  id: 48 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Pedro',

  age: '25',

  createdAt: '2015-06-14T16:58:48.982Z',

  updatedAt: '2015-06-14T16:58:49.045Z',

  id: 47 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Miguel',

  age: '16',

  createdAt: '2015-06-14T16:58:48.977Z',

  updatedAt: '2015-06-14T16:58:49.049Z',

  id: 46 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Gabriel',

  age: '107',

  createdAt: '2015-06-14T16:58:48.988Z',

  updatedAt: '2015-06-14T16:58:49.042Z',

  id: 48 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Pedro',

  age: '25',

  createdAt: '2015-06-14T16:58:48.982Z',

  updatedAt: '2015-06-14T16:58:49.045Z',

  id: 47 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Miguel',

  age: '16',

  createdAt: '2015-06-14T16:58:48.977Z',

  updatedAt: '2015-06-14T16:58:49.049Z',

  id: 46 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Gabriel',

  age: '107',

  createdAt: '2015-06-14T16:58:48.988Z',

  updatedAt: '2015-06-14T16:58:49.042Z',

  id: 48 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Pedro',

  age: '25',

  createdAt: '2015-06-14T16:58:48.982Z',

  updatedAt: '2015-06-14T16:58:49.045Z',

  id: 47 }

null { mascotas:

   [ { name: 'Dedo Gordo',

       color: 'rosa',

       createdAt: '2015-06-14T16:58:48.995Z',

       updatedAt: '2015-06-14T16:58:48.995Z',

       id: 43 },

     { name: 'Arco Iris',

       color: 'azul',

       createdAt: '2015-06-14T16:58:48.997Z',

       updatedAt: '2015-06-14T16:58:48.997Z',

       id: 44 },

     { name: 'Manzana Negra',

       color: 'naranja',

       createdAt: '2015-06-14T16:58:48.999Z',

       updatedAt: '2015-06-14T16:58:48.999Z',

       id: 45 } ],

  name: 'Miguel',

  age: '16',

  createdAt: '2015-06-14T16:58:48.977Z',

  updatedAt: '2015-06-14T16:58:49.049Z',

  id: 46 }

Asociando en una sola dirección

Visión de conjunto

Una asociación de sentido único, es un modelo donde se asocia con otro modelo. Puede realizar una consulta a ese modelo y obtener su modelo asociado. Sin embargo no se puede consultar el modelo asociado y obtener el modelo que lo asocia.

Ejemplo

Entenderemos mejor todo esto con un ejemplo. En este ejemplo, asociaremos un Usuario con una Mascota, pero no una Mascota con un Usuario.

myApp/api/models/Mascota.js

module.exports = {

   attributes: {
       name:
'STRING',
       color:
'STRING',
   }
}

myApp/api/models/Usuario.js

module.exports = {

   attributes: {
       name:
'STRING',
       age:
'STRING',

       poni: {
           model:
'Mascota'
       }
   }
}

Ejecutamos sails console

sails> Mascota.create({name:'Dedo Gordo', color:'rosa'}).exec(console.log)
null { name: 'Dedo Gordo',
 color:
'rosa',
 createdAt: Tue Feb
11 2014 15:45:33 GMT-0600 (CST),
 updatedAt: Tue Feb
11 2014 15:45:33 GMT-0600 (CST),
 id:
5 }

sails
> Usuario.create({name:'Miguel', age:21, poni:5}).exec(console.log);
null { name: 'Miguel',
 age:
21,
 poni:
5,
 createdAt: Tue Feb
11 2014 15:48:53 GMT-0600 (CST),
 updatedAt: Tue Feb
11 2014 15:48:53 GMT-0600 (CST),
 id:
1 }

sails
> Usuario.find({name:'Miguel'}).populate('poni').exec(console.log);
null [ { name: 'Miguel',
   age:
21,
   pony:
    { name:
'Dedo Gordo',
      color:
'rosa',
      id:
5,
      createdAt: Tue Feb
11 2014 15:45:33 GMT-0600 (CST),
      updatedAt: Tue Feb
11 2014 15:45:33 GMT-0600 (CST) },
   createdAt: Tue Feb
11 2014 15:48:53 GMT-0600 (CST),
   updatedAt: Tue Feb
11 2014 15:48:53 GMT-0600 (CST),
   id:
1 } ]

Hemos formado una asociación en uno de los modelos, con lo cual una mascota no tiene restricciones en el número de Usuarios al que puede pertenecer.

Uno-a-Muchos

Visión de conjunto

Las asociaciones de uno-a-muchos afirma que  un modelo se puede asociar a muchos otros modelos. Para construir esta asociación es necesario añadir un atributo a un modelo usando la propiedad collection.

En una asociación de uno-a-muchos uno de los lados ha de tener un atributo collection y el otro lado ha de contener un atributo con el nombre del modelo que lo asocia.

Ejemplo

myApp/api/models/Mascota.js

module.exports = {

   attributes: {
       name:
'STRING',
       color:
'STRING',

        propietario: {

            model: ‘Usuario’

        }
   }
}

myApp/api/models/Usuario.js

module.exports = {

   attributes: {
       name:
'STRING',
       age:
'STRING',

       mascotas: {
           collection:
'Mascota',

            via: ‘propietario’
       }
   }
}

Ejecutamos sails console

sails> Usuario.create({name:'Miguel', age:'21'}).exec(console.log)
null { mascotas: [Getter/Setter],
 name:
'Miguel',
 age:
21,
 createdAt: Tue Feb
11 2014 17:49:04 GMT-0600 (CST),
 updatedAt: Tue Feb
11 2014 17:49:04 GMT-0600 (CST),
 id:
1 }

sails
> Mascota.create({name:'Dedo Gordo', color:'rosa', propietario:1}).exec(console.log)
null { name: 'Dedo Gordo',
   color:
'rosa',
   propietario:
1,
   createdAt: Tue Feb
11 2014 17:58:04 GMT-0600 (CST),
   updatedAt: Tue Feb
11 2014 17:58:04 GMT-0600 (CST),
   id:
2 }

sails
> Mascota.create({name:'Manzana Negra', color:'naranja', propietario:1}).exec(console.log)
null { name: 'Manzana Negra',
   color:
'Naranja',
   propietario:
1,
   createdAt: Tue Feb
11 2014 18:02:58 GMT-0600 (CST),
   updatedAt: Tue Feb
11 2014 18:02:58 GMT-0600 (CST),
   id:
4 }

sails
> Usuario.find().populate('mascotas').exec(function(err,r){console.log(r[0].toJSON())});
{ mascotas:
  [ { name:
'Dedo Gordo',
      color:
'rosa',
      id:
2,
      createdAt: Tue Feb
11 2014 17:58:04 GMT-0600 (CST),
      updatedAt: Tue Feb
11 2014 17:58:04 GMT-0600 (CST),
      propietario:
1 },
    { name:
'Manzana Negra',
      color:
'naranja',
      id:
4,
      createdAt: Tue Feb
11 2014 18:02:58 GMT-0600 (CST),
      updatedAt: Tue Feb
11 2014 18:02:58 GMT-0600 (CST),
      propietario:
1 } ],
 name:
'Miguel',
 age:
21,
 createdAt: Tue Feb
11 2014 17:49:04 GMT-0600 (CST),
 updatedAt: Tue Feb
11 2014 17:49:04 GMT-0600 (CST),
 id:
1 }

sails
> Mascota.find(4).populate('propietario').exec(console.log)
null [ { name: 'Manzana Negra',
   color:
'naranja',
   propietario:
    { mascotas: [Getter/Setter],
      name:
'Miguel',
      age:
21,
      id:
1,
      createdAt: Tue Feb
11 2014 17:49:04 GMT-0600 (CST),
      updatedAt: Tue Feb
11 2014 17:49:04 GMT-0600 (CST) },
   createdAt: Tue Feb
11 2014 18:02:58 GMT-0600 (CST),
   updatedAt: Tue Feb
11 2014 18:02:58 GMT-0600 (CST),
   id:
4 } ]

Uno-a-Uno

Visión de conjunto

Las asociaciones uno-a-uno asumen que un modelo solo se puede relacionar con otro modelo. Para que un modelo sepa que está asociado con otro modelo, se debe incluir en los registros un foreign key.

Ejemplo

En este ejemplo asociamos una mascota a un usuario. El usuario sólo puede tener una mascota asociada y una mascota sólo puede tener un usuario asociado. Sin embargo,  a la hora de realizar una consulta la podemos realizar de cualquiera de los dos lados, crear o actualizar ambos modelos.

myApp/api/models/Mascota.js

module.exports = {

   attributes: {
       name:
'STRING',
       color:
'STRING',

        propietario: {

            model: ‘Usuario’

        }
   }
}

myApp/api/models/Usuario.js

module.exports = {

   attributes: {
       name:
'STRING',
       age:
'STRING',

       poni: {
           model:
'Mascota',
       }
   }
}

Ejecutamos sails console

sails> Usuario.create({ name: 'Miguel', age: 21}).exec(console.log);
null { name: 'Miguel',
 age:
21,
 createdAt: Thu Feb
20 2014 17:12:18 GMT-0600 (CST),
 updatedAt: Thu Feb
20 2014 17:12:18 GMT-0600 (CST),
 id:
1 }

sails
> Mascota.create({ name: 'Dedo Gordo', color: 'rosa', propietario: 1}).exec(console.log)
null { name: 'Dedo Gordo',
   color:
'rosa',
   owner:
1,
   createdAt: Thu Feb
20 2014 17:26:16 GMT-0600 (CST),
   updatedAt: Thu Feb
20 2014 17:26:16 GMT-0600 (CST),
   id:
2 }

sails
> Mascota.find().populate('propietario').exec(console.log)
null [ { name: 'Dedo Gordo',
   color:
'rosa',
   propietario:
    { name:
'Miguel',
      age:
21,
      id:
1,
      createdAt: Thu Feb
20 2014 17:12:18 GMT-0600 (CST),
      updatedAt: Thu Feb
20 2014 17:12:18 GMT-0600 (CST) },
   createdAt: Thu Feb
20 2014 17:26:16 GMT-0600 (CST),
   updatedAt: Thu Feb
20 2014 17:26:16 GMT-0600 (CST),
   id:
2 } ]

sails
> Usuario.find().populate('poni').exec(console.log)
null [ { name: 'Miguel',
   age:
21,
   createdAt: Thu Feb
20 2014 18:11:15 GMT-0600 (CST),
   updatedAt: Thu Feb
20 2014 18:11:15 GMT-0600 (CST),
   id:
2,
   poni: undefined } ]

sails
> Usuario.update({name:'Miguel'},{poni:2}).exec(console.log)
null [ { name: 'Miguel',
   age:
21,
   createdAt: Thu Feb
20 2014 17:12:18 GMT-0600 (CST),
   updatedAt: Thu Feb
20 2014 17:30:58 GMT-0600 (CST),
   id:
1,
   poni:
2 } ]

sails
> Usuario.findOne(1).populate('poni').exec(console.log)
null { name: 'Miguel',
 age:
21,
 createdAt: Thu Feb
20 2014 17:12:18 GMT-0600 (CST),
 updatedAt: Thu Feb
20 2014 17:30:58 GMT-0600 (CST),
 id:
1,
 poni:
  { name:
'Dedo Gordo',
    color:
'rosa',
    id:
2,
    createdAt: Thu Feb
20 2014 17:26:16 GMT-0600 (CST),
    updatedAt: Thu Feb
20 2014 17:26:16 GMT-0600 (CST),
    propietario:
1 } }

Atributos

Los atributos son piezas básicas de información acerca de un modelo. Un modelo, puede contener atributos como, nombre, apellidos, teléfono, edad, email, etc…

Opciones de los atributos

Las opciones que veremos a continuación se pueden utilizar para hacer cumplir limitaciones y añadir mejoras a los atributos del modelo..

type: Especifica el tipo de dato que podrá almacenar el atributo, de los cuales podemos optar por los siguientes

defaultsTo: Cuando se crea un nuevo registro en la base de datos, es posible que algún campo no se le esté suministrando un valor externamente, pero es posible que queramos que cuando ocurra esto se le asigne un valor por defecto, para ello haremos uso de esta opción.

attributes: {
 phoneNumber: {
   type:
'string',
   defaultsTo:
'111-222-3333'
 }
}

autoIncrement: Establece el atributo como una key auto-incremental. Cuando se añade un nuevo registro al modelo, si no se especifica un valor para este atributo, se incrementa en uno tomando como parámetro el último valor de los registros creados anteriormente, si no existe se establece en 1.

Importante! Al atributo que especifiquemos “autoIncrement”, deberá ser siempre de tipo integer. Ten en cuenta también, que si la base de datos con la que estás trabajando con este modelo es MySQL, has de saber que este no permite que dos columnas tengan la key “auto-incremental”

attributes: {
 codigo: {
   type:
'integer',
   autoIncrement:
 true
 }
}

unique: Se asegura de que dos registros no contengan el mismo valor. Esta es una restricción a nivel de adaptador (MySql, MongoDB, ...) con lo que se  crea un índice único en la base de datos.

attributes: {
 username: {
   type:
'string',
   unique:
true
 }
}

primaryKey: Usa este atributo como la clave principal. Sólo un atributo por modelo puede ser primaryKey. Cuidado! Nunca debes utilizar primaryKey si no tienes autoPK establecido a false

attributes: {
 uuid: {
   type:
'string',

    primaryKey: true,
   required:
true
 }
}

enum: Atribuye validación especial, solo guarda los datos, que coinciden con una serie de valores estipulados en un array.

attributes: {
 estado: {
   type:
'string',
   enum: [
'Pendiente', 'Aprovado', 'Denegado']
 }
}

size: Es soportado por el adaptador, puedes usarlo para definir el tamaño del atributo, por ejemplo en MySQL se especifica como número: varchar(n).

attributes: {
 nombre: {
   type:
'string',
   size:
24
 }
}

columnName: Dentro de una definición de atributo, puede especificar un nombre de columna, para forzar a Sails/Waterline a que almacene los datos de ese atributo en una columna específica. Esto no es solamente para MySQL, también funciona en campos de MongoDB y otros. Mientras que la propiedad columnName está diseñada para trabajar con bases de datos heredadas, también puede sernos útil en situaciones en las que la base de datos está siendo compartida por otras aplicaciones o en los casos que no tengas permisos para cambiar el esquema de  la tabla.

// Un atributo en uno de tus modelos:
 
// ...
 numberOfWheels: {
   type:
'integer',
   columnName:
'number_of_round_rotating_things'
 }
 
// ...

Esto es un ejemplo muy sencillo de cómo aplicar un nombre de columna a un atributo, pero veamos un caso más real.

Digamos que tienes un modelo “Usuario” en tu aplicación más o menos similar a este:

// api/models/User.js
module.exports
= {
 connection:
'shinyNewMySQLDatabase',
 attributes: {
   name:
'string',
   password:
'string',
   email: {
     type:
'email',
     unique:
true
   }
 }
};

Todo funciona bien, pero en lugar de usar una base de datos MySQL existente, alberga los datos de usuario en algún lugar de Internet:

// config/connections.js
module.exports
= {
 
// ...

 
// Los Usuarios existentes se encuentran aqui!
 rustyOldMySQLDatabase: {
   adapter:
'sails-mysql',
   user:
'bofh',
   host:
'db.eleven.sameness.foo',
   password:
'Gh19R!?had9gzQ#%AdsghaDABAMR>##G<ADMRH@)$(HTOGNAGNBI@',
   database:
'jonas'
 },
 
// ...
};

Imaginemos que en esta base de datos encontramos una tabla llamada “nuestros_usuarios” que tiene un aspecto como este:

the_primary_key

email_address

full_name

seriously_encrypted_password

7

toni@gmial.com

Tomas Turbado

ranchdressing

14

aitor@menta.com

Aitor Menta Fuerte

thousandisland

Para utilizar esto desde Sails, cambiaríamos nuestro modelo a algo parecido a esto:

// api/models/User.js
module.exports
= {
 connection:
'rustyOldMySQLDatabase',
 tableName:
'nuestros_usuarios',
 attributes: {
   id: {
     type:
'integer',
     unique:
true,
     primaryKey:
true,
     columnName:
'the_primary_key'
   },
   name: {
     type:
'string',
     columnName:
'full_name'
   },
   password: {
     type:
'string',
     columnName:
'seriously_encrypted_password'
   },
   email: {
     type:
'email',
     unique:
true,
     columnName:
'email_address'
   }
 }
};

Habrás notado, que usamos la propiedad tableName en este ejemplo. Indicamos el nombre de la tabla donde se guardarán nuestros datos.

Ciclo de vida de los Callbacks

Sails pone a disposición algunas retrollamadas (callbacks) en los modelos que se ejecutan automáticamente antes o después de ciertas acciones. Por ejemplo, algunas veces utilizamos un callback para encriptar un password antes de crear o modificar un registro de nuestro modelo.

Callbacks (on create) Al Crear

Callbacks on update (Al Modificar)

Callbacks on destroy (Al Eliminar)

Ejemplo

Si queremos encriptar un password antes de que se guarde un registro en la base de datos, haremos uso del callback beforeCreate().

var bcrypt = require('bcrypt');

module.exports
= {

 attributes: {

   username: {
     type:
'string',
     required:
true
   },

   password: {
     type:
'string',
     minLength:
6,
     required:
true,
     columnName:
'encrypted_password'
   }

 },


 
// Lifecycle Callbacks
 
beforeCreate: function (values, cb) {

   
// Encrypt password
   bcrypt.hash(values.password,
10, function(err, hash) {
     
if(err) return cb(err);
     values.password
= hash;
     
// Si llamamos a cb() con un argumento retorna un error. Útil para cancelar la operación si falla algo.
     cb();
   });
 }
};

Modelos

Un modelo representa una estructura de datos, ya sea en una colección o tabla. usualmente corresponde a una única tabla o colección en la base de datos. Los modelos se definen creando un archivo en la carpeta o directorio api/models/

Utilizando los modelos

Accedemos a los modelos a través de los controladores, políticas, servicios, respuestas, tests y métodos personalizados. Existen varios métodos incorporados en sails que están disponibles en los modelos, los más importantes son los métodos denominados crud: find, create, update y destroy. Estos métodos son asíncronos, Waterline envía una petición y espera una respuesta.

Consecuentemente, los métodos de consulta devuelven un objeto deferred. Para ejecutar una consulta, llamamos al objeto .exec(cb), donde cb es la función callback que corre después de que la consulta se ha completado.

Waterline también incluye soporte para las promesas (promises). En lugar de llamar a la función .exec(), podemos ejecutar .then(), .spread() o .catch() esto nos devolverá una respuesta Bluebird promise

Métodos de modelo (estáticos o de  clase [statics or class])

Los métodos de clase son funciones integradas en el propio modelo, que realizan alguna tarea en particular en sus propios registros. Aquí es donde se encuentran los métodos CRUD que habitualmente utilizamos para atacar a la base de datos. .create(), .update(), .destroy(), .find(), etc.

Métodos de modelo personalizados

Waterline permite definir métodos personalizados en nuestros modelos. Esta característica se aprovecha de que los modelos en Waterline ignoran las keys que no reconoce, has de tener cuidado de no sobrescribir los métodos incorporados en sails (.create(), .find(), etc.) o buscadores dinámicos. Los métodos de modelo personalizados son útiles para extrapolar el código de un controlador que se relaciona con un modelo en particular, es decir permite sacar el codigo de los controladores en funciones reusables a los que podemos llamar desde cualquier sitio (con lo que no dependen de los objetos req o res).

Los métodos de modelo son generalmente funciones asíncronas. Por convección los métodos asíncronos deben ser funciones de tipo 2-ary, estos aceptan un objeto como primer argumento (usualmente llamados opts o options) y un callback como segundo argumento. También puedes optar por devolver una promesa. Ambas estrategias funcionan igual de bien, el utilizar una u otra es cuestión de preferencia, si no tienes preferencia sigue utilizando los callbacks.

Una buena práctica es escribir tu propio método de modelo estático, de modo que este pueda aceptar un registro o el valor de la clave principal (primaryKey). Para que los modelos operen en varios registros a la vez, debes pasarle una tabla de registros o un array de valores de clave principal.

Esto hace que nuestro método tenga más líneas de código de lo normal, pero a la vez, nos queda un método más robusto. Y ya que estas haciendo esto para extrapolar la lógica, vale la pena el esfuerzo.

Por ejemplo:

//  api/models/Monkey.js...

// Encuentra monos con el mismo nombre que la persona especificada


findWithSameNameAsPerson:
function (opts, cb) {

 var person = opts.person;

 /* Antes de nada, chekea si el valor es primary key o en su lugar fué pasado un objeto, si es así, entonces buscamos
  * la persona de la que estamos comparando con los monos
  */

  /* Función de auto-llamada */


 (function _lookupPersonIfNecessary(afterLookup){
   if (typeof person === 'object')) return afterLookup(null, person);
   Person.findOne(person).exec(afterLookup);
 })(function (err, person){
   if (err) return cb(err);
   if (!person) {
     err = new Error();
     err.message = require('util').format('No se han encontrado monos con el mismo nombre de persona w/ id=%s porque la persona no existe.', person);
     err.status = 404;
     return cb(err);
   }

   Monkey.findByName(person.name)
   .exec(function (err, monkeys){
     if (err) return cb(err);
     cb(null, monkeys);
   })
 });

}

Ahora podemos...

Monkey.findWithSameNameAsPerson(albus, function (err, monkeys) { ... });
// -o-
Monkey.findWithSameNameAsPerson(
37, function (err, monkeys) { ... });

Otro ejemplo:

// api/models/User.js
module.exports
= {

 attributes: {

   name: {
     type:
'string'
   },
   enrolledIn: {
     collection:
'Course', via: 'students'
   }
 },

 
/**
  * Registra a un usuario en uno o más cursos
  * @param  {Object}   options
  *            => courses {Array} list of course ids
  *            => id {Integer} id of the enrolling user
  * @param  {Function} cb
  */

 
enroll: function (options, cb) {

   User.findOne(options.id).exec(
function (err, theUser) {
     
if (err) return cb(err);
     
if (!theUser) return cb(new Error('User not found.'));
     theUser.enrolledIn.add(options.courses);
     theUser.save(cb);
   });
 }
};

Métodos de Atributo

Un método de atributo es una función que se encuentra disponible en los registros, podríamos decir que es una instancia del modelo. Por ejemplo, si tu quieres buscar o encontrar los diez estudiantes con mejor nota en el modelo Estudiante, cada uno de estos registros tendrá acceso a los métodos de atributos incorporados o que deseemos incorporar.

Construyendo métodos de atributos

Cada modelo incluye unos métodos por defecto, a saber:

Métodos de atributo personalizables

Los modelos de Waterlines, también permiten definir sus propios métodos de atributo personalizado, los definiremos como  cualquier otro atributo,  escribiendo una función al lado del nombre del atributo.

//  api/models/Person.js...

module.exports = {
 attributes: {
   // Primitive attributes
   firstName: {
     type: 'string',
     defaultsTo: ''
   },
   lastName: {
     type: 'string',
     defaultsTo: ''
   },

   // Associations (aka relational attributes)
   spouse: { model: 'Person' },
   pets: { collection: 'Pet' },

   // Attribute methods
   getFullName: function (){
     return this.firstName + ' ' + this.lastName;
   },
   isMarried: function () {
     return !!this.spouse;
   },
   isEligibleForSocialSecurity: function (){
     return this.age >= 65;
   },
   encryptPassword: function () {

   }
 }
};

Ten presente que a excepción de .save() y .destroy(), los demás atributos/método serán siempre síncronos por convección.

Cuando debemos escribir un atributo / método personalizado?

Los atributos/métodos son útiles a la hora de extraer información de un registro. Por ejemplo mirando el código de ejemplo anterior podríamos pedir información al registro de la siguiente manera:

if ( rick.isMarried() ) {
 
// ...
}

Cuando NO escribir un atributo / método personalizado ?

Evita escribir tus propios métodos de atributos asíncronos. A veces pueden tener consecuencias no deseadas y no es la forma más adecuada de construir tu aplicación.

Waterline Query Language (Lenguaje de consulta)

“Waterline Query Language”  es un lenguaje basado en objetos utilizados para recuperar los registros de cualquier adaptador (recuerda que los adaptadores son los encargados de mantener comunicación entre nuestra aplicación y las bases de datos MySQL, MongoDB, etc.).

Esto significa que la misma consulta que escribamos para MySQL nos va a servir tanto para MongoDB, como Redis, etc. Esto nos permitirá en un futuro cambiar de base de datos pero no el código de nuestra aplicación.

Lenguaje de consulta (Lo Básico)

El criterio de estos objetos se forma utilizando uno de los cuatro tipos clave del objeto. Estos son las claves de nivel superior utilizados en un objeto de consulta. Se basa libremente en los criterios que se utiliza en MongoDB, aunque con algunas variaciones.

Las consultas se pueden construir utilizando “WHERE”, también podremos usar las opciones “limit”, “skip” y “sort”. Si en la consulta omitimos el “WHERE”, el criterio que seguirá la consulta, será como si de un simple “WHERE” se tratase. Observa este ejemplo donde lo entenderás mejor.

Model.find({ where: { name: 'foo' }, skip: 20, limit: 10, sort: 'name DESC' });

// O

Model.find({ name:
'foo' });

Pares de claves : valor

El par clave/valor se usa para buscar registros con valores coincidentes exactamente con lo que se especifica. Este es el criterio que sigue el objeto “WHERE”, la clave representa el atributo del modelo y el valor es una estricta verificación de igualdad.

Model.find({ name: 'walter' })

También podemos usar varios atributos para realizar una búsqueda

Model.find({ name: 'walter', state: 'new mexico' })

Las consultas IN tiene una forma muy similar a las consultas in de MySQL, cada elemento del array será tratado como ‘OR

Model.find({
 name : [
'Walter', 'Skyler']
});

Las consultas NOT-IN  son similares a las “IN”, pero siguiendo otro criterio

Model.find({
 name : {
‘!’ :  ['Walter', 'Skyler'] }
});

Las consultas OR se usa mediante un array de pares clave/valor. Los resultados devueltos, serán según los criterios pasados en el array.

Model.find({
 
or : [
   { name:
'walter' },
   { occupation:
'teacher' }
 ]
});

Los siguientes modificadores, están disponibles para su uso a la hora de construir consultas.

‘<’        Menor Que

Busca registros donde el valor es menor que el valor especificado. Por ejemplo

Model.find({ age: { '<': 30 }})

‘<=’        Menor o Igual Que

Busca registros donde el valor es menor o igual que el valor especificado. Por ejemplo

Model.find({ age: { '<=': 21 }} )

‘>’        Mayor Que

Busca registros donde el valor es mayor que el valor especificado. Por ejemplo

Model.find({ age: { '>': 30 }} )

‘>=’        Menor o Igual Que

Busca registros donde el valor es mayor o igual que el valor especificado. Por ejemplo

Model.find({ age: { '>=': 21 }} )

‘!’        NOT

Busca registros donde los valores no son iguales al valor especificado. Por ejemplo

Model.find({ name: { '!': ‘foo’ }} )

‘contains’

Retorna registros donde los valores coincidan con el patrón especificado en cualquier lado de la cadena, ya sea al principio o al final. Por ejemplo

Model.find({ class: { 'contains': 'history' }})

// O lo que es lo mismo como:

Model.find({
class: { 'like': '%history%' }})

‘startsWith’

En esta ocasión sólo retornará los registros que coincidan con el valor especificado solamente al inicio de la cadena

Model.find({ class: { 'startsWith': 'american' }})

//  O lo que es lo mismo como:

Model.find({
class: { 'like': 'american%' }})

‘endsWith’

En esta ocasión sólo retornará los registros que coincidan con el valor especificado solamente al final de la cadena

Model.find({ class: { 'endsWith': 'can' }})

//  O lo que es lo mismo como:

Model.find({
class: { 'like': '%can' }})

‘Intervalo entre Fechas’

Puedes realizar busquedas entre fechas utilizando los operadores de comparación:

Model.find({ date: { '>': new Date('2/4/2014'), '<': new Date('2/7/2014') } })

Opciones de Consulta

Las opciones de consulta permiten afinar los resultados que se devuelven. Las opciones que tenemos disponibles para llevar a cabo esto son:

Model.find({ where: { name: 'foo' }, limit: 20 })

Model.find({ where: { name: 'foo' }, skip: 10 })

// Ordena por nombre (name) en orden ascendente
Model.find({ where: { name:
'foo' }, sort: 'name' });

// Ordena por nombre (name) en orden descendente
Model.find({ where: { name:
'foo' }, sort: 'name DESC' });

// Ordena por nombre (name) en orden ascendente
Model.find({ where: { name:
'foo' }, sort: 'name ASC' });

// Ordena por notación binaria
Model.find({ where: { name:
'foo' }, sort: { 'name': 1 }});

// Ordena por varios atributos
Model.find({ where: { name:
'foo' }, sort: { name:  1, age: 0 });

Cuidado! A la hora de realizar consultas (querys), ya que Waterline es Case-Sensitivity, es decir, que hace diferencia entre mayúsculas y minúsculas.

Validaciones

Sails soporta las validaciones automáticas para los atributos de nuestros modelos. Cada vez que se crea o actualiza un nuevo registro, se cotejan los datos de los atributos con las reglas de validación predefinidas. Esto proporciona un mecanismo de seguridad para las entradas no válidas a la base de datos de nuestra aplicación.

Reglas de validación

Las validaciones en sails son gestionadas por Anchor y Validator (https://github.com/chriso/validator.js), actualmente son las librerías de validación más robustas para Node.js. Sails soporta la mayoría de las validaciones disponibles en Validator (https://github.com/chriso/validator.js).

Nombre Validación

Datos a Comprobar

Notas de Uso

after

Comprueba si la fecha es posterior a la especificada

Debe ser una fecha válida para JavaScript

alpha

Comprueba si la cadena contiene solo letras (a-zA-Z)

alphadashed

Comprueba si la cadena contiene solo letras y/o guiones

alphanumeric

Comprueba si la cadena contiene letras y/o números

alphanumericdashed

Comprueba si la cadena contiene letras, números y/o guiones

array

Comprueba que es un array válido

Las cadenas de texto formateadas, no serán válidas

before

Comprueba si la fecha es anterior a la especificada

binary

Comprueba si los datos son binarios

Si es cadena de texto dará como válido.

boolean

Comprueba que es un boolean válido

Las cadenas de texto no serán válidos

creditcard

Comprueba los datos contienen el formato correcto de una tarjeta de crédito

date

Comprueba que es una fecha válida

datetime

Comprueba que la fecha y hora tengan el formato válido

decimal

Debe contener un decimal o ser menor de 1

email

Comprueba si contiene una dirección email correcta

empty

Todos los arrays, strings o argumentos de funciones con longitud de cero, serán considerados como “empty”

equals

Comprueba si el string es igual al que actualmente contiene el registro

Ambos deben coincidir en tipo y valor

finite

Comprueba que el valor dado es o puede ser un número finito

float

Comprueba si es un número de coma flotante

hexadecimal

Comprueba que es número hexadecimal

hexcolor

Comprueba que es un color hexadecimal

int

Comprueba si es un entero (integer)

integer

Más de lo mismo

ip

Comprueba que el string contiene el formato correcto de una IP (v4 o v6)

ipv4

Comprueba que el string contiene el formato correcto de una IP v4

ipv6

Comprueba que el string contiene el formato correcto de una IP v6

json

Realiza un try/catch para comprobar que es un JSON válido

lowercase

Comprueba que el string contenga todos los caracteres están en minúscula

numeric

Comprueba que solo contenga números

uppercase

Comprueba que el string contenga todos los caracteres están en mayúscula

url

Comprueba que tenga el formato correcto de una url

Reglas de validaciones personalizadas

Podemos definir nuestras propias validaciones. Es posible acceder y comparar valores de otros atributos con la palabra reservada “this”. Esto nos permite mover la lógica de validación a nuestros modelos y mantenerla fuera de la lógica del controlador.

Ejemplo de un modelo

// api/models/foo
module.exports
= {

 types: {
   
is_point: function(geoLocation){
     
return geoLocation.x && geoLocation.y
   },
   
password: function(password) {
     
return password === this.passwordConfirmation;
   }
 },
 attributes: {
   firstName: {
     type:
'string',
     required:
true,
     minLength:
5,
     maxLength:
15
   },
   location: {
     
//note, that the base type (json) still has to be defined
     type:
'json',
     is_point:
true
   },
   password: {
     type:
'string',
     password:
true
   },
   passwordConfirmation: {
     type:
'string'
   }

 }
}

De forma nativa, sails no ofrece soporte para la personalización de mensajes de validación. Pero si dispone de una librería “externa” que podemos instalar en nuestra aplicación y podemos personalizar los mensajes de validación.  https://github.com/lykmapipo/sails-hook-validation

Configurando el / los modelo/s

Las siguientes propiedades que veremos a continuación, se pueden configurar para todos los modelos desde el archivo config/models.js, así y todo si en algún modelo en particular quieres otro tipo de configuración para dicho modelo, se puede sobrescribir.

migrate

migrate: 'safe'

Esta opción controla si Sails ha de reconstruir automáticamente las tablas, colecciones, juegos de datos, etc. a partir de un esquema.

En un entorno producción, Sails utilizará siempre migrate: “safe”, para proteger que accidentalmente puedan ser eliminados algunos datos, sin embargo, mientras estés en un entorno de desarrollo tienes otras opciones para mayor comodidad a la hora de desarrollar tu aplicación.

Cuando levantamos el servidor (Sails), Waterline válida todos los datos de la base de datos. Esta opción (migrate) le dice a Waterline que hacer cuando los datos están corruptos. Si la opción migrate la establecemos a “safe”, ignorará los datos corruptos y el servidor seguirá corriendo con total normalidad. También puedes establecer otras alternativas como:

Estrategia Auto-Migración

Descripción

safe

Nunca auto-migra la bbdd

alter

Auto-migra columnas y campos, pero mantendrá datos existentes

drop

Limpia o borra todos los datos cada vez que Sails se inicia.

Cuidado!, si usas drop o alter, cabe la posibilidad de que pierdas los datos. Nunca utilices drop o  alter si tu aplicación está corriendo en un entorno de producción.

schema (esquema)

schema: true

Si schema lo establecemos a false, podremos almacenar datos en la bbdd, si que en el modelo exista el atributo al que se refiere en la base de datos. En caso contrario si schema lo establecemos a true, sólo los atributos que tengamos definidos en el modelo se guardarán en la base de datos.

Para adaptadores como MongoDB o Redis, que no requieren un esquema (schema) para trabajar, la configuración predeterminada será:  schema: false

connection (conexión)

connection: ‘postgresql-local’

Configuramos el adaptador al que se tiene que conectar para recuperar y guardar datos. Por defecto, Sails viene configurado para la conexión del adaptador localDiskDb.

identity (identificar)

identity: ‘purchase’

De forma predeterminada, se identificará a un modelo automáticamente por su nombre de archivo en minúsculas. ej. user.

Nunca debemos cambiar el valor de esta propiedad en nuestros modelos.

globalId

globalId: ‘Purchase’

Cambia el nombre de tu modelo por el que quieras acceder de forma global, es decir desde cualquier sitio (siempre y cuando la globalización de  los modelos esté activada sails.config.globals). No cambies nunca el valor de esta propiedad en tus modelos individuales. Para desactivar esta propiedad, indicalo en sails.config.globals.

autoPK

autoPK: true

Permite cambiar la definición de una clave principal (primary key) en un modelo. Mysql usa por defecto un entero auto-incremental para la clave principal, mientras que MongoDB lo hace en un string (cadena de texto) UUID. En cualquiera de los casos, la clave principal se generará por autoPK y lo asignará como único. Si establecemos autoPK a false, no se creará de forma automática una clave principal y deberás definirla tu mismo en el modelo de tal manera que se vea algo parecido a esto:

attributes: {
 sku: {
   type:
'string',
   primaryKey:
true,
   unique:
true
 }
}

autoCreatedAt

autoCreatedAt: true

Por defecto al insertar un nuevo registro en la base de datos se crea el campo createdAt para almacenar la fecha en la que se insertó el registro, podemos desactivar esta opción globalmente (estableciendolo a false) y colocar este código en el modelo que queramos para conseguir que este campo se guarde en la base de datos:

attributes: {
 createdAt: {
   type:
'datetime',
   
defaultsTo: function (){ return new Date(); }
 }
}

autoUpdatedAt

autoUpdateAt: true

Por defecto al insertar un nuevo registro en la base de datos se crea el campo updatedAt para almacenar la fecha en la que se modificó un registro, podemos desactivar esta opción globalmente (estableciendolo a false) y colocar este código en el modelo que queramos para conseguir que este campo se guarde en la base de datos:

attributes: {
 updateAt: {
   type:
'datetime',
   
defaultsTo: function (){ return new Date(); }
 }
}

tableName

tableName: ‘mi_tabla’

Puedes definir un nombre de tabla o colección para que se añada al adaptador que tengas configurado. En MySQL, PostgreSQL, Oracle, etc, (nos referimos a nombres de tabla, mientras que en MongoDB o Redis, nos referimos a colección). Si no estableces un nombre de tabla (tableName), Waterline asignará un nombre de tabla automáticamente estableciendo lo de la manera que esté configurado en identy 

attributes

attributes: {
 name: { type:
'string' },
 email: { type:
'email' },
 age: { type:
'integer' }
}

Para saber más dirígete a la sección de atributos de modelo.

9. Vistas

Visión de Conjunto

En Sails, las vistas son plantillas que se compilan en el servidor como páginas HTML. En la mayoría de los casos, las vistas son usadas como la respuesta a una petición HTTP.

Alternativamente una vista puede ser compilada en la parte del backend, directamente en una cadena de texto (string). usando sails.renderView(). Por ejemplo, puedes utilizar este método para enviar mensajes de correo electrónico con formato HTML o incluso para construir XML.

Creando una vista

Por defecto Sails viene configurado con el motor de plantillas EJS. La sintaxis para EJS es bastante convencional, si has escrito código php, asp, erb, jsp, etc. sabrás en todo momento lo que estás haciendo.

Si prefieres trabajar con otro motor de plantillas diferente a EJS, tienes varias opciones para el que Sails da soporta. Sails soporta todos los motores de plantilla compatibles con Express.

Las vistas se definen en nuestra carpeta views/, si tu aplicación no ha de servir páginas HTML dinámicas, es decir, si estás construyendo una API para una app móvil, entonces deberías eliminar esta carpeta de tu aplicación.

Compilando una vista

En cualquier lugar se puede acceder al objeto res (en una acción de controlador, una respuesta personalizada o política), puedemos usar res.view para compilar nuestras vistas, y enviar el código HTML al usuario.

Como vimos en la sección de rutas, podemos conectar una vista directamente a una ruta en nuestro archivo routes.js. Indicando la ruta relativa de nuestra vista desde el directorio de nuestra aplicación views/. Por ejemplo:

{
 
'get /': {
   view:
'homepage'
 },
 
'get /signup': {
   view:
'signupFlow/basicInfo'
 },
 
'get /signup/password': {
   view:
'signupFlow/chooseAPassword'
 },
 
// etc.
}

Aplicaciones de una sola página

Si estás construyendo una aplicación para el navegador, parte de la navegación o toda, puede tener lugar en el lado del cliente (frontend), es decir, en lugar de que el navegador salte a una nueva página HTML cada vez que el usuario solicite alguna acción, el código del lado del cliente (frontend) precarga una plantilla que luego muestra en el navegador sin necesidad de acceder al servidor directamente.

En este caso tenemos dos opciones para crear una aplicación de una sola página:

Plantillas Layouts

Cuando construimos  una aplicación con diferentes páginas, puede ser útil el poder compartir archivos HTML en una plantilla (layout). Esto reduce el tamaño del código en nuestro proyecto y te ayuda a evitar los mismos cambios en varios archivos en la que tengas que repetir código (como podría ser el header o el footer de una aplicación).

En Sails y Express, las plantillas están implementados en los motores de vistas. Aunque, cada uno de ellos tiene su propia sintaxis.

Por conveniencia, Sail soporta las plantillas cuando usamos por defecto el motor de vistas que viene integrado, EJS. Si quieres utilizar otro motor de vistas para tus plantillas, solamente deberás instalarlo, vía npm install y cambiarlo en la configuración. Pero es algo que veremos en unos instantes donde veremos los motores de vistas que actualmente soporta Sails y puedas encuentrar el que mejor se te adapte.

Creando Plantillas

En Sails las plantillas son archivos con extensión .ejs que residen en la carpeta views/ de nuestra aplicación. La plantilla normalmente contiene !DOCTYPE html <html><head>... … </head> <body> … … </body> </html>. Por lo que un archivo de una vista incluiría <%- body  %>.  Los layouts nunca se sirven sin una vista.

Por defecto, Sails compila todas las vistas en la plantilla (layout) que está localizada en views/layout.ejs.

Locals

Las variables que son accesibles desde la vista se les llama locals. Estas representan los datos recibidos desde el lado del servidor (backend), estas tampoco se pueden incluir en tu archivo HTML con la sintaxis propia de un archivo HTML, usan una sintaxis especial que provee el motor de vistas que escojas.

<div>Logged in as <a><%= name %></a>.</div>

Usando locals en nuestras vistas

Como hemos dicho, la notación para acceder a locals depende del motor de vistas que utilices. En EJS usamos una etiqueta especial para incluir locals en nuestra vista (Ej. <%= algunValor %>)

Hay tres tipos de etiquetas en una plantilla EJS:

Un ejemplo de una vista usando dos locals, “user” y “corndog” :

<div>
 
<h1><%= user.name %>'s first view</h1>
 
<h2>My corndog collection:</h2>
 
<ul>
   
<% _.each(corndogs, function (corndog) { %>
   
<li><%= corndog.name %></li>
   
<% }) %>
 
</ul>
</div>

Si los datos que quieres transmitir a la vista son estáticos, no es necesario recurrir a un controlador, con lo que introduciremos locals en la ruta directamente. Recuerda que las rutas se encuentran en el archivo config/routes.js. Observa un ejemplo:

 

// ...
 
'get /profile': {
   view:
'backOffice/profile',
   locals: {
     user: {
       name:
'Frank',
       emailAddress:
'frank@enfurter.com'
     },
     corndogs: [
       { name:
'beef corndog' },
       { name:
'chicken corndog' },
       { name:
'soy corndog' }
     ]
   }
 },
 
// ...

Por otra parte en la mayoría de los casos, los datos recibidos serán dinámicos, con lo que necesitaremos de un controlador que se encargue de gestionar los modelos para recuperar algunos datos de la base de datos y los pase a la vista por medio del método res.view.

Asumiendo que en la ruta la hemos asociado a una acción de controlador y que los modelos están definidos, podríamos enviar a la vista algo como esto:

// en api/controllers/UserController.js...

 
profile: function (req, res) {
   
// ...
   
return res.view('backOffice/profile', {
     user: theUser,
     corndogs: theUser.corndogCollection
   });
 },
 
// ...

Partials

Sails usa ejs-locals en las vistas para renderizar código, de la siguiente manera :

<%- partial ('foo.ejs') %>

Renderiza un partial (Trozo de código) que está ubicado en la carpeta /views/foo.ejs. Esta etiqueta es como si hiciésemos un include (incluye el archivo en la vista)

Las rutas que establezcas serán relativas a la vista que está cargando el partial (Trozo de código). Así que si tienes una vista de un usuario en /views/users/view.ejs y quieres cargar un archivo en /views/partials/widget.ejs, entonces deberás usar :

<%- partial ('../partials/widget.ejs') %>

Algo a tener en cuenta es que partials se renderizan de forma sincrónica, con lo que Sails quedará parado o bloqueado hasta que la petición haya terminado de cargar. Es algo que has de tener en cuenta cuando estás creando tu aplicación, especialmente si esperas un gran número de peticiones.

Motor de vistas

Como hemos comentado anteriormente, por defecto Sails utiliza el motor de vistas EJS.

Cambiar el motor de vistas

Para usar un motor de vistas diferente, instalaremos mediante npm el que escojamos para usar en nuestro proyecto, y en el archivo config/views.js establecemos el nombre de este.

Por ejemplo si el que escogemos es jade, ejecutamos npm install jade --save-dev, y establecemos (en sails.config.views.engine)  engine: ‘jade’

Motores de vistas soportados

10. Servicios

Visión de conjunto

Los servicios son considerados como librerías o bibliotecas que contienen funciones que usaremos en varios lugares de nuestra aplicación. Por ejemplo, tú puedes tener un servicio de envío de correos electrónicos en los que en varios sitios de tu aplicación necesitarás hacer uso de este servicio. El mayor beneficio de usar servicios es que están globalizados, no necesitas hacer un require() para acceder a ellos.

Como se crea un servicio

Simplemente crea un archivo javascript que contenga una función u objeto dentro de la carpeta api/services. El nombre del archivo es el que se utiliza para acceder al servicio globalmente. Mira este ejemplo para entenderlo mejor:

// EmailService.js - en la carpeta api/services
module.exports
= {

   
sendInviteEmail: function(options) {

       
var opts = {"type":"messages","call":"send","message":
           {
               
"subject": "YourIn!",
               
"from_email": "info@balderdash.co",
               
"from_name": "AmazingStartupApp",
               
"to":[
                   {"email": options.email,
"name": options.name}
               ],
               
"text": "Dear "+options.name+",\nYou're in the Beta! Click <insert link> to verify your account"
           }
       };

       myEmailSendingLibrary.send(opts);

   }
};

Para acceder a este servicio desde un controlador, lo haríamos de la siguiente manera:

// En algún controlador
 EmailService.sendInviteEmail({email:
'test@test.com', name: 'test'});

11. Seguridad

Visión de conjunto

Sails y Express proporcionan protección para nuestras aplicaciones relativamente fácil de configurar, contra los tipos más conocidos de ataques que existen a nivel de aplicación web.

Cross-Origin Resource Sharing (CORS)

CORS es un mecanismo que permite que muchos recursos (fuentes, javascript, etc) de una página web puedan hacer peticiones a otro dominio diferente del dominio desde el que se originó el recurso. Por ejemplo, http://miotrositio.com hace una petición a http://api.misitio.com. Al igual que con JSONP, el objetivo de CORS es funcionar como un método seguro para eludir la política de que las peticiones siempre vengan del mismo origen, permitiendo a Sails responder con éxito a las peticiones provenientes de otros dominios. Pero a diferencia de JSONP, podemos hacer uso de peticiones como POST, PUT, DELETE, etc.

Sails puede configurarse para permitir peticiones cross-origin a partir de una lista de dominios o desde cualquier dominio. Lo podemos hacer a nivel de rutas o globalmente para toda la aplicación.

Activando CORS

Por razones de seguridad, CORS viene desactivado por defecto. Pero activarlo es muy simple.

Para permitir peticiones cross-origin desde cualquier dominio a cualquier ruta de nuestra aplicación solo debemos establecer allRoutes (config/cors.js) a true.

allRoutes: true

Configurando CORS para rutas individuales

Además de poder configurar CORS de forma global, también podemos establecer rutas que podrán ser accedidas desde otros dominios.

"get /foo": {
  controller:
"FooController",
  action:
"index",
  cors:
true
}

De la misma manera que activamos cors en la ruta, también podemos desactivar cors en una ruta. Podríamos tener en la configuración global allRoutes : true y en el archivo de configuración de rutas desactivar aquellas que no queramos que sean accedidas desde otros dominios.

"get /foo": {
  controller:
"FooController",
  action:
"index",
  cors:
false
}

Para anular los parámetros específicos de configuración global de CORS  en una ruta, añadimos un objeto a la propiedad cors :

"get /foo": {
  controller:
"FooController",
  action:
"index",
  cors: {
    origin:
"http://sailsjs.org, http://sailsjs.com",
    credentials:
false
  }
}

        

        Nivel de seguridad

Por defecto, sails procesa todas las peticiones que le llegan independientemente del dominio, le basta con establecer en las cabeceras la respuesta correspondiente para que el cliente pueda decidir si mostrar o no la respuesta. Por ejemplo, si enviamos una petición GET a una dirección /foo/bar desde un dominio que no está definido en nuestra lista CORS, la acción bar de nuestro controlador FooController.js se ejecutará, pero el navegador rechazará el resultado.

Esto puede parecer contradictorio, pero esto es importante porque permite a los clientes que no están basados en navegadores, como pueden ser Postman o Curl seguir trabajando sin bloquearlos.

Si lo que quieres es evitar que sails permita por completo las peticiones de dominios no permitidos, podemos utilizar el ajuste securityLevel.

module.exports.cors = {
 allRoutes:
true,
 origin:
"http://sailsjs.org",
 securityLevel:
1
}

Establecer el nivel de seguridad a  1  hará que el código de estado de respuesta a la petición sea un 403, para cualquier petición que provenga de un origen que no esté permitido, tanto si es HTTP o HTTPS.

Un nivel de seguridad establecido a 2 es lo mismo, pero extendido a todos los protocolos, incluidos PostMan o Curl.

Seguridad: CRSF

El CSRF (del inglés Cross-site request forgery o falsificación de petición en sitios cruzados) es un tipo de exploit malicioso de un sitio web en el que comandos no autorizados son transmitidos por un usuario en el cual el sitio web confía.

Es un tipo de ataque que obliga al usuario final a ejecutar acciones no deseadas en el servidor, en nombre del usuario autenticado. En otras palabras, sin protección, las cookies almacenadas en Google Chrome se pueden utilizar para enviar peticiones al servidor desde el ordenador del usuario que está visitando la página con esta vulnerabilidad.

Activando la protección CRSF

Para activar en nuestra aplicación la protección CRSF, basta con que establezcamos a true la propiedad csrf en nuestro archivo config/csrf.js.

csrf: true

Ten en cuenta que si ya tienes código escrito con el que se comunica vía POST, PUT o DELETE con el backend de tu aplicación, será necesario adecuar tu código para que esta protección funcione correctamente, así como incluir el token CSRF en la cabecera de las peticiones.

Tokens CRSF 

La mayoría de las aplicaciones en Node.js, Sails o Express son compatibles con conectores de protección CSRF para protegerse de estos ataques. Este middleware implementa un patrón de sincronización de Tokens. Cuando activamos la protección CSRF, todas las peticiones a excepción de las GET, van acompañadas de un token especial, identificado en la cabecera, en un parámetro de la consulta o en el body HTTP.

Los tokens CSRF son sesiones específicas temporales. Por ejemplo, imagina a Jordi y Laura, ambos son dos usuarios que están realizando compras en nuestro sitio que es un comercio electrónico y este está siendo ejecutado en Sails. La protección CSRF también está habilitada. Digamos que el Lunes tanto Jordi como Laura hacen sus compras. Para ello nuestro sitio tiene que enviar al menos dos tokens CSRF diferentes, uno para Jordi y otro para Laura. A partir de aquí, cualquier petición que reciba nuestro servidor, donde el token sea nulo o incorrecto, este la rechaza. Así ahora podemos estar seguros de que cuando Laura se marche fuera de nuestro sitio a visitar otro, este otro no pueda engañar al navegador para enviar peticiones maliciosas a nuestro sitio utilizando las cookies.

Obteniendo Tokens CRSF 

Para obtener un token CSFR, debe ser iniciada en tu vista usando locals (Es una buena idea usar locals para aplicaciones web con varias páginas) o bien mediante sockets o AJAX.

Usando locals en las vistas

Para tener el CSRF en nuestra página: <%= _csrf %>

<form action="/signup" method="POST">
  <input type="text" name="emailaddress"/>
  <input type='hidden' name='_csrf' value='<%= _csrf %>'>
  <input type='submit'>
</form>

Seguridad: Clickjacking

El Clickjacking, o Secuestro del clic, es una técnica maliciosa para engañar a usuarios de Internet con el fin de que revelen información confidencial o tomar control de su computadora cuando hacen clic en páginas web aparentemente inocentes. En uno de los muchos navegadores o plataformas con alguna vulnerabilidad, un ataque de clickjacking puede tomar la forma de código embebido o script que se ejecuta sin el conocimiento del usuario; por ejemplo, aparentando ser un botón para realizar otra función.

X-FRAME-OPTIONS

Una manera simple de prevenir este tipo de ataque, es activar X-FRAME-OPTIONS en la cabecera.

Usando lusca

lusca es una libreria open-source bajo licencia Apache

# Instalamos lusca en nuestra aplicación
npm install lusca
--save

Una vez instalado, configuramos este middleware en config/http.js 

// ...
 xframe: require(
'lusca').xframe('SAMEORIGIN'),
 
// ...
 order: [
   
// ...
   
'xframe'
   
// ...
 ]

Seguridad: P3P

La Plataforma de Preferencias de Privacidad (Platform for Privacy Preferences) o P3P es un protocolo que permite a los sitios Web declarar las intenciones de uso de la información recopilada sobre los usuarios que lo visitan y dar de esta forma un mayor control a éstos sobre su información personal cuando navegan.

Afortunadamente, existen varios módulos que dan soporte P3P a Express y Sails. Para utilizar uno de estos módulos y manejar las cabeceras, instalamos usando npm install p3p --save, seguidamente abrimos nuestro archivo config/http.js de nuestro proyecto y lo configuramos como un middleware personalizado. Algo parecido a esto:

// .....
module.exports.http
= {

 middleware: {

   p3p: require(
'p3p')(p3p.recommended), // <==== establecemos nuestro middleware personalizdo  "p3p"

   order: [
     
'startRequestTimer',
     
'p3p', // <============ configuramos el orden de nuestro middleware "p3p"
     
'cookieParser',
     
'session',
     
'bodyParser',
     
'handleBodyParserError',
     
'compress',
     
'methodOverride',
     
'poweredBy',
     
'$custom',
     
'router',
     
'www',
     
'favicon',
     
'404',
     
'500'
   ],
   
// .....
 }
};

12. Internacionalización

Visión de Conjunto

Si tu aplicación va a llegar a usuarios de distintos países y diferentes idiomas,  la internacionalización y localización (i18n) va a ser una parte importante a tener en cuenta. Sails provee soporte para detectar las preferencias del lenguaje del usuario, traduciendo palabras o sentencias, gracias a i18n-node. (npm).

Cómo usarlo

En la  vista: 

<h1> <%= __('Hello') %> </h1>
<h1> <%= __('Hello %s, how are you today?', 'Mike') %> </h1>
<p> <%= i18n('That\'s right-- you can use either i18n() or __()') %> </p>

En la controlador:

req.__('Hello'); // => Hola
req.__(
'Hello %s', 'Marcus'); // => Hola Marcus
req.__(
'Hello {{name}}', { name: 'Marcus' }); // => Hola Marcus

O si conoces el identificador de configuración regional, puedes traducir desde cualquier parte de la aplicación utilizando:

sails.__({
 phrase:
'Hello',
 locale:
'es'
});
// => 'Hola!'

Locales

i18n lee archivos JSON-formateado desde el directorio “locales” (config/locales). Cada archivo corresponde con una configuración regional (por lo general un idioma).

Estos archivos contienen cadenas (como JSON pares clave-valor) en los que podemos usar tanto en las vistas como en controladores.

Un ejemplo de un archivo locale (config/locales/es.json):

{
   
"Hello!": "Hola!",
   
"Hello %s, how are you today?": "¿Hola %s, como estas?",
}

Algo a tener en cuenta cuando edites o crees un archivo de traducción es vigilar las mayúsculas y minúsculas ya que Sails requiere coincidencias exactas para acceder.

Detectar o anular la preferencia del idioma

Para anular la preferencia del idioma detectada automáticamente en una solicitud o petición (request), usa req.setLocale(), como por ejemplo:

// Forzar el idioma Alemán:
req.setLocale('de');
// (Esto hará que se use las cadenas de texto localizadas en  `config/locales/de.json` para la traducción)

Por defecto node-i18n detectará el idioma deseado examinando la cabecera de la petición (request).

En la configuración del navegador (del usuario) se establece el idioma, que es pasado a través de la petición al servidor en la cabecera, con ello sails automáticamente seleccionará el archivo locale para servir al usuario el idioma predefinido, aunque si por algún motivo deseas cambiar o anular esta configuración regional detectada, puedes proporcionar la tuya propia.

Por ejemplo, si tu aplicación permite que los usuarios elegir el idioma, puedes crear una regla o política (policy) que compruebe el lenguaje que el usuario haya personalizado y si existe, establece la configuración regional apropiada para su uso en todas las peticiones tanto para las acciones del controlador como las vistas.

// api/policies/localize.js
module.exports = function(req, res, next) {
 req.setLocale(req.session.languagePreference);
 next();
};

 

// config/routes.js

module.exports = { 

    ’/:lang/’: ‘MiControlador.index’,

    ‘/:lang/’: ‘MiControlador.ayuda’,

    ‘/:lang/’: ‘MiControlador.contact’,

    // … etc ...

}

// config/policies.js

module.exports = { ‘*’ : ‘localize} 

// api/policies/localize.js

module.exports =  locale(req, res, next) { 

     req.setLocale(req.param(‘lang’));

      next();

};

13. Políticas (Policies)

Visión de Conjunto

Las políticas en Sails son herramientas versátiles para autorizar, permitir o denegar el acceso a los controladores. Por ejemplo si estás construyendo una aplicación parecida a Dropbox, antes de permitir que un usuario pueda cargar un archivo en una carpeta, se deberá chequear si el usuario está autentificado, también deberá comprobar si la carpeta tiene permisos de escritura. Finalmente también se comprobará si la carpeta dispone de espacio suficiente para cargar el archivo.

Las políticas pueden ser utilizadas para: HTTP BasicAuth, 3rd party single-sing-on, OAuth 2.0 o para una autorización / autenticación personalizada.

NOTA:  Las políticas solamente se aplican a controladores y acciones, nunca a las vistas. Si defines una ruta en tu archivo routes.js que apunte hacia una vista, no podrás aplicarle una política.

Escribiendo nuestra primera política

Las políticas se definen en la carpeta api/policies. Cada política sólo podrá contener una función.

Las políticas son en realidad una función middleware (Connect/Express) que corre antes que los controladores. Pueden encadenarse como quieras, de hecho está pensado para utilizarse así. Lo ideal es que cada función middleware compruebe una sola cosa.

Por ejemplo, la política mencionada anteriormente (Comprobar si una carpeta tiene permisos de escritura) podría tener este aspecto:

// policies/canWrite.js
module.exports
= function canWrite (req, res, next) {
 
var targetFolderId = req.param('id');
 
var userId = req.session.user.id;

 Permission
 .findOneByFolderId( targetFolderId )
 .exec(
function foundPermission (err, permission) {

   
// Error inesperado-- salta al error por defecto (500)
   
if (err) return next(err);

   
// No tiene permiso para esta carpeta.
   
if ( ! permission ) return res.redirect('/notAllowed');

   
// Bien, se encontró un permiso. Pero realmente el permiso le permite escribir?.
   
if ( permission.type !== 'write' ) return res.redirect('/notAllowed');

   
// Si ha llegado hasta aquí, parece que todo está bien, así que dejamos que el usuario acceda a la carpeta.
   next();
 });
};

Protegiendo Controladores con Políticas

Sails ha diseñado un ACL (Lista de Control de Acceso) ubicado en config/policies.js. Este archivo se usa para asignar políticas a controladores.

Este archivo es declarativo, lo que significa que describe los permisos de tu aplicación, no la forma en que deben trabajar. Además de hacernos la vida más fácil a los desarrolladores, ya que se entiende bastante mejor lo que está pasando en nuestra aplicación, también hacemos una aplicación más flexible.

 

Tu archivo config/policies.js exporta un objeto, con claves que son los nombres de los controladores (también podemos usar ‘*’, para que se aplique a todos los controladores) y los valores, el nombre de las acciones que hacen referencia a una o más políticas, observa los siguientes ejemplos para hacerte una idea mejor.

// Aplicamos la política 'isLoggedIn' a la acción 'edit' del controlador 'ProfileController'                                                          // También aplicamos las políticas ‘isAdmin’ y ‘isLoggedIn a la acción create del controlador ‘ProfileController’

{
 ProfileController: {
     edit:
'isLoggedIn'
   
     create: [
'isAdmin', 'isLoggedIn']
 }
}

// Aplicamos la política 'isLoggedIn' a todas las acciones del controlador 'ProfileController'                                                          // Aplicamos las políticas ‘isAdmin’ y ‘isLoggedIn a la acción edit del controlador ‘ProfileController’

{
 ProfileController: {
     
'*': 'isLoggedIn'
   
     edit: [
'isAdmin', 'isLoggedIn']
 }
}

// Aplicamos la política 'isLoggedIn' a todas las acciones de todos los controladores por defecto                                                      

// Aplicamos la política ‘isAdmin’ a la acción foo del controlador ‘ProfileController’

{
 
'*': 'isLoggedIn',
 ProfileController: {
     
'foo': 'isAdmin'
 }
}

Sails proporciona dos políticas integradas que se pueden aplicar a nivel global de toda la aplicación o a un controlador o acción específico.

Sails por defecto viene configurado [ ‘*’: true ] permitiendo el acceso a todos los controladores y acciones. En producción no es una buena práctica dejarlo como esta, debes establecer el valor a false para evitar el acceso a controladores o acciones que no deberían estar expuestas a cualquiera.

Algunos otros casos en los que las políticas pueden ayudarnos al desarrollo de nuestra aplicación son:

14. Middleware

Sails es completamente compatible con Express, de hecho, casi todo el código que escribimos en Sails es middleware, sobre todo las acciones de los controladores y políticas (policies) de la aplicación.

HTTP Middleware

Sails utiliza un adicional middleware configurable sólo para el manejo de las peticiones HTTP. Cada vez que tu aplicación recibe una petición HTTP la configuración HTTP Middleware se ejecuta en el orden que veremos a continuación.

Convencionales Predeterminados

Sails cuenta con un conjunto de middleware HTTP convencionales listo para usarse. Donde por supuesto podemos desactivar, sobreescribir, reordenar y añadir, aunque lo que viene por defecto es bastante aceptable para la mayoría de las aplicaciones, tanto en desarrollo como en producción. Abajo mostramos una lista de funciones HTTP Middleware que vienen con Sails en el orden en que se ejecutan cada vez que el servidor recibe una petición HTTP.

key HTTP Middleware

Descripción

startRequestTimer

Asigna una variable en la memoria para mantener fecha y hora desde que la petición se inició. Podemos acceder y utilizarlo en nuestra aplicación para proporcionar información sobre las peticiones que son lentas.

cookieParser *

Analiza la cookie de la cabecera en un objeto, para su uso posterior en alguna parte del código de nuestra  aplicación

session *

Establece un objeto de sesión único, usando la  configuración de sesión 

bodyParser

Analiza parámetros del cuerpo (body) de la petición HTTP, para usarlos.

compress

Comprime los datos de la respuesta usando gzip

methodOverride

Proporciona soporte, imitando el método HTTP, esto permite usar los verbos HTTP como PUT o DELETE en lugares donde el navegador del cliente no lo soporta (ej. versiones anteriores de Internet Explorer). Si la petición contiene el método (_method) establecido en PUT, la peticición es ruteada como si fuera una petición PUT adecuada. Para más información quizás quieras dar un vistazo a la documentación Connect's methodOverride

poweredBy

Adjunta X-Powered-By a las cabecera de la respuesta.

$custom

Proporciona compatibilidad de opciones de configuración de versiones anteriores de Sails v0.9.x hasta Sails v0.10 ofreciendo mucha más flexibilidad a la hora de configurar HTTP Middleware, siempre y cuando no estés utilizando sails.config.express.customMiddleware, en ese caso puedes quitar este elemento con total seguridad de la lista

router *

Aquí es donde ocurrirá la mayor parte de la lógica de tu aplicación en la que se aplicará a cualquier petición. Además corren antes que algunos procesos internos de Sails, estas peticiones de rutas utilizan las rutas explícitas de nuestra aplicación (sails.config.routes) y/o las rutas blueprint

www *

Sirve archivos estáticos como por ejemplo imágenes, hojas de estilo, scripts, … en la carpeta “public” (configurada en sails.config.paths y si no lo tienes configurado convencionalmente en .tmp/public/ ) usando Connect’s static middleware

favicon

Sirve al navegador el icono que vemos en las pestañas, este icono al que llamamos favicon está ubicado en /assets/favicon.ico

404 *

Maneja las peticiones que no coinciden con ninguna ruta, ejecutando res.notFound()

500 *

Maneja las peticiones que desencadenan en error interno del servidor, como por ejemplo una llamada de Express next(err), ejecutando res.serverError()

Leyenda:

              estás haciendo.

Añadir o Anular un HTTP Middleware

Para construir una función HTTP Middleware personalizada, definimos una nueva key (clave) sails.config.http.middleware.foobar y lo añadimos en la función de configuración middleware, a continuación añadimos el nombre como cadena de texto (string) al array sails.config.http.middleware.order justo donde deseemos que ejecute, recuerda que tal y como están en la lista se ejecutan ordenadamente uno después del otro. Una buena ubicación para ponerla podría ser justo después de “cookieParser”. A continuación un ejemplo de lo dicho hasta ahora del archivo config/http.js :

// ...
 middleware: {

   
// Definimos una función HTTP middleware personalalizada con el nombre de la key (clave) `foobar`:
   
foobar: function (req,res,next) { /*...*/ next(); },

   
// Definimos un par de funciones HTTP middleware con nombres de keys `passportInit` y `passportSession`
   
// (Aquí utilizamos una librería middleware existente de npm)
   passportInit    : require(
'passport').initialize(),
   passportSession : require(
'passport').session(),

   
// Reemplazamos la función convencional cookie parser:
   
cookieParser: function (req, res, next) { /*...*/ next(); },


   
// Ahora configuramos ordenadamente nuestros HTTP middleware
   order: [
     
'startRequestTimer',
     
'cookieParser',
     
'session',
     
'passportInit',                // <==== passport HTTP middleware este corre después de  "session"
     
'passportSession',         // <==== (Documentación en  https://github.com/jaredhanson/passport#middleware)
     
'bodyParser',
     
'compress',
     
'foobar',                        // <==== nuestro HTTP middleware personalizado
     
'methodOverride',
     
'poweredBy',
     
'$custom',
     
'router',
     
'www',
     
'favicon',
     
'404',
     
'500'
   ]
 },

 
customMiddleware: function(app){
   
// destinado a otro middleware que no siga la convención  'app.use(middleware)'
    require(
'other-middleware').initialize(app);
 }
 
// ...

Middleware Express en Sails

Una de las mejores cosas que tienen las aplicaciones con Sails, es que pueden aprovechar la ventaja de utilizar el middleware Express/Connect. Pero surge una pregunta muy común cuando se trata de hacer uso, qué es:

¿Donde hago uso de app.use()?

En la mayoría de los casos, la respuesta a esta pregunta es instalar el middleware Express como un middleware HTTP personalizado en sails.config.http.middleware. Esto dará lugar a que todas las peticiones HTTP de nuestra aplicación, se les permita configurar el orden en que se ejecutan en relación con otro middleware HTTP.

Express Routing en Sails

También podemos incluir Express middleware como una política o regla, configurandolo en config/policies.js. Podemos requerir y configurar el middleware dentro de una nueva política (normalmente suele ser una buena idea) o simplemente requerir directamente en nuestro archivo policies.js.

El siguiente ejemplo muestra brevemente cómo usarlo en el archivo policies.js :

{
 
'*': true,

 ProductController: {

   
// Evita que el usuario final realice operaciones CRUD sobre los productos, estas operaciones están reservadas para administradores      
   
// (use HTTP basic auth)
   
'*': require('http-auth')({
     realm:
'admin area'
   },
function customAuthMethod (username, password, onwards) {
     
return onwards(username === "Tina" && password === "Bullock");
   }),

   
// Todo el mundo puede ver la página productos
   show:
true
 }
}

Para entender mejor este ejemplo, deberías haber leído la sección “Políticas”

15. Subir Archivos al Servidor (Uploads)

body parser: “antes de que llegue el mensaje enviado desde el cliente hasta nuestra ruta, analiza el contenido del body para poder ser utilizado.

frontend: “es la parte donde al usuario se le presenta la página web, donde puede lanzar eventos, rellenar formularios, recibir respuestas del servidor, etc.”

En algún momento, en una de nuestras aplicaciones, es probable que necesitemos subir archivos al servidor. La manera de realizar esto en Sails es muy similar a cómo lo haríamos con Node.js y Express, al mismo tiempo, has de tener en cuenta que es muy diferente si has realizado subidas de archivos al servidor con otras tecnologías como PHP, ASP, .NET, Ruby, o Java. Pero no te apures, el equipo de Sails ha hecho que la subida de archivos sea fácil de conseguir.

Sails cuenta con uno de los mejores “body parser”, llamado Skipper, gracias a este body parser, implementar las subidas de archivos al servidor será relativamente fácil. No solamente para nuestro servidor (si estamos corriendo sails en local), sino que también, para otros servidores alojados en la nube (Amazon S3, MongoDB’s GridFS) o cualquier otro que soporte la subida de archivos.

Subiendo un archivo

Los archivos se cargan en un servidor web HTTP, de igual manera que cuando enviamos un formulario por vía POST, donde este contiene los valores de los input (entradas de texto que se han introducido en el frontend) parámetros que bien podrían ser “nombre”, “email” y “password”, también podríamos agregar otro como “avatar” o “canción”. Mira este ejemplo, esta pequeña porción de código ocurre en un controlador:

req.file('avatar').upload(function (err, uploadedFiles) {
 
// ...
});

Por medio del objeto req podemos manipular los parámetros que recibe la acción del controlador. (No te preocupes si de momento no entiendes bien este pedazo de código ya que más adelante veremos de qué maneras a través del objeto req podemos gestionar lo que llega como parámetros a la acción del controlador).

La acción del controlador es la encargada de cargar el archivo. Vamos a ver ahora un ejemplo más en profundidad que muestra cómo se puede permitir a ciertos usuarios subir una imagen y asociarlas a sus cuentas de usuario. Este ejemplo que verás a continuación, asume que tienes una política de control de acceso a tu aplicación y que tienes almacenado el identificador de usuario en req.session.xxx. (Si tienes adecuada tu página para la autenticación de usuarios con Passport, entonces el identificador del usuario será posiblemente req.session.passport.user)

// api/controllers/UserController.js
//
// ...


/**
* Subimos el archivo en la cuenta de usuario
*
* (POST /user/avatar)
*/

uploadAvatar: function (req, res) {

 req.file(
'avatar').upload({
   
// No permite que el tamaño del archivo exceda de ~10MB
   maxBytes:
10000000
 },
function whenDone(err, uploadedFiles) {
   
if (err) {
     
return res.negotiate(err);
   }

   
// Si no se sube un archivo, respondemos con un error.
   
if (uploadedFiles.length === 0){
     
return res.badRequest('No file was uploaded');
   }


   
// Guardamos la descripción del  archivo y la URL, para que pueda ser accedido posteriormente
   User.update(req.session.me, {  
// Recuerda que si usas passport puede que sea req.session.passport.user

     
// Generamos la URL
     avatarUrl: require(
'util').format('%s/user/avatar/%s', sails.getBaseUrl(), req.session.me),

     
// Cogemos el primer archivo y usamos  `fd` (descripción del archivo)
     avatarFd: uploadedFiles[
0].fd
   })
   .exec(
function (err){
     
if (err) return res.negotiate(err);
     
return res.ok();
   });
 });
},


/**
* Descargar archivo de un usuario, especificando su id (identificador)
*
* (GET /user/avatar/:id)
*/

avatar: function (req, res){

 req.validate({
   id:
'string'
 });

 User.findOne(req.param(
'id')).exec(function (err, user){
   
if (err) return res.negotiate(err);
   
if (!user) return res.notFound();

   
// El usuario no tiene ningún archivo de imagen subido al servidor.
   
// (si el usuario ha llegado hasta aquí, se utiliza una imagen predeterminada)
   
if (!user.avatarFd) {
     
return res.notFound();
   }

   
var SkipperDisk = require('skipper-disk');
   
var fileAdapter = SkipperDisk(/* optional opts */);

   
// Transmite la descarga
   fileAdapter.read(user.avatarFd)
   .on(
'error', function (err){
     
return res.serverError(err);
   })
   .pipe(res);
 });
}

//
// ...

A donde van a parar los archivos subidos?

Por defecto, los archivos subidos al servidor, van a parar al directorio myApp/.tmp/uploads. Podemos cambiar la ubicación donde se almacenarán los archivos subidos usando la opción dirname. Ten en cuenta que necesitarás esta opción cuando se llame a la función upload() y cuando se invoque al adaptador skipper-disk (Tanto para cargar como para descargar ha de ser el mismo lugar).

Almacenando archivos en una carpeta personalizada

En el ejemplo anterior el archivo se subía por defecto a la carpeta .tmp/uploads. Para forzar a que los archivos que se suban al servidor, se almacenen en otra carpeta que no sea temporal como podría ser la carpeta assets/images/ lo haremos mediante la adición de opciones que nos permite la función upload, tal y como mostramos a continuación:

req.file('avatar').upload({
 dirname: require(
'path').resolve(sails.config.appPath, '/assets/images')
},
function (err, uploadedFiles) {
 
if (err) return res.negotiate(err);

 
return res.json({
   message: uploadedFiles.
length + ' archivo(s) subido al servidor satisfactoriamente!'
 });
});

Otro Ejemplo

Primero de todo necesitamos generar un nuevo API para servir / guardar archivos. Para ello, utilizamos la valiosa herramienta que nos proporciona Sails, desde la línea de comandos introducimos lo siguiente:

$ sails generate api file

debug: Generated a
new controller `file` at api/controllers/FileController.js!
debug: Generated a
new model `File` at api/models/File.js!

info: REST API generated @ http:
//localhost:1337/file
info:
and will be available the next time you run `sails lift`.

Acciones del Controlador

Escribiremos dos acciones index para iniciar la carga de archivos y upload para recibir el archivo.

// myApp/api/controllers/FileController.js

module.exports
= {

 
index: function (req,res){

   res.writeHead(
200, {'content-type': 'text/html'});
   res.end(
   
'<form action="http://localhost:1337/file/upload" enctype="multipart/form-data" method="post">'+
   
'<input type="text" name="title"><br>'+
   
'<input type="file" name="avatar" multiple="multiple"><br>'+
   
'<input type="submit" value="Upload">'+
   
'</form>'
   )
 },
 
upload: function (req, res) {
   req.file(
'avatar').upload(function (err, files) {
     
if (err)
       
return res.serverError(err);

     
return res.json({
       message: files.
length + ' file(s) uploaded successfully!',
       files: files
     });
   });
 }

};

Fijate que en la acción upload no hemos especificado una carpeta diferente, para almacenar los archivos, con lo que damos por hecho que se almacenan por defecto en .tmp/uploads. (Anteriormente ya vimos cómo cambiar esto). Así y todo aquí lo muestro de nuevo, aunque con alguna diferencia

var uploadPath = './assets/images';
 uploadFile.upload({ dirname: uploadPath },
function onUploadComplete (err, files) {            

     
if (err)
       
return res.serverError(err);

     
return res.json({
       message: files.
length + ' file(s) uploaded successfully!',
       path:uploadPath
       file:files
     });
 });

Pero que pasa si nuestro servidor está corriendo en “Amazon S3”. Bien, Sails también viene preparado para que esto lo podamos realizar de forma sencilla. Primero de todo instalaremos el adaptador S3 filesystem adapter

$ npm install skipper-s3 --save

y entonces en el controlador modificamos de la siguiente manera:

req.file('avatar').upload({
   adapter: require(
'skipper-s3'),
   key:
'S3 Key'
   secret:
'S3 Secret'
   bucket:
'Bucket Name'
 },
function whenDone(err, uploadedFiles) {
   
if (err) return res.negotiate(err);
   
else return res.ok({
     files: uploadedFiles,
     textParams: req.params.all()
   });
 });

Otro adaptador útil es MongoDB's GridFS. Si queremos utilizar este adaptador primero deberemos instalarlo al igual que hicimos con el adaptador de Amazon S3

$ npm install skipper-gridfs  --save

Y en el controlador usaremos el adaptador de la siguiente manera:

req.file('avatar').upload({
 
// ...cualquier otra opcion aqui...
 adapter: require(
'skipper-gridfs'),
 uri:
'mongodb://[username:password@]host1[:port1][/[database[.bucket]]'
}, ...);

16. Log

“Un log es un registro oficial de eventos durante un rango de tiempo en particular. Para los profesionales en seguridad informática es usado para registrar datos o información sobre quién, qué, cuándo, dónde y porqué un evento ocurre para un dispositivo en particular o aplicación.”

Visión de conjunto

Sails cuenta con un simple registrador de log’s llamado captains-log . El modo de utilizarlo es muy simple al de Node.js “console.log”, pero con algunas características extras, uno de las características es el soporte para varios niveles de salida a la consola coloreada.

Configuración

Configuraremos el registrador de log’s, desde el archivo config/log.js.

Cuando configuramos un nivel de log, Sails dará salida a los mensajes que se emiten un nivel igual o superior al que esté configurado. También emitirá las salidas que se generen con socket.io, waterline y otras dependencias. La jerarquía de los niveles y sus prioridades relativas se resume en  la siguiente tabla:

Prioridad

Nivel

Funciones Log Visibles

0

silent

---

1

error

.error()

2

warn

.warn(), .error()

3

debug

.debug(), .warn(), .error()

4

info

.info(), .debug(), .warn(), .error()

5

verbose

.verbose(), .info(), .debug(), .warn(), .error()

6

silly

.silly(), .verbose(), .info(), .debug(), .warn(), .error()

sails.log()

Visión de conjunto

Cada uno de los métodos que describiremos a continuación, acepta un número infinito de argumentos de cualquier tipo de datos. Los argumentos siempre deberemos separarlos por comas.

Esta es la función por defecto cuando escribimos una salida a consola, de nivel  “debug”

sails.log('hello');
// -> debug: hello.

sails.log.error()

Escribimos un log de salida a consola de nivel “error”.

sails.log.error('Unexpected error occurred.');
// -> error: Unexpected error occurred.

sails.log.warn()

Escribimos un log de salida a consola de nivel “warn”.

sails.log.warn('File upload quota exceeded for user','request aborted.');
// -> warn: File upload quota exceeded for user- request aborted.

sails.log.debug()

        Alias de sails.log()

sails.log.info()

Escribimos un log de salida a consola de nivel “info”.

sails.log.info('A new user (', 'mike@foobar.com', ') just signed up!');
// -> info: A new user ( mike@foobar.com ) just signed up!

sails.log.verbose()

Escribimos un log de salida a consola de nivel “verbose”. Útil para capturar información detallada acerca de cuándo ocurre algo que no es corriente en la aplicación.

sails.log.verbose('Un usuario ha intentado transferir su cuenta...')
// -> verbose: Un usuario ha intentado transferir su cuenta...

sails.log.silly()

Escribimos un log de salida a consola de nivel “silly”.

sails.log.verbose('Un usuario ha intentado transferir su cuenta...')
// -> verbose: Un usuario ha intentado transferir su cuenta...

17. Testeando nuestro código

Para realizar las pruebas de código, usaremos mocha. Antes de empezar a construir nuestras pruebas de código, primero hemos de organizar la estructura de nuestro directorio ./test como por ejemplo:

./myApp
├── api
├── assets
├── ...
├── test
│  ├── unit
│  │  ├── controllers
│  │  │  └── UsersController.test.js
│  │  ├── models
│  │  │  └── Users.test.js
│  │  └── ...
│  ├── fixtures
│  ├── ...
│  ├── bootstrap.test.js
│  └── mocha.opts
└── views

bootstrap.test.js

Este archivo es útil cuando deseamos ejecutar algún código antes o después de ejecutar las pruebas. Debido a que nuestros modelos se convertirán en colecciones de waterline, es necesario levantar nuestro servidor (sails lift) para realizar la pruebas. Esto se aplica igualmente a los controladores de  la aplicación, así que nos tenemos de asegurar de llamar a este archivo primero.

var Sails = require('sails'),
 sails;

before(
function(done) {
 Sails.lift({
   
// configuración para nuestras pruebas
 },
function(err, server) {
   sails
= server;
   
if (err) return done(err);
   
// aquí cargamos lo necesario para realizar las pruebas.
   done(err, sails);
 });
});

after(
function(done) {
 
// aquí limpiamos lo cargado anteriormente.
 sails.lower(done);
});

mocha.opts

Este archivo contiene toda la configuración, para ver las opciones de configuración que dispone mocha dirigente a mocha.opts

Nota: Si vas a escribir tus pruebas unitarias con CoffeScript, has de añadir estas lineas en tu mocha.opts

--require coffee-script/register
--compilers coffee:coffee-script/register

Escribiendo nuestras pruebas

Una vez que nuestro directorio está preparado, podemos empezar a escribir el código para nuestras pruebas unitarias.

./test/unit/models/Users.test.js

describe.only('UsersModel', function() {

 describe(
'#find()', function() {
   it(
'should check find function', function (done) {
     Users.find()
       .
then(function(results) {
         
//  Algunas pruebas
         done();
       })
       .
catch(done);
   });
 });

});

 

Pruebas a los controladores

Para probar las respuestas de nuestros controladores podemos usar la librería Supertest, esta librería nos proporciona varios métodos útiles  para las peticiones HTTP.

./test/unit/controllers/UsersController.test.js

var request = require('supertest');

describe(
'UsersController', function() {

 describe(
'#login()', function() {
   it(
'should redirect to /mypage', function (done) {
     request(sails.hooks.http.app)
       .post(
'/users/login')
       .send({ name:
'test', password: 'test' })
       .expect(
302)
       .expect(
'location','/mypage', done);
   });
 });

});

Ejecutando nuestras pruebas

Con el fin de ejecutar las pruebas, utilizaremos mocha  desde la línea de comandos, pasando como argumentos la prueba que deseemos ejecutar. Asegurate de llamar a bootstrap.test.js antes que al resto de las pruebas que hayas escrito. ( mocha test/bootstrap.test.js test/unit/**/*.test.js )

18. Desplegando nuestra aplicación

Antes de desplegar

Antes de lanzar tu aplicación web, es necesario que te hagas las siguientes preguntas:

Desplegando en un único servidor

Node.js, es realmente rápido. Para la mayoría de las aplicaciones, manejar el tráfico en un único servidor es más que suficiente, al menos al principio.

Configurando

Desplegando 

En producción, en vez de usar sails lift para ejecutar el servidor, quizás sea una buena idea usar forever o PM2, para asegurarte de que aunque en el servidor ocurra algún error este no se detenga y siga corriendo.

Instala forever: $ sudo npm install -g forever

o bien …

Instala PM2: $ sudo npm install  PM2 -g --unsafe-perm

Desde el directorio de tu aplicación, iniciaremos el servidor con:

forever →  forever start app.js --prod

PM2 → pm2 start app.js -x -- --prod

Esto es lo mismo que usar sails lift --prod, la diferencia está en lo que comentábamos anteriormente, tanto forever como PM2 consiguen que el servidor se reinicie automáticamente aunque surjan errores.