sequelize-middleware/libs/QueryBuilder.js
2020-02-13 22:36:14 +01:00

667 lines
17 KiB
JavaScript

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;
// eslint-disable-next-line default-case
switch (page) {
case "first":
newIndex = 1;
break;
case "last":
newIndex = maxPage;
break;
case "prev":
newIndex = currentPage - 1;
break;
case "next":
newIndex = currentPage + 1;
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;
case "$like":
newObject[Op.like] = value;
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";
if (!req.user) {
req.user = {};
}
if (!req.user.role) {
req.user.role = "guest";
}
// 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 overrideValues = {};
if (!this.params.override) {
return overrideValues;
}
const params = this.params.override ? this.params.override[method] : {};
if (!params) {
return overrideValues;
}
// 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
)
);
overrideValues = Object.assign(
overrideValues,
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];
overrideValues[currentParam.append] =
req[currentParam.from][currentParam.value];
}
}
return overrideValues;
}
/**
* Fonction permettant de charger la liste des relations à inclures lors d'un get
* @param {Object} req
* @param {Array} include
* @return {Mixed}
*/
setInclusions(req, include) {
if (!req.user) {
req.user = {};
}
if (!req.user.role) {
req.user.role = "guest";
}
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;
}
/**
* Fonction permettant de peupler une table de jointure (belongsToMany)
* @param {Object} item
* @param {String} key
* @param {Array} values
* @param {String} mode
* @param {Function} callback
*/
createBelonsTo(item, key, values, mode, callback) {
const collection = this.params.belongsToMany[key];
const action = `${mode}${collection}`;
item[action](values)
.then(() => {
callback();
})
.catch(callback);
}
deleteBelongsTo(item, key, values, callback) {
const collection = this.params.belongsToMany[key];
const action = `remove${collection}`;
item[action](values)
.then(() => {
callback();
})
.catch(callback);
}
/**
* Méthode interne permettant de créer un item
* @param {Object} req
* @param {Object} value
* @param {Function} callback
*/
insertItem(req, value, callback) {
const values = {};
const _done = 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);
};
// 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 => {
if (this.params.belongsToMany) {
const needCreate = [];
let created = 0;
const _next = err => {
if (err) {
// TODO: break and revert
} else {
created += 1;
if (created === needCreate.length) {
_done(item);
}
}
};
Object.keys(this.params.belongsToMany).map(key => {
if (values[key]) {
needCreate.push(key);
}
return true;
});
if (needCreate.length > 0) {
for (let i = 0; i < needCreate.length; i += 1) {
const currentKey = needCreate[i];
this.createBelonsTo(
item,
currentKey,
values[currentKey],
"set",
_next
);
}
} else {
_done(item);
}
} else {
_done(item);
}
})
.catch(err => {
switch (err.name) {
case "SequelizeUniqueConstraintError":
callback(new ErrorBuilder(409.1, "Duplicate item"));
break;
case "SequelizeForeignKeyConstraintError":
callback(new ErrorBuilder(406.2, "Bad foreign key"));
break;
default:
callback(new ErrorBuilder(500, err));
}
});
}
/**
* 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 => {
// TODO: revoir ce switch
switch (typeof formatRule[key]) {
case "string":
if (item) {
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.1, 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.1, "You're not allowed"));
return false;
}
// On teste la query (ou les params)
const toValidate = req.getType === "list" ? req.query : req.params;
if (!toValidate) {
callback(
new ErrorBuilder(
406.0,
`Missing ${req.getType === "list" ? "query" : "params"}`
)
);
return false;
}
const { error, value } = this.params.validate[
req.getType
].validate(toValidate, { abortEarly: false });
// Un paramètre n'est pas bon dans la query
if (error) {
callback(new ErrorBuilder(406.1, error));
return 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;
});
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 = 50;
}
// 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 (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;
callback(null, query, {
start: value.page * value.limit - value.limit,
limit: value.limit
});
return true;
}
}
module.exports = QueryBuilder;