import ErrorBuilder from "./ErrorBuilder"; /** * Classe permettant de gérer les tables simples */ class QueryBuilder { /** * Initialisation de la classe * @param {Object} params * @param {Object} models */ constructor(params, models) { this.params = params; this.models = models; this.includes = []; } /** * Méthode permettant de générer le lien vers une ressource * @param {String} href * @param {Number} currentPage * @param {Number} maxPage * @param {String} page */ static _createLink(href, currentPage, maxPage, page) { if ( (page === "first" && currentPage > 1) || (page === "last" && currentPage < maxPage) || (page === "prev" && currentPage > 1) || (page === "next" && currentPage < maxPage) ) { let newIndex = 0; switch (page) { case "first": newIndex = 1; break; case "last": newIndex = maxPage; break; case "prev": newIndex = currentPage - 1; break; case "next": newIndex = currentPage + 1; break; default: newIndex = currentPage; break; } return { href: href.replace(`page=${currentPage}`, `page=${newIndex}`), methdod: "GET" }; } return undefined; } /** * Méthde permettant de générer les élements de pagination * @param {Object} req * @param {Number} total * @param {Number} limit * @param {Number} skip * @return {Object} */ static _createPagination(req, total, limit, skip) { const maxPage = Math.ceil(total / limit); // Nombre total de pages const currentPage = Math.ceil(skip / limit) + 1; // Numéro de la page courante const href = `${req.protocol}://${req.get("host")}${req.originalUrl}`; // Lien vers la page actuelle return { paging: { first: QueryBuilder._createLink(href, currentPage, maxPage, "first"), prev: QueryBuilder._createLink(href, currentPage, maxPage, "prev"), next: QueryBuilder._createLink(href, currentPage, maxPage, "next"), last: QueryBuilder._createLink(href, currentPage, maxPage, "last") }, total, maxPage }; } /** * Fonction permettant de remplacer les attributs du genre $lte pat Op.lte * @param {Object} obj * @return {Object} */ _replaceKeys(obj) { const { Op } = this.models.Sequelize; let newObject = {}; if (Array.isArray(obj)) { newObject = []; for (let i = 0; i < obj.length; i += 1) { const value = typeof obj[i] === "object" ? this._replaceKeys(obj[i]) : obj[i]; newObject.push(value); } } else { Object.keys(obj).map(key => { const value = typeof obj[key] === "object" ? this._replaceKeys(obj[key]) : obj[key]; if (key.indexOf("$") === 0 && key.slice(-1) !== "$") { switch (key) { case "$or": newObject[Op.or] = value; break; case "$lte": newObject[Op.lte] = value; break; case "$gte": newObject[Op.gte] = value; break; case "$contains": newObject[Op.contains] = [value]; break; case "$in": newObject[Op.in] = value.split(","); break; default: newObject[key] = [value]; break; } } else { newObject[key] = value; } return true; }); } return newObject; } /** * Fonction permettant de savoir si un utilisateur a assez de droit pour effectuer une action * @param {Object} req * @return {Boolean} */ _haveRight(req) { let allowedRole = "all"; // eslint-disable-next-line default-case switch (req.method) { case "POST": allowedRole = this.params.crud.write; break; case "PATCH": case "PUT": allowedRole = this.params.crud.edit; break; case "GET": allowedRole = this.params.crud.read; break; case "DELETE": allowedRole = this.params.crud.delete; break; } return allowedRole === "all" || allowedRole.indexOf(req.user.role) !== -1; } /** * Fonction permettant d'ajouter des restructions sur un get * @param {Object} req * @return {Object} */ _restrictOn(req) { const where = {}; if (!this.params || !this.params.restrictOn) { return where; } const _overrideWhere = restrictions => { if (!restrictions) { return false; } for (let i = 0; i < restrictions.length; i += 1) { const restrict = restrictions[i]; const value = restrict.type === "raw" ? restrict.value : req[restrict.type][restrict.value]; if (restrict.roles.indexOf(req.user.role) !== -1) { where[restrict.field] = value; } } return true; }; _overrideWhere(this.params.restrictOn[req.getType]); switch (req.method) { case "PATCH": _overrideWhere(this.params.restrictOn.update); break; case "DELETE": _overrideWhere(this.params.restrictOn.delete); break; default: // Do nothing } return where; } /** * Fonction permettant de surcharger des valeurs * @param {Object} req * @param {String} method * @return {Object} */ _override(req, method) { let override = {}; const params = this.params.override ? this.params.override[method] : {}; if (!this.params.override) { return override; } if (!params) { return override; } // On surcharge certains paramètres passé en query if (params.filters) { Object.keys(params.filters).map(column => { if (req.query[column]) { const value = req.query[column]; const query = JSON.parse( JSON.stringify(params.filters[column]).replace( new RegExp("_TERM_", "g"), value ) ); override = Object.assign(override, this._replaceKeys(query)); } return true; }); } // On rajoute des paramètres à la requête if (params.params) { for (let i = 0; i < params.params.length; i += 1) { const currentParam = params.params[i]; override[currentParam.append] = req[currentParam.from][currentParam.value]; } } return override; } /** * Fonction permettant de charger la liste des relations à inclures lors d'un get * @param {Object} req * @param {Array} include * @return {Mixed} */ _setInclusions(req, include) { const includes = []; const listOfIncludes = include || this.params.includes; for (let i = 0; i < listOfIncludes.length; i += 1) { const current = listOfIncludes[i]; // const include = current.collection; if ( !current.requiredRole || current.requiredRole.indexOf(req.user.role) !== -1 ) { let currentInclude = null; if (!current.model) { currentInclude = current.collection; } else { currentInclude = { as: current.collection, model: current.model, required: current.required || false }; // Pour cette inclusion il y a des filtres à appliquer if (current.restrictOn || current.include) { currentInclude.where = {}; if (current.include) { currentInclude.include = this._setInclusions( req, current.include ); } // On parcours la liste des règles d'inclusion pour ce modèle if (current.restrictOn) { for (let j = 0; j < current.restrictOn.length; j += 1) { const currentRestriction = current.restrictOn[j]; // Cette restriction s'applique à tout le monde (pas de field roles) // ou alors elle s'applique juste sur une liste de groupes if ( !currentRestriction.roles || currentRestriction.roles.indexOf(req.user.role) !== -1 ) { if (currentRestriction.type === "raw") { currentInclude.where[currentRestriction.field] = currentRestriction.value; } } } } } } includes.push(currentInclude); } } // Mode reccursif if (include) { return includes; } this.includes = includes; return true; } /** * Méthode interne permettant de créer un item * @param {Object} req * @param {Object} value * @param {Function} callback */ _insertItem(req, value, callback) { const values = {}; // On converti les 'null' en null (formData par exemple) Object.keys(value).map(key => { if (value[key] === "null") { values[key] = null; } else { values[key] = value[key]; } return true; }); // Création de l'élément this.models[this.params.model] .create(values) .then(item => { let createdItem = item; if (this.params.format) { const formatRules = req.user ? this.params.format[req.user.role] : null; if (formatRules) { createdItem = this._formatItem(item, formatRules); } } callback(null, createdItem); }) .catch(err => { if (err.name === "SequelizeUniqueConstraintError") { callback(new ErrorBuilder(409, "Duplicate item")); return false; } callback(err); return false; }); } /** * Fonction permettant de formater un item en fonction du user * @param {Object} item * @param {Object} formatRule * @return {Object} */ _formatItem(item, formatRule) { const formated = {}; Object.keys(formatRule).map(key => { switch (typeof formatRule[key]) { case "string": formated[key] = item[key] || null; break; case "object": if (Array.isArray(item[key])) { formated[key] = []; for (let i = 0; i < item[key].length; i += 1) { formated[key].push( this._formatItem(item[key][i], formatRule[key]) ); } } else { formated[key] = this._formatItem(item[key], formatRule[key]); } break; default: // Do nothing } return true; }); return formated; } /** * Fonction permettant de formater une liste d'items en fonction du user * @param {Object} req * @param {Object} items * @return {Object} */ _formatItems(req, items) { if (!this.params.format) { return items; } const formatRules = this.params.format[req.user.role]; if (!formatRules) { return items; } const formated = []; for (let i = 0; i < items.length; i += 1) { formated.push(this._formatItem(items[i], formatRules)); } return formated; } /** * Méthode permettant de vérifier que les valeurs reçues dans le body sont valides * @param {Object} req * @param {Function} callback */ _checkCreateOneValues(req, callback) { // On regarde s'il faut surcharger les valeurs du body avec des valeurs dérivées (req.user, req.params...) if (this.params.override && this.params.override.create) { if (this.params.override.create.body) { for (let i = 0; i < this.params.override.create.body.length; i += 1) { const override = this.params.override.create.body[i]; req.body[override.append] = req[override.from][override.value]; } } } // On teste le body const { error, value } = this.params.validate.create.validate(req.body, { abortEarly: false }); if (error) { callback(new ErrorBuilder(406, error)); } else { callback(null, value); } } /** * Fonction permettant de vérifier et de surcharger des valeurs lors d'un get * @param {Object} req * @param {Function} callback * @return {Boolean} */ _createQuery(req, callback) { this._setInclusions(req); const query = {}; let where = {}; let order = []; // On test les droits if (!this._haveRight(req)) { callback(new ErrorBuilder(401, "You're not allowed")); return false; } // On teste la query (ou les params) const toValidate = req.getType === "list" ? req.query : req.params; const { error, value } = this.params.validate[ req.getType ].validate(toValidate, { abortEarly: false }); // On vire, pour le moment la liste des filtres un peu particuliers let listOfIgnoredFilters = [ "limit", "page", "sort", "order", this.params.itemId ]; if (this.params.removeKeys && this.params.removeKeys[req.getType]) { listOfIgnoredFilters = listOfIgnoredFilters.concat( this.params.removeKeys[req.getType] ); } if ( this.params.override && this.params.override[req.getType] && this.params.override[req.getType].filters ) { Object.keys(this.params.override[req.getType].filters).map(key => { listOfIgnoredFilters.push(key); return true; }); } // On rajoute les filtres autorisés Object.keys(value).map(key => { if (listOfIgnoredFilters.indexOf(key) === -1) { where[key] = value[key]; } return true; }); // Un paramètre n'est pas bon dans la query if (error) { callback(new ErrorBuilder(406, error)); return false; } if (req.getType === "list") { // Aucune pagination n'est passée, on set celle par défaut if (!value.page || !value.limit) { value.page = 1; value.limit = 20; } // Un tri est spécifié if (value.order && value.sort) { order = [[value.sort, value.order.toUpperCase()]]; } } else { // On get un item. on set son id where.id = value[this.params.itemId]; } // S'il y a des restrictions (genre un utilisateur n'a le droit de voir que tel ou tel items) const restrict = this._restrictOn(req); where = Object.assign(where, restrict); // On regarde s'il n'y a pas des valeurs à overrider const override = this._override(req, req.getType); where = Object.assign(where, override); // if ( value.page) { // query.offset = ( value.page - 1 ) * value.limit; // query.limit = value.limit; // } if (order) { query.order = order; } query.distinct = true; // On supprime les id en double (jointure de type hasmany) query.where = where; // On rajoute des filtres if (this.includes) { query.include = this.includes; // On set la liste des modèles à inclure } // Hack pour faire un recherche dans les nested de type hasMany query.subQuery = false; if (!value.page || !value.limit) { value.page = 1; value.limit = 50; } callback(null, query, { start: value.page * value.limit - value.limit, limit: value.limit }); return true; } } module.exports = QueryBuilder;