About SailsJS tricks

Sails logo

He aquí la lista de trucos que he aprendido a la hora de utilizar Sails durante dos años.

HOOKS

Desactiva todo aquello que no vayas a utilizar en el fichero .sailsrc:

{
  "generators": {
    "modules": {}
  },
  "hooks": {
    "sockets": false,
    "pubsub": false,
    "cors": false,
    "blueprints": false,
    "i18n": false
  }
}

Puedes mirar la lista de hooks que trae para activarlos o desactivarlos a tu gusto aquí.

Aprende a crear tus propios hooks, desde un task runner con webpack, añadir métodos propios al objeto request, o ejecutar tareas contra la DB etc...

Además que puedes instalarlos vía npm o añadiéndolos a mano en la carpeta api/hooks y modificarlos en el proyecto a tu gusto. En este caso tendrás que instalar las dependencias si las tiene a mano.

CARPETA CONFIG/ENV

Poder sobre escribir las configuraciones y las variables de entorno dependiendo del enviroment es otra de las cosas que más flexibilidad dan a la hora de desarrollar, hacer tests, subir a producción etc...

Se cogerá el fichero de la carpeta config/env que coincida con lo que igualemos el NODE_ENV: NODE_ENV=preproduction node app.js. Es este caso, leerá de esa carpeta todas las configuraciones que queramos como, por ejemplo, la DB de preproducción, la key de stripe de desarrollo y los datos de Redis donde almacenar los sockets. Así es mi fichero de preproducción:

module.exports = {

  models: {
    connection: 'mongodb-preproduction',
    createIndexes: true // Just run when we create new models with new indexes
  },

  port: 1337,

  log: {
    level: 'info'
  },

  session: {
    host: 'somecoolurl.com',
    port: 43222,
    pass: 'helloworld!',
    db: 0,
    ttl: 30 * 24 * 60 * 60,
    prefix: 'sess'
  },

  stripeKey: 'sk_test_AWWpergERGERgERgcijI23424'

};

Por cada entorno, preproduction, production, test y development tengo un fichero.

Para usos más avanzados echad un ojo aquí.

BLUEPRINTS

No soy muy amigo de esta funcionalidad pero hay que admitir que en desarrollo puede ser muy útil. Hay que tener cuidado de no pasarlo a producción activado, para ello podemos activarlos únicamente en development, llendo a config/env/development.js y añadiendo lo siguiente:

blueprints: {  
  shortcuts: true,
  actions: true,
  rest: true
}

EXTENDER FUNCIONALIDADES DE WATERLINE

Waterline es el ORM con el que trabaja Sails, tiene una sintaxis muy declarativa con la que se trabaja muy bien. Sin embargo como todo ORM vas a encontrar limitaciones.

En mi caso no me gusta como trabaja creando indexes con Mongo, en producción modifica la estructura de la DB para no correr riesgos, por lo que lo mejor es que crees tu propio hook que haga lo que necesitas.

Por ejemplo, yo utilizo el hook Mongoat para la creación de indexes complejos.

WATERLINE DYNAMIC FINDERS

Si algo no me gusta de Sails es que trae muchas funcionalidades activadas que no utilizo, entonces has de tener cuidado y desactivarlas para evitar gastar memoria.

Una de estas funcionalidades viene en Waterline, y son los dinamicFinders: Person.findByFirstName('emma').exec(function(err,people){ ... }); donde firstName es un atributo del modelo Person.

En el fichero models añadir esta linea dynamicFinders: false ahorrara RAM en proporción al tamaño de tus modelos, aunque obviamente no podrás usar más esa sintaxis en el proyecto.

WATERLINE UPDATE/REMOVE PELIGROS

Waterline permite multiples sintaxis para una misma acción. Por ejemplo:

var _id = 'someId';  
User.update(_id, {name: 'Joseba'}).exec();  
// Es igual a:
User.update({id: _id}, {name: 'Joseba'}).exec();  

¿Donde está el peligro? En la primera query, porque si _id es null o undefined considerará que no hay criteria por lo tanto hará algo así:

User.update({}, {name: 'Joseba'}).exec();  

Y TODOS los users se llamarán Joseba a partir de entonces. Imaginad ese error con un User.destroy(null).exec(), adios a todos los usuarios. En el otro caso, User.destroy({id: null}) solo eliminaría a un usuario que tenga como id null, caso que no debería de ocurrir nunca.

De primeras, no hay problema en usar User.update(id, {...}) pero tenemos que estar 100% seguros de que idcontendrá un valor SIEMPRE.

Una vez casi la lío gordísima con esto y hago que todas las clases de todos los colegios se llamarán igual.

WATERLINE NATIVE/QUERY

http://sailsjs.org/documentation/reference/waterline-orm/models/native

http://sailsjs.org/documentation/reference/waterline-orm/models/query

WATERLINE MONGO - NATIVE OBJECTID

Para obtener un ObjectId() nativo de mongo, para utilizar junto a Model.native(), podemos hacer algo así:

User.mongo.objectId('72742743284238423') // === native mongoId  

Cualquier modelo que utilice el adapter sails-mongo tendrá esa funcionalidad.

CONSTANTES

A veces necesitamos guardar valores constantes que son parte de determinado servicio, por ejemplo, mailchimp. En ese caso, dentro de la carpeta config/ podemos crear un archivo llamado config/mailchimp.js que sería algo así:

module.exports.mailchimp = {

  API_KEY: 'ed363634-us14',

  TEACHERS_LIST: '234234346346'

};

De esa forma podría acceder a los valores desde cualquier lograr de la aplicación, haciendo lo siguiente: sails.config.mailchimp.API_KEY. Una vez más si en producción API_KEY tuviera otro valor diferente dentro del fichero config/production.js haría algo así:

module.exports = {

  mailchimp: {
    API_KEY: 'productionApiKey-us14',
  }

};

Ahora en producción el valor de sails.config.mailchimp.API_KEY sería 'productionApiKey-us14' mientras que en cualquier otro entorno sería 'ed363634-us14'.

ENVIAR EMAIL CON UN TEMPLATE

Podemos enviar un email utilizando nuestro template engine que hayamos puesto en nuestra app con este código:

var compileTemplate = function (view, data, done){  
  var relPath = path.join(EMAIL_TEMPLATES, view);
  if(typeof data.layout === 'undefined') data.layout = false;
  sails.hooks.views.render(relPath, data, done);
};

Y llamar a la función compileTemplate así:

var template = '/schools/adminWelcome';  
var data = {email: email, name: name, school: school};  
compileTemplate(template, data, function emailHTMLParsed(err, html){  
  // HTML === compiled html from our view (.ejs, .jade, .hbs, etc...)
  if(err) return cb(err);
  var emailData = {
     subject: 'Hello ' + school,
     name   : name,
     email  : email,
     body   : html
  };
  sendEmail(parseEmail(emailData), false, '', '', function(err){
     if(err) sails.log.error(err, 'Sending admin welcome', email);
     return cb(err);
    });
  });

TESTING

Siempre tener el migrate: drop en los modelos, para vaciar la DB cada vez que empecemos los tests. Ejemplo de mi servidor de tests:

var Sails = require('sails');  
var loadDb = require("../db/loadDb"); // File with all the data to load to the DB  
var testBootstrap = require('../db/testBootstrap');

before(function(done) {  
  Sails.lift({
    // configuration for testing purposes
    environment: 'test', // Set enviroment
    bootstrap: testBootstrap, // Different bootstrap function
    log: {level: process.env.log || 'error'}, // Optional log level
    hooks: {grunt: false, sockets: false, pubsub: false} // Disable some hooks
  }, function(err, sails) {
    if(err) return done(err);
    if(sails.config.environment !== 'test') return done('Please, check the environment'); // Make sure that we are in test env, to avoid accidental drop
    // Load mockup data
    loadDb.start(function(err){
      //start tests
      if(err) return done(err);
      done(err, sails);
    })
  });
});

after(function(done) {  
  // Give a extra time to make sure that the all DB transactions are finished
  setTimeout(done, 500);
});

Detalle super útil, cargar una función de bootstrap diferente a la que tenemos por defecto. Me permite hacer validaciones o crear datos específicos antes del test, y que solo ocurre en el momento de lanzar los tests.

MONGO & SCHEMA

Siempre he utilizado Waterline con Mongo y recomiendo que siempre tengáis la opción de schema: true en todos los modelos por defecto. En caso de querer hacer que un modelo tenga un schema variable añadidlo al modelo en question, mejor así, que luego vienen los disgustos. Más info.

GRUNT, MINIFY & CACHE

El pipeline de Sails te genera (en producción) un fichero minificado que se llama production.min.js y te lo inyecta en el layout. Funciona muy bien, pero la única pega es que si hace un cambio en los JS y lo subes a producción, el fichero se seguirá llamando igual por lo que se bajara el JS de la caché y no el nuevo JS con los cambios.

Para evitar esto, hay que modificar el Grunt que viene por defecto para que coja el package version del package.json y genere un min.js con la versión actual. Algo asi como, production.1.3.7.min.js. De esta forma el usuario se bajará los últimos cambios siempre.

Lo mismo para el CSS.

GRUNT MULTIPLE LAYOUTS

Otra mejora que hacer al Grunt es que por cada layout que tengamos cargue diferentes javascripts. Por ejemplo, no es lo mismo el JS de la home sin iniciar sesión que el JS una vez que estas logado como user. En un caso el fichero se llamará home.0.12.1.production.min.js y el otro app.0.12.1.production.min.js.

El primero se incrustara en aquel layout que tenga este comentario:

<!--SCRIPTS APP-->  
<script src="/min/home.0.12.1.production.min.js"></script>  
<!--SCRIPTS APP END-->  

Y en el segundo:

<!--SCRIPTS APP-->  
<script src="/min/app.0.12.1.production.min.js"></script>  
<!--SCRIPTS APP END-->  

Lo mismo para los CSS:

<!--STYLES APP-->  
<link rel="stylesheet" href="/min/app.0.12.1.production.min.css">  
<!--STYLES APP-->  

SOCKETS SESSION REDIS

Obligatorio activar las sesiones en redis, no solo por la escalabilidad, también para no perder la sesión cada vez que reiniciamos el server (por defecto se guarda en memoria). Además que evitaremos memory leaks.

Exactamente lo mismo para los sockets.

PM2

Node4 + PM2, son los mejores aliados para utilizar sails en producción. Es extremadamente sencillo crear una instancia por cada CPU Core, y aumentar la velocidad y la capacidad de manejar peticiones. Además de evitar downtimes a la hora de reiniciar la app por los nuevos cambios.

Es importante utilizar Redis para sockets y sesiones, si utilizamos multiples instancias de Sails aunque sea en el mismo server (captain obvious).

NGINX

Utiliza nGinx para servir los estáticos si o si.

location ~ ^/(images|javascript|js|css|media|static)/ {  
   autoindex off;
   root /tu/ruta/hasta/app/.tmp/public;
}

CRON

Si vas a usar un Cron en tu app, puedes cargarlo directamente en el config/bootstrap.js:

module.exports.bootstrap = function(cb) {  
  var mailer = sails.services.mailer;

  var stadisticsJob = new CronJob(
    "00 12 01 * * *",
    function(){mailer.prepareSending.call(mailer);},
    function(){sails.log.error("Job stopped! at ->", new Date());},
    true,
    "Europe/Madrid"
  );
};

De ahí la utilidad de usar diferentes bootstrap en tests...

MORE

Hay que subrayar que realmente han sido dos buenos años usando Sails, obviamente he encontrado bugs o limitaciones pero siempre han sido solucionados en poco tiempo. O se han encontrado alternativas para evitar el bug :)

Se han puesto mucho las pilas estas últimas semanas, y vuelve a molar mucho ser parte de la comunidad.

Pd: Añadiré más trucos útiles según me acuerde.

comments powered by Disqus