sails
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
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.
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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. |
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 |
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 |
[ OpenSUSE & SLE ]
sudo zypper ar http://download.opensuse.org/repositories/devel:/languages:/nodejs/openSUSE_12.1/ NodeJSBuildService |
[ 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:
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:
Comprobado que nuestro servidor funciona, vamos a pasar a materia y explicar como trabajar con sails
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. |
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 :
{ |
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.
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 |
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. |
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 |
// En su controlador, servicio, modelo u otra |
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.
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 |
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 |
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 |
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 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' |
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', |
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'}, |
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', |
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).
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) { |
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 = { |
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
|
Sails generará un archivo en api/controllers/ComentarioController.js parecido a esto:
/** |
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) { |
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) { |
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:
/** |
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 |
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 |
// Producto.js |
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 |
// Producto.js |
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 = { |
myApp/api/models/Usuario.js
module.exports = { |
myApp/config/bootstrap.js
module.exports.bootstrap = function (cb) { |
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 = { |
myApp/api/models/Usuario.js
module.exports = { |
Ejecutamos sails console
sails> Mascota.create({name:'Dedo Gordo', color:'rosa'}).exec(console.log) |
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 = { propietario: { model: ‘Usuario’ } |
myApp/api/models/Usuario.js
module.exports = { via: ‘propietario’ |
Ejecutamos sails console
sails> Usuario.create({name:'Miguel', age:'21'}).exec(console.log) |
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 = { propietario: { model: ‘Usuario’ } |
myApp/api/models/Usuario.js
module.exports = { |
Ejecutamos sails console
sails> Usuario.create({ name: 'Miguel', age: 21}).exec(console.log); |
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: { |
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: { |
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: { |
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: { primaryKey: true, |
enum: Atribuye validación especial, solo guarda los datos, que coinciden con una serie de valores estipulados en un array.
attributes: { |
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: { |
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: |
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 |
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 |
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 |
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.
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'); |
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...
/* Función de auto-llamada */
|
Ahora podemos...
Monkey.findWithSameNameAsPerson(albus, function (err, monkeys) { ... }); |
Otro ejemplo:
// api/models/User.js |
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... |
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' }); |
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({ |
Las consultas NOT-IN son similares a las “IN”, pero siguiendo otro criterio
Model.find({ |
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({ |
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' }}) |
‘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' }}) |
‘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' }}) |
‘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 |
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 | |
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 |
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: { |
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: { |
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: { |
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: { |
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:
{ |
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> |
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:
// ... |
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... |
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 |
Para acceder a este servicio desde un controlador, lo haríamos de la siguiente manera:
// En algún controlador |
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": { |
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": { |
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": { |
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 = { |
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"> |
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 |
Una vez instalado, configuramos este middleware en config/http.js
// ... |
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:
// ..... |
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> |
En la controlador:
req.__('Hello'); // => Hola |
O si conoces el identificador de configuración regional, puedes traducir desde cualquier parte de la aplicación utilizando:
sails.__({ |
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):
{ |
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: |
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 |
// 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 |
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’ { |
// 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’ { |
// 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’ { |
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 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 :
{ |
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 |
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({ |
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 |
Acciones del Controlador
Escribiremos dos acciones index para iniciar la carga de archivos y upload para recibir el archivo.
// myApp/api/controllers/FileController.js |
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'; |
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({ |
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({ |
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'); |
sails.log.error()
Escribimos un log de salida a consola de nivel “error”.
sails.log.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.'); |
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!'); |
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...') |
sails.log.silly()
Escribimos un log de salida a consola de nivel “silly”.
sails.log.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 |
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'), |
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 |
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() { |
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'); |
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.