
Erwan GÉRÉEC
Full Stack Developer & Scrum Master
__Erwan

Mobile developer in my spare time (Android Kotlin, Flutter)
(NodeJS, VueJS)
Web app
Mobile app
RESTs API
Moteur de calcul
BDD
Collecteur
Points d'accès Wifi










Web app
Mobile app
RESTs API
Moteur de calcul
BDD
Collecteur
Points d'accès Wifi










EN 1 HEURE CHRONO
On va voir comment utiliser le framework, découvrir son architecture et jouer avec son ORM Waterline pour créer des APIs REST
SailsJS, Késako ?
Un framework basé sur Express
- Un framework Javascript pour créer des applications Node.js suivant le pattern MVC (Model/View/Controller)
- Idéal pour concevoir des applications temps réel et/ou des services REST
- Basé sur Express
- Fournit un support websocket avec socket.io intégré par défaut
- Pré-configuré avec une liste de conventions définies par défaut
- Entièrement configurable et personnalisable
- Support de gridFS (Stockage fichier dans base mongo)
- Upload d'image sur Amazon S3

Avec un puissant ORM (Waterline)
Un ORM?
- Pour "Object Relationnal Mapping". Fournit une couche d'abstraction pour interagir avec la base de données peu importe la base de données que vous utilisez.
- Plusieurs "adapteurs" disponibles avec Waterline:
- Local Disk (par défaut, utile en mode développement)
- MySQL
- PostgreSQL
- MongoDB
- Redis
- Cassandra
Mais aussi: Oracle, OrientDB, MySQL Server...

Avantages
- Pré-configuré : Offre un cadre de démarrage à l'équipe de développement
- Environnement NodeJS : Accès à tous les modules NPM
- Entièrement en javascript : utilisation commune du JS côté Back et Front
- Documentation très complète
- Basé sur Express
- Ecriture en JavaScript ou TypeScript
- Entièrement paramétrable
- Prêt pour être intégré avec des bases de données multiples grâce à Waterline
Inconvénients
- Framework moins connu qu'Express
- Basé sur npm (On tire des modules de modules de modules, etc..)
- Taille de repository qui reste du coup assez importante
Installation
On installe SailsJS globalement avec npm
npm install sails -gC'est le bon moment pour aller prendre une tasse de café.

Nouveau projet
sails new brestjsOn part sur un projet vide: [2]
Choose a template for your new Sails app:
1. Web App · Extensible project with auth, login, & password recovery
2. Empty · An empty Sails app, yours to configure
(type "?" for help, or <CTRL+> to cancel)
?cd brestjs
sails liftOn démarre le projet:
On créé le projet:

Le nouveau projet est prêt.
Par défaut, SailsJS renvoie une page statique basée sur du templating EJS.
erwan:brestjs erwangereec$ sails lift
info: Starting app...
info:
info: .-..-.
info:
info: Sails <| .-..-.
info: v1.2.3 |\
info: /|.\
info: / || \
info: ,' |' \
info: .-'.-==|/_--'
info: `--'-------'
info: __---___--___---___--___---___--___
info: ____---___--___---___--___---___--___-__
info:
info: Server lifted in `/Users/erwangereec/Prez/brestjs`
info: To shut down Sails, press <CTRL> + C at any time.
info: Read more at https://sailsjs.com/support.
debug: -------------------------------------------------------
debug: :: Mon Jan 13 2020 18:47:01 GMT+0100 (GMT+01:00)
debug: Environment : development
debug: Port : 1337
debug: -------------------------------------------------------Hot Reloading
npm i -g nodemonnpm run sailsCréez un script "sails" dans le fichier package.json :
Installer nodemon : (https://nodemon.io/)
...
"scripts": {
"sails": "nodemon app.js"
},
...Exécutez votre script:
Architecture d'une application SailsJS
Structure du projet

/api : Couches logiques: Controlleurs, Services, Modèles de données
/assets : Tout le nécessaire pour l'application côté frontend (optionnel)
/config : les fichiers de configuration de l'application SailsJS (Base de donnée, websockets, ..)
/node_modules : Modules Node installés par défaut
/tasks : Liste de tâches Grunt pour la compilation
/views : templates de page statiques au format EJS
app.js : Fichier d'entrée du projet
Cheminement d'une requête
Controleur
Service 1
Modèle
Base de données
Service 2
Service n...
Routes
Policies
Routes
Tout se passe dans un seul fichier : routes.js
C'est très déclaratif et facile à maintenir.
On peut définir des sous-dossiers pour une meilleure organisation du projet.
["POST /beers"]: {
controller: "BeerController",
action: "createBeer"
},
["GET /beers"]: {
controller: "BeerController",
action: "getAllBeers"
},
["GET /beers/:beedId"]: {
controller: "BeerController",
action: "getOneBeer"
},
["PUT /beers/:beerId"]: {
controller: "BeerController",
action: "updateOneBeer"
},
["DELETE /beers/:beerId"]: {
controller: "BeerController",
action: "deleteOneBeer"
}On va y définir toutes les URLs de nos services REST
config/routes.js
Policies
Pour contrôler les données en entrée avant de diriger la requête vers le bon contrôleur.
C'est l'endroit idéal pour tester l'authentification associée à une requête, vérifier qu'un utilisateur a la permission d'accéder à une ressource donnée, vérifier la validité des paramètres du body.
Pour chaque route, on déclare les policies qui y sont associées.
config/policies.js
module.exports.policies = {
"BeerController": {
createBeer: [
"isBeerSchemaValidated"
]
}
};Hapijs/joi
api/policies/isBeerSchemaValidated.js
const Joi = require("joi");
module.exports = function(req, res, next) {
let params = req.body;
// Define validation schema
const schema = Joi.object().keys({
name : Joi.string().required(),
origin : Joi.string().required(),
manufacturingDate : Joi.string().isoDate()
});
// Validate the request body data with the schema
Joi.validate(params, schema, async (errors) => {
if (errors) {
// Return 400 Bad Request
} else {
return next(); // Continue to the next policy
}
});
};
Une lib pour valider très facilement les données en entrée de vos APIs POST et PUT.
Hapijs/joi
- motDePasse : Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$'))
- motDePasseRep : Joi.ref('motDePasse')
- nom : Joi.string().alphanum().min(3).max(30).required()
- dateDeNaissance : Joi.number().integer().min(1900).max(2013)
- planetes : Joi.string().valid(["pluton", "neptune"])
Controlleurs
- Consomme un ou plusieurs services pour travailler sur les données.
- Retourne la réponse avec le code de retour approprié.
(RFC 7231: https://tools.ietf.org/html/rfc7231)
/**
* Create a new Beer
* This API is used to create a new beer
*
* @name createBeer
* @param {Object} req
* @param {Object} res
*/
async createBeer(req, res) {
try {
var createBeerRes = await BeerService.createBeer(req.body);
if (createBeerRes.status === "SUCCESS") {
return res.status(200).json(createBeerRes.data);
} else {
return res.status(500); // Fucking world
}
} catch (err) {
// ...
}
}api/controllers/BeerController.js
Services
- Contiennent des méthodes réutilisables à travers l'application
- Sont automatiquement injectés par Sails pour être utilisés dans les contrôleurs. (Injection de dépendances)
- Travaillent sur les modèles de données en utilisant l'ORM Waterline
async createBeer(params) {
let attrs = {};
if (params.hasOwnProperty("name")) {
attrs.name = params.name;
}
if (params.hasOwnProperty("origin")) {
attrs.origin = params.origin;
}
var createdBeer = await Beer.create(attrs).fetch();
return {
status: "SUCCESS",
data: createdBeer
}
}api/services/BeerService.js
Modèle 1/5
- Un fichier par modèle de données
- Contient la déclaration des primitives (attributs) et des associations
- Plusieurs types gérés par défaut (string, integer, json, datetime, boolean...)
module.exports = {
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
name: {
type: "string",
required: true
},
origin: {
type: "string"
}
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
}
}
api/models/Beer.js
Modèle 2/5
On peut définir une valeur par défaut qui sera renseignée automatiquement en cas d'absence de valeur fournie pour l'attribut.
module.exports = {
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
phoneNumber: {
type: 'string',
defaultsTo: '111-222-3333'
}
}
}
Modèle 3/5
On peut définir des valeurs de validation d'un attribut.
module.exports = {
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
musicType: {
type: 'string',
isIn: ['pop', 'electro', 'classic'],
}
}
}
Modèle 4/5
On peut décider de chiffrer la valeur d'un attribut en base de donnée.
Dans ce cas la valeur récupérée lors d'une recherche sera elle aussi chiffrée. (Pourra être déchiffrée en fournissant .decrypt() dans la requête)
module.exports = {
attributes: {
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
password: {
type: 'string',
encrypt: true
}
}
}
Modèle 5/5
- Une méthode "customToJSON()' définie par défaut qui permet de customiser les clés qui seront retournées pour ce modèle.
- Plusieurs méthodes de callback disponibles pour gérer le cycle de vie d'un modèle : beforeCreate(), afterCreate(), beforeUpdate(), afterUpdate(), beforeDestroy(), afterDestroy()
customToJSON: function () {
const data = _.omit(this, ["password"]);
return data;
},
afterDestroy: async (deletedUser, proceed) => {
await Activity.destroy({user: deletedUser.id});
await proceed();
}
api/models/User.js
Jouons avec Waterline

Quelques mots...
- Le langage de requête s'appelle "Waterline Query Language".
- Waterline sait comment interpréter ce langage pour le convertir dans le langage adapté à la base de données cible.
- Waterline utilise le(s) adaptateur(s) de base de données déclaré(s) dans votre projet pour traduire ce langage en requêtes natives et envoyer ces requêtes à la base de données.
- Cela signifie que vous pouvez utiliser la même requête avec MySQL que vous le faites avec Redis ou MongoDB.
- Cela signifie également que vous pouvez modifier votre base de données avec un minimum de modifications dans votre code applicatif.
Quelques exemples
find({})
var data = await Communaute.find({
name: 'brestjs'
});var data = await Communaute.find({
name: 'brestjs',
location: 'brest'
});var data = await Communaute.find({
name: {
'contains' : 'brest'
}
});find({})
var data = await User.find({
age: { '<': 30 }
});var data = User.find({
name: { in: ['jules', 'enzo'] }
});var data = await Log.find({
trace: { contains: 'error' }
});Tous les critères
'<'
'<='
'>'
'>='
'!='
nin
in
contains
startsWith
endsWith
Options
Définir une limite sur le volume de données
User.find({
where: { name: 'john' },
limit: 20
});Tri ascendant
User.find({
where: { name: 'john' },
sort: 'name ASC'
});User.find({
where: { name: 'john' },
sort: 'name DESC'
});Tri descendant
create()
Création d'un nouveau utilisateur
await User.create({
name:'john',
age: 20
});await User.create({
name:'john',
age: 20
}).fetch();Création d'un nouveau utilisateur et récupération immédiate
update() / updateOne()
Mise à jour de plusieurs entrées à la fois
await User.update({ name:'john' })
.set({
name:'lucas'
});await User.updateOne({ name:'john' })
.set({
name:'lucas'
});Mise à jour d'une entrée
Mise à jour et récupération immédiate
await User.update({ name:'john' })
.set({
name:'lucas'
}).fetch();stream()
Utile pour travailler sur une grande quantité de données sans mettre votre serveur parterre...
await User.stream({
name:'john'
}).eachRecord(async (user)=>{
sails.log(`Nouvel utilisateur trouvé. Id: ${user.id}.`);
});getDatastore()
Un accès direct à la base de données sans passer par Waterline. Peut vous être utile dans certains cas particuliers.
var db = Site.getDatastore().manager;
db.collection("site").ensureIndex({location: '2dsphere'}, function () {
cb();
});Exemple: On indique ici que la clé "location" du modèle site est un GeoJSON.
Gestion des associations

Association One-to-One 1/2
Exemple : Un utilisateur a une adresse postale.
module.exports = {
attributes: {
name: {
type: "string"
},
email: {
type: "string",
required: true
},
address: {
collection: "address", // Target model
via: "user" // Current model
}
}
};module.exports = {
attributes: {
rue: {
type: "string"
},
codePostal: {
type: "string"
},
ville: {
type: "string"
},
user: {
model: "user",
unique: true
}
}
};api/models/User.js
api/models/Address.js
Pour ajouter une adresse à un utilisateur, il faut créer l'adresse en donnant l’id de l’utilisateur :
await Address.create({
rue: "2 rue victor hugo", codePostal: "29200", ville: "brest",
user: "abcdefghijklmnqrtuvwxyz"
})Association One-to-One 2/2
Exemple : Un utilisateur a une adresse postale.
Du coup, lors de la récupération d'un utilisateur, on peut utiliser populate() pour populer automatiquement l'association address:
await User.findOne({
email: "test@test.com"
}).populate("address");{
name: "test",
email: "test@test.com",
address: {
rue: "2 rue victor hugo",
codePostal: "29200",
ville: "brest"
}
}Association One-to-Many
Exemple : Un utilisateur a plusieurs comptes bancaires.
module.exports = {
attributes: {
name: {
type: "string"
},
email: {
type: "string",
required: true
},
bankAccounts: {
collection: "bankAccount", // Target model
via: "user" // Current model
}
}
};module.exports = {
attributes: {
name: {
type: "string"
},
solde: {
type: "integer"
},
user: {
model: "user"
}
}
};api/models/User.js
api/models/BankAccount.js
Pour ajouter un compte à un utilisateur, il faut créer le compte en donnant l’id de l’utilisateur :
await BankAccount.create({ name: "compte courant", solde: 500, user: "abcdefghijklmnqrtuvwxyz" })Association Many-to-Many
Exemple : Un utilisateur peut conduire plusieurs voitures. Une voiture peut être conduite par plusieurs utilisateurs.
module.exports = {
attributes: {
voitures: {
collection: "voiture", // Target model
via: "user" // Current model
through: "voitureuser" // Pivot collection
}
}
};module.exports = {
attributes: {
immatriculation: {
type: "string"
},
users: {
collection: "user",
via: "voiture",
through: "voitureuser"
}
}
};api/models/User.js
api/models/Voiture.js
api/models/VoitureUser.js
module.exports = {
attributes: {
user: {
model: "user"
},
voiture: {
model: "voiture"
}
}
};On créé dans ce cas une collection Pivot qui contiendra les ids des 2 objets.
Allons un peu plus loin
Une APIDoc aux petits oignons

Checklist
- Avoir un Guide d'authentification
- Avoir une section Getting Started
- Avoir une section API Reference avec descriptions des endpoints
- Avoir des exemples de code dans différents langages
- Avoir une description des réponses (succès et erreurs)
- Stripe: https://stripe.com/docs/api
- Twilio: https://www.twilio.com/docs/usage/api
- Dropbox: https://www.dropbox.com/developers/documentation
- Github: https://developer.github.com/v3/guides/getting-started/


sails-hook-swagger-generator
Un outil pour générer une doc Swagger à partir de vos APIs écrites dans Sails.
["POST /beers"]: {
controller: "BeerController",
action: "createBeer",
swagger: {
summary: "Create one Beer",
description: "This API allows to create a new beer.",
body: {
name: {
type: "string",
required: true,
description: "This is the name of the Beer."
},
origin: {
type: "string",
required: true,
description: "This is the country code origin of the Beer."
}
},
parameters: [{
in: "header",
name: "Authorization",
required: true,
type: "string",
description: "Bearer {{access_token}}"
}],
...L'outil génère un fichier swagger.json....

qui peut être lu sur : editor.swagger.io ...
... ou être utilisé pour générer une documentation avec Redoc

Lecture du fichier swagger.json pour générer une APIdoc avec Redoc
node ./node_modules/redoc-cli/index.js bundle
--output apidoc/swagger/apidoc.html
--title 'Mon API API Reference'
swagger/swagger.jsonRésultat : (fichier apidoc.html)

Merci :)
Questions ?

Erwan GÉRÉEC
__Erwan
SailsJS
By Erwan Gereec
SailsJS
- 420