Site icon Le blog Tech d'Indy

Comment se passer de Mongoose ?

Avantages / inconvénients de Mongoose

Avantages

Inconvénients

L’équipe de MongoDB a fait un comparatif assez complet ici : MongoDB & Mongoose: Compatibility and Comparison

Validation des données : Json Schema vs mongoose

Mongoose :

new Schema({
	firstName: { type: String, required: true, min: 2, },
	lastName: { type: String, required: true, min: 2, },
	phone: {
    type: String,
    validate: {
      validator: function(v) {
        return /\\\\d{3}-\\\\d{3}-\\\\d{4}/.test(v);
      },
      message: props => `${props.value} is not a valid phone number!`
    },
    required: [true, 'User phone number required']
  },
});

Json Schema :

{
	properties: {
		firstName: { type: 'string', minLength: 2 },
		lastName: { type: 'string', minLength: 2 },
		phone: { type: 'string' },
	},
	required: ['firstName', 'lastName'],
}

On voit ici que JSON Schema ne permet pas d’implémenter des logiques de validations personnalisées puisque que ce sont des schemas statiques. Ils ont ainsi l’avantage d’être language agnostic ou encore de pouvoir être retourné par une API à un client qui pourra l’exploiter.

Ici il faudra alors valider le format du champ phone au niveau du code métier de l’application.

Utiliser la validation MongoDB Json Schema

Depuis la version 3.2, il est possible d’enforcer la validation d’un document MongoDB à partir d’un JSON Schema (Specification | JSON Schema), bien que ce ne soit vraiment utilisable que depuis la v5 qui rajoute la raison pour laquelle une validation a été rejetée.

{
   "code": 121,
   "errmsg": "Document failed validation",
   "errInfo": {
       "failingDocumentId": ObjectId("5fe0eb9642c10f01eeca66a9"),
       "details": {
           "operatorName": "$jsonSchema",
           "schemaRulesNotSatisfied": [
               {
                   "operatorName": "properties",
                   "propertiesNotSatisfied": [
                       {
                           "propertyName": "price",
                           "details": [
                               {
                                   "operatorName": "minimum",
                                   "specifiedAs": {
                                       "minimum": 0
                                   },
                                   "reason": "comparison failed",
                                   "consideredValue": -2
                               }
                           ]
                       }
                   ]
               }
           ]
       }
   }
}

Dans l’exemple ci-dessus on peut voir que la validation a échoué puisque le champ price ne pouvait pas avoir une valeur inférieure à 0.

Ajouter un Json Schema à une collection MongoDB

Il est possible d’ajouter une validation Json Schema a une collection existante :

db.runCommand({
   collMod: "users",
   validator: {
		$jsonSchema: {
      	bsonType: "object",
      	required: ["first_name", "last_name"],
      	properties: {
				first_name: { bsonType: 'string', minLength: 2 },
				last_name: { bsonType: 'string', minLength: 2 },
      	}
   	},
	},
});

Par défaut, MongoDB applique la validation sur TOUS les documents existants, incluant ceux qui ne respectent pas le schema de validation (si votre model a évolué au fil du temps par exemple, attention à ce que le schema ne soit pas trop restrictif). Il est possible de demander à MongoDB de n’enforcer la validation que sur les documents existants qui respectent les règles de validation en rajoutant validationLevel: ‘moderate’ à la query (par défaut sa valeur est strict).

Plus de détails dans la documentation de MongoDB : Schema Validation — MongoDB Manual

Intégration dans une application NodeJS

En prenant cet exemple d’application Node ExpressJs qui utilise Mongoose : Node.js (forked) – StackBlitz

Intégrer le client MongoDB

Installer le client : npm i —save mongodb.

On vient mettre à jour initDatabase pour utiliser le driver :

async function initDatabase() {
  const client = new MongoClient(connectionString, {
    serverSelectionTimeoutMS: 1000,
  });

  await client.connect();

  const db = client.db(config.dbName);

  await createCollections({ db, models }); // une première étape qui permet de créer les collections en base si nécessaire

  await registerJsonSchemaValidators({ db, models }); // une deuxième étape dans laquelle on vient enregistrer les json schemas de validation pour chaque collection

  loadDriverCollections({ db, models }); // et une dernière étape qui va nous servir à garder en mémoire l’instance de chaque db.collection du client MongoDB (ce sera plus clair par la suite).
}

Ici on rajoute trois étapes en plus de la connexion à la base

Comme l’idée ici est de pouvoir migrer facilement de Mongoose au MongoDB driver, il va falloir faire en sorte de minimiser les différences d’utilisation entre notre librairie et Mongoose, notamment pour ce qui est d’utiliser les models.

Nous allons donc exposer une fonction registerModel qui aura à la fois comme but de créer le model en interne, mais aussi d’en récupérer une instance pour pouvoir l’utiliser directement, comme le propose mongoose :

const UserModel = require('../models/User');

function findOneUserById({ userId }) {
	return findOne({ _id: userId });
}

L’implémentation de la fonction registerModel :

const models = {};

function registerModel(name, { schema }) {
  Object.assign(models, {
    [name]: {
      name,
      schema,
      collection: {}, // mongodb driver collection instance
    },
  });

  return models[name].collection;
}

Premièrement, on vient rajouter un object models qui nous permettra de garder en mémoire au sein du module les models enregistrés par l’application.

Pour l’exemple, le format d’un model est très simple puisqu’on voit qu’il ne se compose que d’un name et d’un schema (le JSON schema). On pourrait très bien imaginer rajouter d’autres propriétés telles que indexes nous permettant de créer des index…

On initialise un model vide à l’appel de registerModel grâce à Object.assign. La propriété collection est un object vide (qui contiendra une fois la connexion avec MongoDB établie l’instance de la collection du driver MongoDB) ce qui nous permettra de le modifier par référence un peu plus tard.

On peut maintenant définir un model comme suit :

const userSchema = {
  bsonType: 'object',
  properties: {
    email: { type: 'string', format: 'email' },
    password: { type: 'string', minLength: 8 },
  },
  required: ['email', 'password'],
};

module.exports = registerModel('user', {
  schema: userSchema,
});

On voit ici que le format email est supporté par JSON Schema ce qui est plutôt pratique.

La liste des types supportés par JSON Schema est disponible ici JSON Schema Reference — Understanding JSON Schema 2020-12 documentation

Il ne manque plus qu’à créer la collection en base (si nécessaire, on ne créer que celles qui n’existent pas) et à lui assigner un JSON Schema.

async function createCollections({ db, models }) {
  const existingCollections = await db
    .listCollections({}, { nameOnly: true })
    .toArray();
  const modelsCollectionsNames = _.map(models, 'name');
  const collectionsToCreate = _.difference(
    existingCollections,
    modelsCollectionsNames
  );

  const createCollectionsPromises = _.map(
    collectionsToCreate,
    (collectionName) => db.createCollection(collectionName)
  );

  return Promise.all(createCollectionsPromises);
}

On récupère tout d’abord la liste de noms des collections existantes en base (await db.listCollections({}, { nameOnly: true }).toArray()) puis on vient récupérer celles qui n’existent pas pour finalement les créer.

function registerJsonSchemaValidators({ db, models }) {
  const registerValidatorsPromises = _.map(models, ({ schema, name }) =>
    db.command({
      collMod: name,
      validator: { $jsonSchema: schema },
      validationLevel: 'strict',
    })
  );

  return Promise.all(registerValidatorsPromises);
}

Ensuite on vient mettre à jour notre référence à l’objet collection du MongoDB driver pour NodeJS :

function loadDriverCollections({ db, models }) {
  _.forEach(models, (model) => {
    const collection = db.collection(model.name);

    Object.assign(model.collection, collection);
  });
}

En terme d’usage, c’est très proche de ce que peut proposer Mongoose bien qu’il manque certains sucres syntaxiques tels que populate pour faire des aggregations ou encore sort et paginate. Il faudra plutôt utiliser un aggregate MongoDB pour les remplacer, ce qui a aussi l’avantage d’écrire des queries utilisables à la fois dans notre application que dans un Mongoshell ou autre client MongoDB.

Projet d’exemple : Mongodb driver – StackBlitz

Quitter la version mobile