Micro Services
REX de la réalisation d'un service
d'envoi de sms

Les objectifs
- Microservice
- inspiration de MGDIS
- Utiliser au maximum les technos mise en oeuvre
- pouvoir utiliser le plein potentiel
- identifier les difficultés
- Pouvoir extrapoler pour d'autres services
- notamment les référentiels
Les Technos et outils

Serveur

Base de données

UI


Environnement


Autres outils




Autres librairies
- fast-json-patch (validation de schema json)
- basic-auth (authentification basic)
- jsonwebtoken (authentification jwt)
- mongoose (dao mongo)
- rxjs (implémentation de ReactiveX pour js)
- http-errors (gestion des erreurs http)


Gestion des sources

L'environnement de développement
Éviter le syndrôme
"chez moi ça marche"
Les modules npm
- Ne rien installer en global (cf blog)
- prohiber l'utilisation du
# npm install -g xxx
utiliser exclusivement
# npm install -save[-dev] - certain CLI n'aime pas être que local
# export PATH=$PATH:./node_modules/.bin
- prohiber l'utilisation du
- exception pour les générateurs qui ne servent que pour initialiser un projet
La base de données
- Docker pour faire tourner la base de données sans installation (cf dépôt)
- Des données pour chaque type de déploiement
- test, développement,...
- Switch facile (voir automatique) lors des tests
- Plus léger qu'une VM
La base de données
Utilisation de docker
#!/usr/bin/env bash
mode=`docker ps|grep mongo_myapp|cut -d _ -f 4`
if [[ $mode = test || $mode = dev ]]
then
echo "kill $mode"
docker kill mongo_myapp_$mode > /dev/null 2>&1
fi
if [[ $mode = test ]]
then
docker start mongo_myapp_dev > /dev/null 2>&1
echo "now it's dev "
else
docker start mongo_myapp_test > /dev/null 2>&1
echo "now it's test "
fi
$ docker run -d \
--name mongo_myapp_dev \
-p 27017:27017 \
-e MONGODB_USERNAME=myusernamefordev \
-e MONGODB_PASSWORD=mypasswordfordev \
-e MONGODB_DBNAME=mydbfordev \
mongo_micro
Autres ressources
Le multi tenant
Implémentations possibles
- duplication de service
- une base par tenant
- une collection (table) par tenant
- chaque objet contient l'id de son tenant
Configurations possibles
- 4 > 3
- client voulant que ses données soient isolées
ou client gros consommateur- 1+2 on duplique le service pour lui et on le met sur une base dédiée
Le design de l'API
Découverte du service
-
GET /admin retourne l'information générale du service (title + description) et dans l'entête Link une liste d'urls permettant de découvrir le service (les liens ci-après)
-
GET /admin/status retourne l'état courant du service et de ses dépendances (base de données,...)
-
GET /admin/version retourne la version du service
-
GET /admin/license retourne la license d'exploitation du service
-
GET /admin/roles retourne les différents rôles gérés par le service
-
GET /admin/swagger.json retourne la définition swagger de l'API complète du service
Administration des tenants
CRUD RESTful sur les tenants
-
GET /admin/tenants récupération de la liste des tenants
-
POST /admin/tenants création d'un tenant
-
GET /admin/tenants/{tenantId} récupération d'un tenant
-
PUT /admin/tenants/{tenantId} mise à jour d'un tenant
-
PATCH /admin/tenants/{tenantId} mise à jour partiel d'un tenant
-
DELETE /admin/tenants/{tenantId} suppression d'un tenant
Configuration des tenants
CRUD RESTful sur les tenants
-
configuration "privée" uniquement accessible à l'administrateur du service
/admin/tenants/{tenantId}/settings -
configuration "publique" accessible au manager du tenant
/{tenantId}/settings
Extras des tenants
La configuration du tenant accessible au manager peut aussi comprendre :
- i18n : /{tenantId}/i18n/{lang} : permet de modifier certains textes en fonction du tenant et de la langue
- authentication : /{tenantId}/authentication : permet de définir les paramètres du système d'authentification pour le tenant
- roles : /{tenantId}/roles/mapping : permet de définir le mapping entre les groupes issus de l'Account-Management et les rôles du service du tenant
- status : /{tenantId}/status : permet de connaître l'état de fonctionnement du tenant
- ...
Swagger
Modéliser l'API
Historique
- Swagger 2
- OpenAPI 3


Intérêt
- Fournit le modèle de l'API proposée par le service 👍👍
- Permet de générer le squelette du serveur
- contrôleur & router
- mock
- test
Structure d'un projet swagger
- /api
- /controllers
- /helpers
- /mocks
- /swagger
- swagger.yaml
- /config
- default.yaml
- /test
- app.js
- package.json
Bagpipe
- moteur de flux enchaînant des traitements de données
- configuration > programmation
- enchaîner ou paralléliser des appels
- à des webservices
- à des fonctions personnalisées
swagger utilise
Pipe swagger définit dans default.yaml
# swagger configuration file
# values in the swagger hash are system configuration for swagger-node
swagger:
fittingsDirs: [ api/fittings ]
defaultPipe: null
swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers
# values defined in the bagpipes key are the bagpipes pipes and fittings definitions
# (see https://github.com/apigee-127/bagpipes)
bagpipes:
_router:
name: swagger_router
mockMode: false
mockControllersDirs: [ api/mocks ]
controllersDirs: [ api/controllers ]
_swagger_validate:
name: swagger_validator
validateResponse: true
# pipe for all swagger-node controllers
swagger_controllers:
- onError: json_error_handler
- cors
- swagger_security
- _swagger_validate
- express_compatibility
- _router
# pipe to serve swagger (endpoint is in swagger.yaml)
swagger_raw:
name: swagger_raw
Pipe swagger
cors
security
validate
express compatibility
router
security handler
controllers
Security
On ajoute les contrôleurs d'accès
- Bearer basé sur jwt
- Basic basé sur usr+pwd
securityDefinitions:
Bearer:
description: |
For accessing the API a valid JWT token must be passed in all the queries in
the 'Authorization' header.
A valid JWT token is generated by the *Account Management* after giving a
valid user & password.
The following syntax must be used in the '**Authorization**' header : **Bearer xxxxxx.yyyyyyy.zzzzzz**
type: apiKey
name: Authorization
in: header
Basic:
description: For accessing the API a valid username & password must be passed in all the queries header.
type: basicconst configSwagger = {
appRoot: __dirname, // required config
swaggerSecurityHandlers : {
Bearer: (req, authOrSecDef, authorizationHeader, callback) => { ... },
Basic: (req, authOrSecDef, authorizationHeader, callback) => { ... }
}
};
SwaggerExpress.create(configSwagger, (err, swaggerExpress) => {
Controlers
On ajoute les contrôleurs
- À chaque famille d'opérations un contrôleur
- À chaque opération une fonction
Ajout d'un middleware
dans le pipe
_middlewareConfigured:
name: myMiddleware
myParameter: 42
swagger_controllers:
- onError: json_error_handler
- cors
- myMiddleware
- _middlewareConfigured
- swagger_security
- _swagger_validate
- express_compatibility
- _routermodule.exports = (cfg) => (context, next) => {
console.log(cfg);
//...
if (err) {
const error = Error('unknow');
error.statusCode = 404;
error.code = "UNKNOW-ERROR";
next(error);
}
else {
next(); // ou next(null, result);
}
};
{ name: 'myMiddleware', input: undefined }
{ name: 'myMiddleware', myParameter: 42 }contexte d'exécution contient notamment
context.request context.response
La callback à appeler pour passer à la suite du pipe
Ajouts d'autres middlewares
dans le pipe
- préchargement du tenant
- désactivation automatique d'un tenant
- ...
# pipe for all swagger-node controllers
swagger_controllers:
- onError: json_error_handler
- cors
- tenantLoader
- tenantDisabler
- swagger_security
- _swagger_validate
- authorizationSetter
- express_compatibility
- _routerAjouter des paramètres
dans swagger.yaml
/{tenant}/settings:
x-swagger-router-controller: tenantSettings
get:
summary: Get the settings
description: Get the settings of the tenant
tags:
- Management
- API
- Tenant
operationId: getTenantSettings
x-tenantLoader: tenant
x-tenantDisabler: true
security:
- Bearer: []
- Basic: []
x-allowedRoles:
roles: [adm, mng]
produces:
- application/json
parameters:
- name: tenant
in: path
description: Le nom du tenant
required: true
type: stringPréchargement de tenant
- Charger le tenant lorsque c'est possible
- Disposer du tenant pour les middlewares suivants
module.exports = () => (context, next) => {
const swagger = context.request.swagger;
const tenantPrm = swagger.operation['x-tenantLoader'];
const tenantId = tenantPrm && swagger.params[tenantPrm] && swagger.params[tenantPrm].value || null;
if(tenantId !== null) {
tenants.get(tenantId)
.then(tenant => {
createParam(context.request, "tenant", { value: tenant, err: null });
next();
})
.catch(err => {
error(`unknow tenant "${tenantId}"`);
createParam(context.request, "tenant", { value: null, err });
next();
});
}
else {
next();
}
};
/{tenant}/settings:
get:
x-tenantLoader: tenant /{tutu}/settings:
get:
x-tenantLoader: tutuExpress vs. Bagpipe
Différence entre middlewares
const myMiddleware =
(req, res, next) => {
//...
next();
}module.exports =
(cfg) => (context, next) => {
//...
next();
};
const express = require('express');
const app = express();
app.use(myMiddleware); _middlewareConfigured:
name: myMiddleware
myParameter: true
swagger_controllers:
...
- myMiddleware
- _middlewareConfigured
...const myMiddleware =
(cfg) => (req, res, next) => {
//...
next();
}const cfg = { /*...*/ }
app.use(myMiddleware(cfg));app.use('/path/', myMiddleware);Validation des données de l'API
Swagger generator
est-ce si intéressant ?
Le générateur de swagger est-il utile ?
Qu'est-ce que cela apporte au final ?
- génère le projet du serveur 😊
- valide les entrées et sortie 😊
(en fait une simple ligne avec ajv) - impose sa CLI 😞
(pour exécuter et mettre à jour le serveur, nodemon le fait aussi) -
utilise bagpipe 😞
(système magique pas plus pratique que les middlewares express) -
ne sait pas travailler avec la configuration 😞
(veut modifier la configuration qui est un objet immutable !) - ne prend pas encore en charge OpenAPI 3 😞

Qui gagne la guerre des CLI ?
Chaque outils propose son CLI
(swagger, ng, node, mocha,...)

Il faut revenir à la racine : node
=> donc pas de swagger-generate
Ils jouent à Highlander
Retour aux sources
Utiliser Node en direct
Quels middlewares
- Gérer l'accès par jwt et les roles
- Effectuer le préchargement du tenant
- Désactiver automatiquement le tenant
Title Text
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin urna odio, aliquam vulputate faucibus id, elementum lobortis felis. Mauris urna dolor, placerat ac sagittis quis.
Les logs
avec lib debug
Besoins de logs
- avoir la date
- identifier la provenance
- chaque service doit avoir son identifiant
- avoir différents niveaux
- gravité
trace, debug, info, warning, error,... - fonctionnel
user, settings, models,...
- gravité
$ npm install --save debugInstallation
Exemple de la lib debug
Utilisation
const debug = require('debug')("myapp:db");
const error = require('debug')("myapp:error:db");
//...
const errorHandler = err => {
error(`Mongoose default connection error: ${err}`);
};
const disconnectHandler = () => {
debug('Mongoose default connection disconnected');
};$ DEBUG="myapp:*" node app.js
...
myapp:db Mongoose default connection disconnected +34ms
...
myapp:error:db Mongoose default connection error: unable to connect +121ms
...Configuration
Comment...
Configuration
Qu'est-ce que c'est ?
- définir des valeurs de paramètres
- initialiser le service
Exemple de la lib config
Cette librairie permet de :
- gérer les différents environnements
- test
- dev
- prod
- machine
- hiérarchique
(système de poupées russes) -
json ou yaml
(le second est préférable car on peut y mettre pleins de commentaires)
const debug = require('debug')("myapp:info");
const express = require('express');
const config = require('config');
const app = express();
const port = config.get('port');
app.listen(port);
debug(`try this: curl http://127.0.0.1:${port}/`);# port du service
port: 10010{
"port": 80
}/config/default.yaml
/config/test.json
/app.js
$ DEBUG="myapp:*" node app.js
myapp:info try this: curl http://127.0.0.1:10010/ +34ms
^C
$ DEBUG="myapp:*" node test
...
myapp:info try this: curl http://127.0.0.1:80/ +32ms
...Choisir la configuration
- Depuis le code
process.env.NODE_ENV = 'test';NODE_ENV=generateKey node test/api/helpers/security/generateAuthorization.js- Depuis la ligne de commande
Variables d'environnement
- Simplicité pour s'adapter à ... l'environnement
- Particulièrement adapté aux containers docker
const db_name = process.env.HOST || "127.0.0.1";
//...
process.env.NODE_ENV = 'test';Utilisation
Dans le code
$ docker run -d \
--name my_app_run \
-e HOST=mymicroservice.com \
my_app
Pour lancer le container
Stratégie de configuration
Port du serveur
- variable d'environnement PORT
ou si pas définie - variable de configuration port
ou si pas définie - "10010"
Host du serveur
- variable d'environnement HOST
ou si pas définie - variable de configuration host
ou si pas définie - "127.0.0.1"
//...
const host = process.env.HOST || config.get('host') || "127.0.0.1";
const port = process.env.PORT || config.get('port') || "10010";
app.listen(port);
debug(`try this: curl http://${host}:${port}/`);
//...Mix Var.Env. & Config.
Directement dans le code
{
"port": "PORT",
"host": "HOST"
}
Mix Var.Env. & Config.
Dans la config : custom-environment-variables.json
//...
const host = config.get('host') || "127.0.0.1";
const port = config.get('port') || "10010";
app.listen(port);
debug(`try this: curl http://${host}:${port}/`);
//...Dans le code (rien ne change)
Ne pas oublier
- l'ordre de priorité
- lorsque plusieurs façons d'initialiser
- lors de la définition des fichiers de configuration
- fournir des valeurs par défaut
- qui vont bien
- ou qui arrête le service proprement (ou ne le démarre pas)
La programmation réactive
avec Promise et RxJS
Promesse
- "une valeur/appel asynchrone"
- permet de gérer les appels asynchrones
- évite le "callback hell"
- Promise objet natif dans ES6
- sera replacé par async await (mais pas tout de suite)
const promise = new Promise((resolve, reject) => {
// quelque chose d'asynchrone
setTimeout(() => {
if (/* est-ce que tout est ok ?*/) {
resolve("ok");
}
else {
reject(Error("erreur"));
}
}, 3000);
});
promise.then(result => {
console.log(result);
}, err => {
console.log(err);
});RxJS
- "une série de valeurs asynchrone"
- utilise les concepts de la programmation fonctionnelle
- map, filter, concat, interval, merge, count, debounce, delay, distinct,...
- objets de base
- Observable (produit des valeurs asynchrones)
- Observer (consomme des valeurs)
-
Subject ( = Observer U Observable)
seront des objets natifs dans ES7
- utilisé par angular
Persistance
Snapshot/Incrémentale
Snapshot
On ne garde que le dernier état de la ressource
- simple
- la ressource est immédiatement disponible
- permet peu de choses
- requêtes simples
Incrémentale
On stocke toutes les modifications réalisées sur la ressource
- complet : qui, quoi, quand
- besoin de reconstituer la ressource
- permet de retracer l'historique
- requêtes complexes
Rappel sur JSONPatch
- JSONPatch permet de décrire des opérations de changement d'un json
- JSONPointer permet de décrire un "chemin" sur un nœud du json
{
"biscuits": [
{ "name": "Digestive" },
{ "name": "Choco Leibniz" }
],
"foo/bar~": "baz",
"": "empty key"
}"" => racine de l'objet
"/" => "empty key"
"/biscuits" => tableau
"/biscuits/1/name" => "Choco Leibniz"
"/foo~1bar~0" => "baz"
"/biscuits/-" => fin du tableau, utile pour ajouter à la finRappel sur JSONPatch
Liste des opérations
{ "op": "add", "path": "/biscuits/1", "value": { "name": "Ginger Nut" } }{ "op": "remove", "path": "/biscuits/0" }
{ "op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive" }
{ "op": "copy", "from": "/biscuits/0", "path": "/best_biscuit" }
{ "op": "move", "from": "/biscuits", "path": "/cookies" }{ "op": "test", "path": "/best_biscuit/name", "value": "Choco Leibniz" }-
add
-
remove
-
replace
-
copy
-
move
-
test
Utilisation de mongoose
Avec node la librairie de référence pour mongo est mongoose
Elle permet :
- d'associer un schéma à un type d'objet
- d'effectuer les requêtes
Utilisation de mongoose
Stocker un object de type :
{
"id": "un id unique type uuid",
"des": "valeurs",
"quelconques": "comme des ",
"chiffres": 0
}Snapshot
Incrémentale
const entitySchema = new mongoose.Schema({
id: {
type: String,
required: true,
},
des: {
type: String
},
quelconques: {
type: mongoose.Schema.Types.Mixed
},
chiffres: {
type: mongoose.Schema.Types.Number
}
});const objectPatch = new mongoose.Schema({
id: {
type: String,
required: true,
},
//ownerId: {
// type: String,
// required: true,
//},
userId: {
type: String,
required: true,
},
date: {
type: mongoose.Schema.Types.Date,
required: true,
},
patch: [
mongoose.Schema.Types.Mixed
]
});Utilisation de mongoose
Déclaration du schéma
(générique)
Snapshot
Incrémentale
const EntityModel =
mongoose.model(
'entity',
entitySchema ,
'entities'
);const objectPatchModel =
(name, collection) =>
mongoose.model(
name,
objectPatch ,
collection || name
);
Utilisation de mongoose
Déclaration du modèle
(générique)
Snapshot
Incrémentale
{
"_id": ObjectId("5a61...df5f"),
"id": "un id unique type uuid",
"des": "valeurs",
"quelconques": "comme des ",
"chiffres": 42
}{
"_id": ObjectId("5a61...df5f"),
"id": "un id unique type uuid",
"userId": "162c5c24-...718b",
"date": ISODate("20...T...78Z"),
"patch": [
{
"op": "replace",
"path": "/chiffres",
"value": 42
}
]
}Utilisation de mongoose
Ce qui est sstocké lors d'un changement de la valeur de chiffres en 42
Requête
Snapshot
const getAll = () =>
EntityModel
.find({});const get = id =>
EntityModel
.findOne({ id })
.then(entity => {
if (entity === null)
throw Error(`NOT_FOUND no such entity ${id}`);
return entity;
});Requête
Snapshot
const update = (entity) =>
EntityModel
.findOneAndUpdate({ id: entity.id }, entity)
.then(e => {
if (e === null)
throw Error(`NOT_FOUND no such entity ${entity.id}`);
return e;
});const create = (id, newEntity) =>
EntityModel.findOne({ id: newEntity.id }, {id: 1})
.then(entity => {
if (entity !== null) {
throw Error(`ALREADY_EXISTS the entity "${newEntity.id}" already exists`);
}
debug(`create the entity "${newEntity.id}"`);
return EntityModel.create(newEntity);
});Requête
Incrémentale
const get = (id) =>
objectPatchModel
.aggregate()
.match({ id }) // recherche tous les patches sur l'entité
.sort({ date: 1 }) //trie les résultats par date
.unwind("$patch") //applati tous les patch
.group({ _id: "$id", patch: { $push:"$patch" } }) // regroupe tous les patch en un tableau
.exec())
.then(result => {
if(result === null || result.length === 0) {
throw error(NOT_FOUND, message: `no such entity ${id}`);
}
//reconstruction de l'entité
return jsonpatch.applyPatch({ id }, result[0].patch).newDocument;
});Requête
Incrémentale
const getAll = (id) =>
objectPatchModel
.aggregate()
.match({}) // recherche tous les patches sur l'entité
.sort({ date: 1 }) //trie les résultats par date
.unwind("$patch") //applati tous les patch
.group({ _id: "$id", patch: { $push:"$patch" } }) // regroupe tous les patch en un tableau
.exec())
.then(result => {
const entities = [];
for(let r of result) {
entities.push(jsonpatch.applyPatch({ id: r._id }, r.patch).newDocument);
}
return entities;
});Requête
Incrémentale
const createPatch = (id, userId, prevEntity, newEntity) => {
const patch = jsonpatch.compare(prevEntity, newEntity);
return patch.length === 0 ?
Promise.resolve() :
objectPatchModel.create({
id,
userId,
date: new Date(),
patch,
})
.then((entry) => {});
};
const update = (userId, newEntity) =>
get(newEntity.id || "")
.then(prevEntity => {
return createPatch(newEntity.id, userId, prevEntity, newEntity);
});
Requête
Incrémentale
const createPatch = (id, userId, prevEntity, newEntity) => {
const patch = jsonpatch.compare(prevEntity, newEntity);
return patch.length === 0 ?
Promise.resolve() :
objectPatchModel.create({
id,
userId,
date: new Date(),
patch,
})
.then((entry) => {});
};
const patch = (entityId, userId, patch) => get(entityId || "")
.then(prevEntity => {
if (prevEntity === null) {
throw error(NOT_FOUND, `unknow horse`);
}
if (!validateJsonPatch(patch)) {
throw Error('BAD_REQUEST, bad patch format');
}
const newEntity = jsonpatch.applyPatch(prevEntity, patch).newDocument;
//TODO check entity schema
return createPatch(entityId, userId, prevEntity, newEntity);
});Test
Tests unitaires
Environnement de test
Pour que les tests soient reproductibles, l'environnement de test doit être immuable
- utiliser le même jeu de données d'un test à l'autre
- nettoyer et pré-remplir la base de données si nécessaire mongorestore
- image docker avec les fichiers nécessaires)
- utiliser une configuration spécifique aux tests
- pas interdit d'avoir plusieurs jeux de tests
Structure de test
- description
describe('controllers admin general', function() {it('should return general information', function() {- test
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
[1,2,3].indexOf(5).should.equal(-1);
[1,2,3].indexOf(0).should.equal(-1);
});
});
});- exemple
Array
#indexOf()
✓ should return -1 when the value is not presentStructure de test
- hooks pour le cycle de vie
// Avant chaque test
beforeEach(function() { /* initialiser quelque chose */ }
// Après chaque test
afterEach(function() { /* terminer quelque chose */ }
// Avant le test
before(function() { /* initialiser quelque chose */ }
// Après le test
after(function() { /* terminer quelque chose */ }beforeEach(function(done) {
// initialiser quelque chose asynchrone
done();
});
beforeEach(function() {
// initialiser quelque chose synchrone
});- synchrone/asynchrone
Écriture de tests
- supertest
const server = require('../../../app');
const request = require('supertest');
it('test API', function(done) {
request(server)
.get('url')
.set('Header', 'value')
.expect('Header', value')
.expect(200) // ou 3.., 4.., 5..
.end(function(err, res) {
// tests ...
done();
});
});Écriture de tests
- should
const should = require('should');
//...
.end(function(err, res) {
should.not.exist(err);
should.exist(res);
done();
});
//...res.body.should.match({
name: /notifSender/,
version: v => v.should.match({
number: /^\d*\.\d*\.\d*$/,
build_type: /^(debug|release)$/,
build_number: /^\d*$/
})
});should.not.exist(err);
res.body.should.have.match({
status: REGEX_STATUS,
dependencies: d => d.should
.be.instanceof(Array)
.and
.matchEach({
id: id =>
id.should.be.a.String,
description: descr =>
descr.should.be.a.String,
status: REGEX_STATUS,
})
});res.body.should.have.properties('title', 'description');Les entêtes de réponse
- Sensibilité à la casse
Les tests sont case sensitive or les headers sont case insensitive
=> ajout d'une méthode de test-
Au lieu de
res.header.should.have.property('link').match(REGEX_LINK); -
On utilisera
propertyCI(res.header.should.have, 'Link').match(REGEX_LINK);
-
module.exports = (assertion, name) => {
name = name.toLowerCase();
let obj = assertion.obj;
for(let p in obj) {
if(obj.hasOwnProperty(p) && name === p.toLowerCase()) {
assertion.obj = obj[p];
return assertion;
}
}
assertion.assert(false);
return assertion;
};res.header.should.have.property('link').match(REGEX_LINK);propertyCI(res.header.should.have, 'Link').match(REGEX_LINK);Exemple réel de test d'API
describe('controllers admin general', function() {
describe('admin', function() {
describe('GET /admin', function() {
it('should return general information', function(done) {
request(server)
.get('/admin')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.end((err, res) => {
should.not.exist(err);
res.body.should.have.properties('title', 'description');
propertyCI(res.header.should.have, 'Link').match(REGEX_LINK);
done();
});
});
});
...controllers admin general
admin
GET /admin
✓ should return general information
...Que tester ?
- tester
- API
- middlewares
- helpers
- ...
- tester différents cas de figure
- le fonctionnement nominal
ok quand il y a les bons paramètres - la gestion des cas d'erreurs
erreur quand il y a de mauvais paramètres
- le fonctionnement nominal
Outils
Plusieurs systèmes existent
- moteurs : mocha, jasmine, karma,...
- assertions : should, sinon, must,...
- requêteurs : supertest, http,...
difficile de s'y retrouver
Les questions
auxquelles il faut répondre
JS ou TS ?
JS de base (ES5)- ECMAScript 6 (ES6)
- TypeScript (TS)
-
le typage évite les erreurs en amont
- clarifie les contrats de méthodes et fonction
- améliore la qualité du code écrit
- Angular utilise TS
- tout typer est compliqué
- utilisation de .d.ts pour ajouter des types aux .js
- mettre any partout supprime l'intérêt du typage
=> partir sur un langage et s'y tenir
ng ou react ?
ng
- modulaire (bien pour les gros projets, le chargement dynamique permet de ne pas saturer le navigateur)
- apprentissage difficile (syntaxe absconse, débogage non trivial)
- impose sa CLI (difficile d'ajouter ng après coup)
react
- pas modulaire (on peut faire proprement)
- apprentissage aisé (quand on connait js)
- simple librairie js (facile à ajouter à un projet node existant moyennant un webpack)
À l'avenir il sera plus dur de trouver des compétences en ng qu'en react (un peut comme avec zend et symfony)
Webhook
Design
Version Configuration MGDIS
"webhooks": [
{
"topic": "PUT+*/entities/(:reference)",
"callback": "url/to/call/the/webhook/{reference}",
"method": "PUT",
"headers": {
"Authorization": "Basic YWRtaW46Q…zFXMGVtS1E="
},
"options": {
...
}
},
...
]
Webhooks dynamiques
- Créer une API RESTful de CRUD
- Le format doit (devrait) être le même que pour la configuration
- les options peuvent contenir
- une date de péremption
- un nombre d'utilisation
- un système de formatage de données (xslt)
les websockets
les websockets
- Intallation de ws
npm install --save ws- echo.websocket.org
const WebSocket = require('ws');
const ws = new WebSocket('wss://echo.websocket.org/', {
origin: 'https://websocket.org'
});
ws.on('open', function open() {
console.log('connected');
ws.send(Date.now());
});
ws.on('close', function close() {
console.log('disconnected');
});
ws.on('message', function incoming(data) {
console.log(`Roundtrip time: ${Date.now() - data} ms`);
setTimeout(function timeout() {
ws.send(Date.now());
}, 500);
});
Mise à jour récurrente
- API fournissant le status (sans websocket)
const StatusBuilder = require('../helpers/statusBuilder');
function status(req, res) {
new StatusBuilder()
.addDependencie(require('../helpers/dbStatus'))
.getStatus()
.then(status => res.json(status));
}Mise à jour récurrente
- le service de récupération du status (sans websocket)
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Status } from '../models/status';
import io from 'socket.io-client';
const SERVER_URL = 'http://localhost:10010';
@Injectable()
export class HttpService {
constructor(
private url: string,
private http: HttpClient
) { }
public get(defaultValue?: Status) : Observable<Status> {
return this.http.get<Status>(this.url)
.pipe(
catchError(this.handleError(defaultValue))
);
}
private handleError(result?: T) {
return (error: any): Observable<T> => {
if(result === undefined) throw error;
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
}
Mise à jour récurrente
- appel récurrent au service (sans websocket)
...
export class AppStatusComponent implements OnInit, AfterViewInit, OnDestroy {
status : Status = new Status("Non disponible");
private interval: any = null;
constructor(private statusService: StatusService) { }
fetchStatus(): void {
this.statusService.get(new Status( "Service notifSender"))
.subscribe(status => { this.status = status; });
}
ngOnInit() {
this.fetchStatus();
this.interval = interval(10000);
}
ngAfterViewInit(): void {
this.interval.subscribe(() => {
this.fetchStatus();
});
}
ngOnDestroy(): void {
this.interval.unsubscribe();
}
}
les websockets
- envoyer le status régulièrement
const io = require('socket.io');
const StatusBuilder = require('../helpers/statusBuilder');
module.exports =
server => {
const wss = io(server);
let prevStatus = null;
wss.on('connect', function connection(ws) {
wss.emit('status', prevStatus);
});
setInterval(function timeout() {
new StatusBuilder()
.addDependencie(require('../helpers/dbStatus'))
.getStatus()
.then(status => {
if (prevStatus === null || prevStatus.status !== status.status) {
prevStatus = status;
wss.emit('status', status);
}
});
}, 1000);
};les websockets
- le service de mise à jour
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Status } from '../models/status';
import io from 'socket.io-client';
const SERVER_URL = 'http://localhost:10010';
@Injectable()
export class SocketService {
private socket;
public initSocket(): void {
this.socket = io(SERVER_URL);
}
public onMessage(): Observable<Status> {
return new Observable<Status>(observer => {
this.socket.on('status', (data: Status) => observer.next(data));
});
}
}
les websockets
- L'affcihage
...
export class AppStatusWSComponent implements OnInit {
status : Status = new Status("Non disponible");
constructor(private statusService: SocketService) { }
fetchStatus(): void {
this.statusService.initSocket();
this.statusService.onMessage()
.subscribe(status => { this.status = status; });
}
ngOnInit() {
this.fetchStatus();
}
}
Ce qui manque
dans la suite
Atelier logiciel
- Pas d'analyse de code avec sonarqube
- Pas d'automatisation dans gitlab
Déploiement
- Déploiement docker
- Automatisation du déploiement
Analyse de log
- Utiliser un système de logs type ELK
- ElasticSearch
- Logstash
- Kibana
- Permet de naviguer dans les logs
MEAN mise en œuvre
By Benoît Chanclou
MEAN mise en œuvre
Utilisation de swagger pour développer un service avec la stack MEAN (mongo, express, angular, node). La partie back est détaillée, la partie front est à peine abordée.
- 727