Blog tech

Rails à la sauce JavaScript

Rédigé par Numa Claudel | 10 septembre 2014

Pour ce billet nous allons embarquer vers la découverte d’un framework que je voulais essayer depuis un moment : Sails.

C’est un jeune framework JavaScript construit sur Node.js (vous le connaissez sûrement: il permet de construire un serveur en JavaScript), et Express.js (un ensemble d’outils destiné au développement d’applications Node.js).

Premier constat, son nom ressemble énormément à celui de Rails, ce qui n’est pas étonnant puisqu’il s’en inspire sur pas mal de points. Ensuite, comme dit sur leur site, il émule le modèle MVC, et a été dessiné pour servir des API orientées données. Pratique pour le développement de SPA.

Un tour du propriétaire

Pour commencer vous aurez besoin de Node.js et npm (le gestionnaire de paquet pour Node.js) d’installés sur votre machine pour pouvoir installer Sails. Si vous rencontrez des problèmes d’installation dû au fait que l’executable node n’est pas trouvé, c’est parce-qu’il a été renommé en nodejs dans les versions récentes mais que certains programmes utilisent toujours node. Dans ce cas installez le paquet nodejs-legacy qui crée un lien symbolique entre /usr/bin/node et /usr/bin/nodejs. Puis :

sudo npm install sails -g

L’option -g sert à installer un paquet de manière globale sur votre machine, de telle sorte que la commande sails soit accessible partout, d’où la nécessité de l’exécuter en tant que root. Après un certains temps, nous pouvons créer notre première application :

sails new ohMonForum
cd ohMonForum
sails lift

La dernière commande démarre le serveur, qui est accessible à la page : http://localhost:1337

Nous sommes en train de visionner la vue homepage qui nous donne quelques informations pour démarrer et quelques liens vers la documentation.

views/homepage.ejs nous indique que le moteur de template utilisé par défaut par Sails est EJS, une sorte de portage de ERB pour JavaScript.

Je vous propose de jeter un coup d’œil à l’arborescence générée par Sails dans notre dossier ohMyForum. Pour ce faire, cliquons sur le lien AppStructure de la page d’accueil de notre application :

Nous nous retrouvons sur une page qui décrit chaque partie de l’application, avec des fichiers d’exemples. On peut observer que le dossier api va être celui dans lequel nous passerons le plus clair de notre temps :

Le dossier views est quand à lui externe au dossier api, ce qui est logique, et on retrouve assets, config et tasks. On remarque aussi la présence d’un fichier Gruntfile.js. Grunt est un outil pour la création et le lancement de tâches automatisées au même titre que Rake. tasks contient donc les tâches Grunt.

Allons voir ce qu’il y a dans config/connections.js. Dans ce fichier se trouve quelques exemples de configurations de bases de données avec différents adaptateurs, ainsi que l’adaptateur utilisé par défaut : sails-disk.

sails-disk est un adaptateur qui charge et utilise les données en mémoire lors du fonctionnement de l’application, et qui persiste les données au moment de l’arrêt du serveur dans le fichier .tmp/localDiskDb.db. Il existe aussi sails-memory qui ne conservera aucune donnée. Ce driver semble particulièrement adapté pour les tests, en permettant de gagner en temps d’exécution.

Les configurations locales aux bases de données se feront dans config/local.js qui est d’ailleurs ignoré dans le .gitignore.

Embarquement

Nous allons maintenant créer notre première API. Sails vient avec un générateur en ligne de commandes avec lequel créer un modèle se fait sous cette forme :

sails generate model <unModel> attr1:type attr2:type ...

et un controller :

sails generate controller <unController> action1 action2 …

Mais pour faire simple nous allons suivre ce que nous indique la page d’accueil de notre application, à savoir :

sails generate api user

Cette commande va nous générer le modèle User et son contrôleur, tout deux sans attributs ni méthodes, mais nous les ajouterons plus tard.

Première chose à faire redémarrer le serveur. Oui il faut redémarrer le serveur pour chaque changement effectué, ce qui n’est pas très pratique. Aussi je vous propose d’installer forever, un outils en ligne de commande qui va garder notre application en fonction, tout en se chargeant de la redémarrer à chaque changement de base de code.

sudo npm install forever -g

Ensuite ajoutons le fichier .foreverignore à la base de notre projet avec ce contenu :

**/.tmp/**
**/.git/**
**/views/layout.ejs

Puisque l’on va demander à forever d’observer les changements dans la base de code, et de redémarrer à chaque fois, ignorer ces fichiers/dossiers évitera que forever ne redémarre notre serveur en boucle. Sails écrit dans .tmp, Grunt écrit dans views/layout.ejs, et il y a de fréquentes modifications d’index de Git dans .git. Il faudra toutefois penser à redémarrer l’application manuellement pour les modification de views/layout.ejs.

Lançons donc Sails avec forever :

forever -w start app.js

L’option -w dit à forever d’observer les changements dans les fichiers. C’est bien mais puisque le serveur est lancé en tache de fond, on n’a plus accès aux logs de la console. Essayons d’avoir quelques informations sur le processus forever que nous avons lancé :

forever list

Dans la liste on peut noter plusieurs choses, entre autre l’uid du processus, mais surtout son fichier de log. Un tail -f sur ce fichier et on retrouvera nos logs. Mais il y a encore plus simple :

forever -w -f start app.js

-f indique que nous voulons suivre les logs dans la console. Parfait.

Un peu avant, lorsque j’ai dit que Sails écrivait dans le .tmp c’est parce que le fichier de base de données sails-disk s’y trouve mais aussi parce que Sails copie les assets dans ce dossier.

Ensuite pourquoi Grunt écrit dans views/layout.ejs ? Lorsque vous déposez un fichier CSS, JavaScript ou un template dans les dossiers d’assets et que vous redémarrez le seveur, Grunt parcourt ces dossiers et insère pour chaque asset un lien dans le layout. Ouvrez donc views/layout.ejs, vous y trouverez les sections :

<!--STYLES-->
<!--STYLES END-->
<!--TEMPLATES-->
<!--TEMPLATES END-->
<!--SCRIPTS-->
<!--SCRIPTS END-->

Grunt insère les liens vers les assets entre leurs balises respectives.

Revenons à notre API user fraîchement créée. On remarque dans le terminal qu’on a un message « warning », ainsi qu’un prompt nous demandant de choisir une stratégie de migration. Pour l’instant la proposition d’auto-migration me semble la plus intéressante, mais plutôt que de répondre au prompt je vous propose d’aller dans le fichier config/models.js, et de décommenter cette ligne :

migrate: 'alter'

Sails ne nous posera plus de questions à chaque création / modification de modèle, il sait maintenant quoi faire. La documentation nous dit qu’en production Sails utilise toujours le paramètre safe, ce paramètre n’agit donc que sur notre base de développement.

Naviguons maintenant vers /user: chose assez surprenante aucun message d’erreur, mais la représentation d’un tableau vide. On a donc accès à une liste d’utilisateurs sans définir de routes ? C’est grâce à Blueprint API qu’utilise Sails, qui dès lors qu’un modèle et son contrôleur sont créés, rend disponible ces routes par défaut: find, create, update, destroy, populate, add and remove. Par la suite, si on ajoute une action à un contrôleur, il en sera de même, la route sera créé sans avoir à compléter le fichier config/routes.js.

Il est tout de même possible de le compléter avec des routes personnalisées dont voici quelques exemples, ainsi que de désactiver ce comportement automatique en modifiant dans config/blueprints.js les valeurs de actions et rest.

Une autre option présente dans ce fichier m’a intrigué, c’est shortcuts. En parcourant la documentation de Sails sur Blueprint API, il apparaît que cette option donne accès à toutes les méthodes que je viens de citer au travers du navigateur. D’accord, rendons nous à /user/create?name=Numa Claudel :

{
  "name": "Numa Claudel",
  "createdAt": "2014-09-08T14:23:55.397Z",
  "updatedAt": "2014-09-08T14:23:55.397Z",
  "id": 1
}

Sails nous retourne l’utilisateur qui viens d’être créé. On peut en créer d’autres, avec d’autre attributs. Pratique. Après avoir créé quelques utilisateurs, retourner sur /user devrait laisser apparaître une liste d’utilisateurs sous forme de JSON. Cette fonctionnalité va être pratique pendant notre phase de développement, mais par sécurité il faudra la désactiver en mode production: shortcuts: false.

Hissons les voiles

Continuons notre traversée, et créons quelques modèles relatifs à ce qui pourrait être un forum, ainsi que leurs attributs et quelques validations. Pour commencer, ajoutons un paramètre de configuration dans config/models.js :

schema: true

Ce paramètre à false laisse la liberté d’ajouter sans restriction tout attributs et leurs données à un modèle. A true seul les attributs définis dans nos modèles seront sauvés en base.

api/models/User.js :

module.exports = {

  attributes: {
    name: {
      type: 'string',
      required: true
    },
    email: {
      type: 'email',
      required: true
    },
    birthDate: 'date',
    topics: {
      collection: 'topic',
      via: 'creator'
    },
    posts: {
      collection: 'post',
      via: 'author'
    }
  }
};

Notre utilisateur pourra créer des topics et des commentaires que nous allons créer :

sails generate api topic
sails generate model post

api/models/Topic.js :

module.exports = {

  attributes: {
    subject: {
      type: 'string',
      required: true,
      minLength: 20
    },
    content: 'text',
    creator: {
      model: 'user',
      required: true
    },
    posts: {
      collection: 'post',
      via: 'topic'
    }
  }
};

api/models/Post.js :

module.exports = {

  attributes: {
    content: {
      type: 'text',
      required: true
    },
    author: {
      model: 'user',
      required: true
    },
    topic: {
      model: 'topic',
      required: true
    }
  }
};

Essayons maintenant d’ajouter un nouveau topic à l’utilisateur Numa en navigant vers http://localhost:1337/user/1/topics/add. Voici le retour :

{
  "error": "E_VALIDATION",
  "status": 400,
  "summary": "2 attributes are invalid",
  "model": "Topic",
  "invalidAttributes": {
    "subject": [
      {
        "rule": "string",
        "message": "`undefined` should be a string (instead of \"null\", which is a object)"
      },
      {
        "rule": "required",
        "message": "\"required\" validation rule failed for input: null"
      },
      {
        "rule": "minLength",
        "message": "\"minLength\" validation rule failed for input: null"
      }
    ],
    "creator": [
      {
        "rule": "required",
        "message": "\"required\" validation rule failed for input: null"
      }
    ]
  }
}

La validation fonctionne bien, essayons alors /user/1/topics/add?subject=Le premier topic de Numa&creator=1. Ça fonctionne ! Allons voir /user/1/topics :

[
  {
    "subject": "Le premier topic de Numa",
    "creator": 1,
    "createdAt": "2014-09-08T15:24:17.605Z",
    "updatedAt": "2014-09-08T15:24:17.624Z",
    "id": 1
  }
]

On a très rapidement défini une API JSON prête à répondre à nos futures requêtes.

Un autre point que je voulais aborder avec vous est le concept des policies. C’est un outil fournit par Sails qui permet de gérer les droits / autorisations d’accès, dont les règles définies sont appliquées avant d’entrer dans les actions des contrôleurs. Les policies se définissent dans le dossier api/policies et sont appliquées dans config/policies.js. Une policy sessionAuth.js est déjà présente, mais dans le fichier de config tout est commenté.

Disons que sur notre forum nous voulons qu’un post ne soit créé / modifié / consulté qu’à partir d’un topic, donc nous ne voulons pas que l’url /user/:id/posts... soit accessible, alors créons une policy qui retournera une 400 (bad request) sur cette url.

api/policies/noPostWithoutTopic.js :

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

  if((/^\/user\/\d+\/posts/).test(req.path)) {
    return res.badRequest('A post can only be accessed from a topic.');
  }
  next();
};

Il faut ensuite ajouter une règle à config/policies.js pour spécifier à quel moment cette policy doit être utilisée :

UserController: {
  '*': 'noPostWithoutTopic'
}

On applique la règle sur toutes les actions du contrôleur UserController, car l’URL testée pointe vers les actions de la collection posts d’un utilisateur. Essayons maintenant d’accéder à des posts directement par un utilisateur, par exemple /user/1/posts, ou encore /user/1/posts/add?content=blabla&author=1 : le serveur nous retourne bien une erreur 400 avec notre message d’erreur.

Avant de jeter l’ancre

Très brièvement je veux vous exposer un dernier concept, le temps-réel.

Sails introduit le concept de PubSub (publication / subscription) : un modèle émet des évènements lors de son utilisation, auquel il est possible de souscrire pour en être notifié.

Pour le coté client Sails vient avec la librairie Sails socket client qui est un wrapper de Socket.IO, une librairie JavaScript destinée à l’implémentation d’évènements temps-réel dans une application.

Prenons une profonde inspiration, et faisons un petit test très simple. Ouvrons 2 fenêtres de navigateurs. Dans l’une naviguons vers http://localhost:1337, ouvrons la console du navigateur et entrons ceci :

io.socket.get('/user');
io.socket.on('user', function(data) { console.log(data); });

Maintenant dans la deuxième fenêtre créons un utilisateur supplémentaire avec cette url http://localhost:1337/user/create?name=untel&email=untel@untel.fr. Dans la console de la première on voit instantanément apparaître:

Object {verb: "created", data: Object, id: 3}

Supprimons cet utilisateur http://localhost:1337/user/destroy/3 :

Object {verb: "destroyed", id: "3", previous: Object}

La première instruction que nous avons entrée dans la console du navigateur, se connecte au serveur en demandant la liste des utilisateurs. La deuxième instruction demande à souscrire aux événements émis par le modèle User, et affiche le retour avec un console.log. Ainsi nous sommes notifiés pour chaque action émise par ce modèle.

Il est temps de poser pied à terre !

Conclusion

Découvrir ce framework m’a fait l’effet d’une grande bouffée d’air frais, j’ai rapidement retrouvé mes marques, tout en étant agréablement surpris par certaines facilités. Je pense notamment aux routes générées automatiquement, aux raccourcis au travers du navigateur pour les actions sur les modèles, à la simplicité de créations de règles de contrôles, ainsi qu’à la facilité d’accès à des événements temps-réel.

Ce n’était qu’un aperçu, il y a encore beaucoup à explorer ! J’espère que cette rapide traversée vous a plu, et qu’elle vous aura donné envie autant qu’à moi de continuer l’exploration.

L’équipe Synbioz.

Libres d’être ensemble.