#!/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
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
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
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
La configuration du tenant accessible au manager peut aussi comprendre :
swagger utilise
# 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
cors
security
validate
express compatibility
router
security handler
controllers
On ajoute les contrôleurs d'accès
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) => {
On ajoute les contrôleurs
_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
# pipe for all swagger-node controllers
swagger_controllers:
- onError: json_error_handler
- cors
- tenantLoader
- tenantDisabler
- swagger_security
- _swagger_validate
- authorizationSetter
- express_compatibility
- _router/{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: stringmodule.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: tutuconst 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);Qu'est-ce que cela apporte au final ?
Chaque outils propose son CLI
(swagger, ng, node, mocha,...)
Il faut revenir à la racine : node
=> donc pas de swagger-generate
Ils jouent à Highlander
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.
$ npm install --save debugInstallation
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
...Qu'est-ce que c'est ?
Cette librairie permet de :
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
...process.env.NODE_ENV = 'test';NODE_ENV=generateKey node test/api/helpers/security/generateAuthorization.jsconst db_name = process.env.HOST || "127.0.0.1";
//...
process.env.NODE_ENV = 'test';Dans le code
$ docker run -d \
--name my_app_run \
-e HOST=mymicroservice.com \
my_app
Pour lancer le container
Port du serveur
Host du serveur
//...
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}/`);
//...Directement dans le code
{
"port": "PORT",
"host": "HOST"
}
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)
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);
});On ne garde que le dernier état de la ressource
On stocke toutes les modifications réalisées sur la ressource
{
"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 finListe 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" }Avec node la librairie de référence pour mongo est mongoose
Elle permet :
Stocker un object de type :
{
"id": "un id unique type uuid",
"des": "valeurs",
"quelconques": "comme des ",
"chiffres": 0
}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
]
});const EntityModel =
mongoose.model(
'entity',
entitySchema ,
'entities'
);const objectPatchModel =
(name, collection) =>
mongoose.model(
name,
objectPatch ,
collection || name
);
{
"_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
}
]
}Ce qui est sstocké lors d'un changement de la valeur de chiffres en 42
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;
});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);
});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;
});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;
});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);
});
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);
});Pour que les tests soient reproductibles, l'environnement de test doit être immuable
describe('controllers admin general', function() {it('should return general information', function() {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);
});
});
});Array
#indexOf()
✓ should return -1 when the value is not present// 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
});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();
});
});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');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);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
...Plusieurs systèmes existent
difficile de s'y retrouver
=> partir sur un langage et s'y tenir
ng
react
À l'avenir il sera plus dur de trouver des compétences en ng qu'en react (un peut comme avec zend et symfony)
"webhooks": [
{
"topic": "PUT+*/entities/(:reference)",
"callback": "url/to/call/the/webhook/{reference}",
"method": "PUT",
"headers": {
"Authorization": "Basic YWRtaW46Q…zFXMGVtS1E="
},
"options": {
...
}
},
...
]
npm install --save wsconst 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);
});
const StatusBuilder = require('../helpers/statusBuilder');
function status(req, res) {
new StatusBuilder()
.addDependencie(require('../helpers/dbStatus'))
.getStatus()
.then(status => res.json(status));
}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);
};
}
}
...
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();
}
}
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);
};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));
});
}
}
...
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();
}
}