Browse Source

Merge branch 'master' of git.unistra.fr:W4a/w4ab

Clément Krebs 5 years ago
parent
commit
ae7dc1bf6d
4 changed files with 298 additions and 1 deletions
  1. 1 1
      README.md
  2. 157 0
      TP3.md
  3. 116 0
      correction_tp1/server.js
  4. 24 0
      test_promises.js

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# W4b - Compléments de programmation web côté serveur / client
+# W4ab - Compléments de programmation web côté serveur / client
 Bienvenue dans ce module de programmation web :-)
 
 Vous trouverez dans ce dépôt les ressources nécessaires pour la réalisation des TP.

+ 157 - 0
TP3.md

@@ -0,0 +1,157 @@
+# Organisation extensible
+
+Créer les dossiers suivants :
+- models
+- controllers
+- routes
+
+## Modèles
+
+Le dossier `models` est organisé comme suit :
+- des fichiers `[nom_modele].js` contenant chacun la définition d'un modèle
+- un fichier `index.js` chargé d'initialiser l'objet sequelize puis de charger les définitions des modèles et de réaliser les associations
+
+Le code ci-après est celui du fichier `index.js`.
+Les commentaires devraient vous suffire à comprendre son fonctionnement.
+```js
+const
+fs = require('fs'),
+Sequelize = require('sequelize');
+
+// create Sequelize instance
+const sequelize = new Sequelize('[db_name]', '[username]', '[password]', {
+	host: '[hostname]',
+	port: 3306,
+	dialect: 'mysql',
+	dialectOptions: { decimalNumbers: true }
+});
+
+// this object will contain the model objects
+// each key being the model's name
+const db = {};
+
+// read the files of the current directory
+fs.readdirSync(__dirname)
+.filter((filename) => filename !== 'index.js') // avoid this file
+.forEach((filename) => {
+	const model = require('./' + filename)(sequelize); // get the model definition
+	db[model.name] = model; // add the entry in the db object
+});
+
+// go through each entry of the db object
+Object.keys(db).forEach((modelName) => {
+	// call the "associate" function on the model object
+	// and pass it the db object (so that it can have access to other models)
+	db[modelName].associate(db);
+});
+
+// sync the DB
+sequelize.sync();
+
+// expose the db object
+module.exports = db;
+```
+
+Dans ce code, on voit que l'on `require` chacun de nos fichiers dans lequel on définit un modèle.
+Ce code s'attend à ce que le type obtenu soit une fonction, que l'on appelle immédiatement en lui passant l'instance `sequelize`, et qui nous retourne le modèle défini.
+
+On observe également que l'on fait ensuite appel à la fonction `associate` sur chaque modèle.
+Cette fonction doit faire partie des méthodes de classe du modèle, elle reçoit l'objet `db` en paramètre et c'est à cet endroit que l'on va déclarer d'éventuelles relations entre ce modèle et d'autres modèles définis dans `db` (voir https://sequelize.org/v5/manual/models-definition.html#expansion-of-models pour la documentation sur l'ajout de méthodes aux modèles).
+
+Le code ci-dessous illustre un exemple de module définissant un modèle.
+```js
+const Sequelize = require('sequelize');
+
+module.exports = (sequelize) => {
+
+	class Bidule extends Sequelize.Model {
+		static associate(db) {
+			Bidule.hasMany(db.Machin);
+		};
+	}
+	
+	Bidule.init({
+		bla: Sequelize.STRING,
+		bli: Sequelize.STRING
+	}, {
+		sequelize,
+		modelName: 'Bidule'
+	});
+	
+	return Bidule;
+
+};
+```
+
+## Contrôleurs
+
+Chaque fichier du dossier `controllers` définit un module qui exporte un objet contenant des fonctions qui seront utilisées en tant que middleware de nos routes `express`.
+Comme on y utilise certainement les modèles, ces fichiers démarrent la plupart du temps par importer l'objet `db` contenant leur définition.
+La fonction `require` générant un singleton, chacun des fichiers contrôleur peut récupérer l'objet `db` sans risquer d'exécuter plusieurs fois le code de définition des modèles.
+
+Le code ci-dessous illustre un exemple de module définissant un contrôleur.
+```js
+const db = require('../models');
+
+module.exports = {
+	
+	get_all: (req, res, next) => {
+		return db.Bidule.findAll()
+		.then((bidules) => res.json(bidules))
+		.catch((err) => next(err));
+	};
+
+};
+```
+
+## Routes
+
+Le dossier `routes` est organisé comme suit :
+- des fichiers `[nom_routes].js` contenant chacun des déclarations de routes
+- un fichier `index.js` qui charge les fichiers précédents et enregistre les routes auprès de l'application `express`
+
+Voici du code pour le fichier `index.js` :
+```js
+const
+fs = require('fs');
+
+module.exports = (app) => {
+	
+	// read the files of the current directory
+	fs.readdirSync(__dirname)
+	.filter((filename) => filename !== 'index.js') // avoid this file
+	.forEach((filename) => {
+		// load routes array and register them
+		require('./' + filename).forEach((r) => {
+			app[r.method](r.url, r.func);
+		});
+	});
+
+};
+```
+
+Dans ce code, on voit que chaque module chargé exporte un tableau d'objets (utilisation de la fonction `forEach` sur le résultat du `require`) contenant des informations sur les routes à enregistrer.
+En particulier, on s'attend à ce que chaque objet du tableau contienne :
+- un champ `method` indiquant la méthode HTTP
+- un champ `url` indiquant l'url de la route
+- un champ `func` qui peut être un middleware ou un tableau de middlewares
+
+Le code ci-dessous illustre un exemple de module définissant des routes :
+```js
+const bidule_ctrl = require('../controllers/bidule');
+
+module.exports = [
+	
+	{
+		url: '/bidule',
+		method: 'get',
+		func: bidule_ctrl.get_all
+	}
+
+];
+
+```
+
+## Application Express
+
+Etant donné que les modules définissant les routes chargent les contrôleurs nécessaires, et que les modules définissant les contrôleurs chargent les modèles, l'application principale est réduite à la création de l'application `express`, le chargement du module `routes` et le lancement de l'écoute.

+ 116 - 0
correction_tp1/server.js

@@ -0,0 +1,116 @@
+const
+express = require('express'),
+Sequelize = require('sequelize'),
+bodyParser = require('body-parser');
+
+
+const app = express();
+
+
+app.use(bodyParser.json());
+
+
+// create Sequelize instance
+const sequelize = new Sequelize('w4a', 'w4a', 'w4aw4aw4a', {
+	host: 'localhost',
+	port: 3306,
+	dialect: 'mysql',
+	dialectOptions: { decimalNumbers: true }
+	// logging: false
+});
+
+// create models
+class Person extends Sequelize.Model { }
+
+Person.init({
+	lastname: Sequelize.STRING,
+	firstname: Sequelize.STRING
+}, {
+	sequelize,
+	modelName: 'Person'
+});
+
+// sync DB
+sequelize.sync();
+
+// create controllers
+const personCtrl = {
+
+	get_all: (req, res, next) => {
+		return Person.findAll({
+			order: ['lastname'] // sort results by lastname
+		})
+		.then((people) => res.json(people))
+		.catch((err) => next(err));
+	},
+
+	get_by_id: (req, res, next) => {
+		return Person.findByPk(req.params.person_id)
+		.then((person) => {
+			if (!person) {
+				throw { status: 404, message: 'Requested Person not found' };
+			}
+			return res.json(person);
+		})
+		.catch((err) => next(err));
+	},
+
+	create: (req, res, next) => {
+		const data = {
+			firstname: req.body.firstname || '', // use empty string if not defined
+			lastname: req.body.lastname || '' // use empty string if not defined
+		};
+		return Person.create(data)
+		.then((person) => res.json(person))
+		.catch((err) => next(err));
+	},
+
+	update_by_id: (req, res, next) => {
+		return Person.findByPk(req.params.person_id)
+		.then((person) => {
+			if (!person) {
+				throw { status: 404, message: 'Requested Person not found' };
+			}
+			Object.assign(person, req.body);
+			return person.save();
+		})
+		.then((person) => res.json(person))
+		.catch((err) => next(err));
+	},
+
+	delete_by_id: (req, res, next) => {
+		return Person.findByPk(req.params.person_id)
+		.then((person) => {
+			if (!person) {
+				throw { status: 404, message: 'Requested Person not found' };
+			}
+			return person.destroy();
+		})
+		.then(() => res.status(200).end())
+		.catch((err) => next(err));
+	}
+
+};
+
+// register routes
+app.get('/person', personCtrl.get_all);
+app.post('/person', personCtrl.create);
+app.get('/person/:person_id', personCtrl.get_by_id);
+app.put('/person/:person_id', personCtrl.update_by_id);
+app.delete('/person/:person_id', personCtrl.delete_by_id);
+
+// register error handling middleware
+app.use((err, req, res, next) => {
+	if (err.status === undefined) {
+		return res.status(500).send(err.message);
+	} else {
+		return res.status(err.status).send(err.message);
+	}
+});
+
+// launch server
+const server = app.listen(3000, () => {
+	const host = server.address().address;
+	const port = server.address().port;
+	console.log('App listening at http://%s:%s', host, port);
+});

+ 24 - 0
test_promises.js

@@ -0,0 +1,24 @@
+
+const add_later = (a, b) => {
+    return new Promise((resolve, reject) => {
+        setTimeout(() => {
+            if (a < 0 || b < 0) {
+                reject('Erreur: nombre négatif');
+            }
+            resolve(a+b);
+        }, 1000);
+    });
+};
+
+add_later(3, 4)
+.then((x) => {
+    console.log('result = ' + x);
+    return add_later(x, -5);
+})
+.then((x) => {
+    console.log('result = ' + x);
+})
+.catch((err) => console.log(err))
+.then(() => console.log('after catch'));
+
+console.log('blablabla');