Compare commits

...

23 commits

Author SHA1 Message Date
dbroqua
02734db677 Added LICENCE 2020-02-26 16:45:35 +01:00
dbroqua
3ed190bfde Updated docker images and tests suites 2020-02-18 13:38:40 +01:00
dbroqua
aed75b9576 Updated model index 2020-02-18 11:20:10 +01:00
dbroqua
77979d8fac Updated covegare from rules 2020-02-18 11:16:49 +01:00
dbroqua
a77e068c73 Added deleteOne tests 2020-02-17 15:16:00 +01:00
dbroqua
918f873fea Added some new tests 2020-02-13 22:36:14 +01:00
dbroqua
0039a8b4ec Renamed model 2020-02-13 13:10:16 +01:00
dbroqua
c2571cb40d Debug 2020-02-11 21:16:25 +01:00
dbroqua
4ea05a1de2 Debug 2020-02-11 21:01:16 +01:00
dbroqua
d2aac53881 Debug 2020-02-11 20:53:05 +01:00
dbroqua
a1c35a6c56 Debug 2020-02-11 20:52:06 +01:00
dbroqua
0d8f1971f7 Debug 2020-02-11 20:47:29 +01:00
dbroqua
0b94286b22 Debug 2020-02-11 20:45:51 +01:00
dbroqua
0bd87bd223 Debug 2020-02-11 20:37:34 +01:00
dbroqua
b32ab96ace Debug 2020-02-11 20:24:42 +01:00
dbroqua
bc6adfe920 Debug 2020-02-11 20:23:48 +01:00
dbroqua
5f50c568ed Debug 2020-02-11 20:01:17 +01:00
dbroqua
35f743ae14 Debug 2020-02-11 19:54:59 +01:00
dbroqua
2b043f952b Debug CI 2020-02-11 19:52:53 +01:00
dbroqua
0e8101c9e4 Debug CI 2020-02-11 19:49:23 +01:00
dbroqua
fae42cb001 Added gitlab pipeline config 2020-02-11 19:22:29 +01:00
dbroqua
9f0886541c Added some tests (createOne and getAll) 2020-02-11 15:38:28 +01:00
dbroqua
ef7ca0315b Added first iteration for the module 2020-02-10 16:13:42 +01:00
40 changed files with 9860 additions and 0 deletions

30
.babelrc Normal file
View file

@ -0,0 +1,30 @@
{
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
],
[
"@babel/plugin-proposal-class-properties"
]
],
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"env": {
"test": {
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
}
}
}

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
/node_modules/

30
.eslintrc.js Normal file
View file

@ -0,0 +1,30 @@
module.exports = {
parser: "babel-eslint",
extends: ["airbnb-base", "prettier", "plugin:jest/recommended"],
plugins: ["prettier", "jest"],
env: {
es6: true,
browser: false,
node: true,
"jest/globals": true
},
globals: {
__DEV__: true
},
rules: {
// enable additional rules
"no-plusplus": [2, { allowForLoopAfterthoughts: true }],
"no-underscore-dangle": "off",
indent: ["error", 2, { SwitchCase: 1 }],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
// Forbid the use of extraneous packages
// https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md
"import/no-extraneous-dependencies": ["error", { packageDir: "." }],
// ESLint plugin for prettier formatting
// https://github.com/prettier/eslint-plugin-prettier
"prettier/prettier": "error",
"func-names": ["error", "never"]
}
};

69
.gitignore vendored Normal file
View file

@ -0,0 +1,69 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
/reports/
junit.xml
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
/dist/
# Seeder info, proper to each environment
sequelize-data.json

46
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,46 @@
image: node:latest
variables:
NODE_ENV: "ci"
POSTGRES_DB: "postgres"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
stages:
- test
- deploy
cache:
paths:
- node_modules/
testing:
stage: test
services:
- postgres:latest
script:
- yarn install
- ./node_modules/.bin/sequelize db:migrate
- ./node_modules/.bin/sequelize db:seed:all
- yarn test --ci --collectCoverage=true --coverage
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
paths:
- junit.xml
- coverage/
- reports/
reports:
junit: junit.xml
pages:
stage: deploy
dependencies:
- testing
script:
- mv coverage/ public/
artifacts:
paths:
- public
expire_in: 30 days
only:
- develop

21
LICENCE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Damien Broqua and contributors (https://framagit.org/dbroqua/sequelize-middleware/-/graphs/master)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

215
README.md
View file

@ -1,2 +1,217 @@
# sequelize-middleware # sequelize-middleware
[![pipeline status](https://framagit.org/dbroqua/sequelize-middleware/badges/develop/pipeline.svg)](https://framagit.org/dbroqua/sequelize-middleware/commits/develop) [![coverage report](https://framagit.org/dbroqua/sequelize-middleware/badges/develop/coverage.svg)](https://framagit.org/dbroqua/sequelize-middleware/commits/develop)
Sequelize-middleware est un module NodeJS permettant d'automatiser la création d'une API REST utilisant Sequelize comme SGBD.
Sequelize-middleware s'appuie également sur Joi pour valider les données reçues ainsi que les appels API effectuées.
## Fonctionnalités
Ce module est découpé en 3 fonctionnalités principales :
* Le `Middleware`, partie centrale du projet
* `ErrorBuilder` qui permet de surcharger la méthode `new Error()`
* `ResponseFormater` qui se charge de formater les données retournée au client final
### Middleware
Méthode par défaut, permettant de générer tout le code propre à un endpoint.
```js
import Middleware from "sequelize-middleare";
import models from "../models";
import params from "../rules/Endpoint";
const middleware = new Middleware(params, models);
middleware.getAll(req, callback);
```
#### Initilisation
Le constructeur doit recevoir 2 paramètres :
* params
* models
`params` représente le fichier des règles pour cet endpoint.
`models` fait référence au fichier `models/index.js` généré par Sequelize.
Le fichier params doit ressembler à ceci :
```js
import Joi from "@hapi/joi";
const models = require("../models"); // Facultatif
const Rules = {
model: "Areas", // Nom de la collection Sequelize
crud: { // Liste des rôles (req.user.roles) autorisés en fonction de l'action
read: ["installer", "admin", "user"],
write: ["installer", "admin"],
edit: ["installer", "admin"],
delete: ["installer", "admin"]
},
includes: [ // Liste des inclusions à faire lors d'un getOne/getAll
{
collection: "Devices", // Nom de la collection à inclure
requiredRole: ["installer", "admin", "user"], // Rôles autorisés à voir cette inclusion
required: false, // Sauf cas particulier toujours mettre false
model: models.AreasDevices, // Nom du modèle Sequelize à inclure
include: [ // Liste des sous-inclusions
{
collection: "Device",
requiredRole: ["installer", "admin", "user"]
}
]
}
],
format: { // Formatage des données en fonction du rôle de l'utilisateur (dans le cas ou l'on veut surcharger le formatage du modèle)
user: {
idRetourne: "_idSql", // Clé retournée: Clé du modèle
id: "id",
nom: "name"
}
},
itemId: "areaId", // Id de l'item dans la req.params
validate: { // Définition des des règles de valeurs acceptées en fonction du verbe
// Création d'un nouvele élément
create: Joi.object({
name: Joi.string().required(),
type: Joi.string()
.valid("OFFICE", "COMMUNAL-AREAS")
.required()
}),
// Mise à jour d'un élément
update: Joi.object({
name: Joi.string(),
type: Joi.string().valid("OFFICE", "COMMUNAL-AREAS")
}),
// Sélection d'un élément (req.params)
item: Joi.object({
areaId: Joi.number().required()
}),
// Sélection d'une liste d'éléments (req.query)
list: Joi.object({
limit: Joi.number()
.integer()
.min(1)
.max(50),
page: Joi.number()
.integer()
.min(1),
type: Joi.string().valid("OFFICE", "COMMUNAL-AREAS"),
sort: Joi.string()
.valid("id", "name", "type", "createdAt", "updatedAt")
.only(),
order: Joi.string()
.valid("asc", "desc")
.only()
})
.with("limit", "page")
.with("page", "limit")
.with("sort", "order")
.with("order", "sort")
},
removeKeys: {
// Permet de supprimer automatiquement des valeurs de requêtes
item: ["partnerId"] // Sur la sélection d'un item on supprime req.params.partnerId
},
override: {
list: { // Permet de convertir des paramètres reçus via req.query pour en générer des filtres complexes
filters: {
'category': { // On converti req.query.category
$or: [
{
categoryId: '_TERM_',
},
{
categoriesId: {
$contains: '_TERM_',
},
},
],
},
'countryId': { // On converti req.query pour dire à Sequelize que ça correspond à $Details.countryId$
'$Details.countryId$': '_TERM_',
}
},
},
create: {
// Création d'un nouvel item
body: [
// On modifie req.body
{
append: "offerId", // On rajoute un attribut offerId
from: "params", // Que l'on prends dans req.params
value: "offerId" // Et dont la clé est offerId
}
]
}
},
restrictOn: { // Permet de rajouter des restrictions en fonction des rôles
update: [ // Lors d'une mise à jour au autorise l'utilisateur à modifier uniquement les éléments donc state est égal à NEW.
{
roles: ['user'],
type: 'raw', // raw permet de forcer une valeur, sinon user/params/body/query
field: 'state',
value: 'NEW',
},
],
delete: [],
list: [],
create: []
},
};
export default Rules;
```
### ErrorBuilder
```js
import { ErrorBuilder } from "sequelize-middleare";
```
Ce module permet de remplacer la méthode `new Error()` de JS en ajoutant la possibilité de générer un code d'erreur qui sera ensuite retourné via `res.status().json()`.
Le code reçu doit être un `float` dont la partie entière représente un code HTTP valide et la partie réelle représente un code d'erreur plus détaillé de l'erreur.
Exemples d'utilistion :
```js
import { ErrorBuilder } from "sequelize-middleare";
new ErrorBuilder(406.0, "Erreur générique de type 406");
new ErrorBuilder(406.1, "Le champs mot de passe est absent");
new ErrorBuilder(406.2, "Le champs mot de passe doit contenir 8 caractères minimum");
...
```
### ResponseFormater
```js
import { ResponseFormater } from "sequelize-middleare";
```
Ce module permet de formater à la fois le json ainsi que le code http retourné au client une fois que le middleware à fini son traitement.
Exemples d'utilisation :
```js
import { ResponseFormater } from "sequelize-middleare";
router
.route("/")
.get(passport.authenticate(["jwt"]), function(req, res, next) {
middleware.getAll(req, (err, response) => {
ResponseFormater(req, res, next, err, response);
});
})
.post(passport.authenticate(["jwt"]), function(req, res, next) {
middleware.createOne(req, (err, response) => {
ResponseFormater(req, res, next, err, response);
});
});
```

19
config/config.json Normal file
View file

@ -0,0 +1,19 @@
{
"test": {
"username": "postgres",
"password": "postgres",
"database": "test",
"host": "127.0.0.1",
"port": "5666",
"dialect": "postgres",
"seederStorage": "sequelize"
},
"ci": {
"username": "postgres",
"password": "postgres",
"database": "postgres",
"host": "postgres",
"dialect": "postgres",
"seederStorage": "sequelize"
}
}

17
docker-compose.yml Normal file
View file

@ -0,0 +1,17 @@
version: "2"
services:
sequelize-middleware-db:
image: postgres:latest
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
restart: always
ports:
- 127.0.0.1:5666:5432
sequelize-middleware-adminer:
image: adminer
restart: always
ports:
- 127.0.0.1:8666:8080

7
index.js Normal file
View file

@ -0,0 +1,7 @@
import _ErrorBuilder from "./libs/ErrorBuilder";
import Middleware from "./libs/Middleware";
import _ResponseFormater from "./libs/ResponseFormater";
export default Middleware;
export const ErrorBuilder = _ErrorBuilder;
export const ResponseFormater = _ResponseFormater;

21
libs/ErrorBuilder.js Normal file
View file

@ -0,0 +1,21 @@
/**
* Classe permettant la gestion des erreurs personilisées
*/
class ErrorBuilder extends Error {
/**
* @param {Number} errorCode
* @param {Mixed} ...params
*/
constructor(errorCode, ...params) {
super(...params);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ErrorBuilder);
}
this.errorCode = errorCode.toFixed(1);
this.date = new Date();
}
}
export default ErrorBuilder;

290
libs/Middleware.js Normal file
View file

@ -0,0 +1,290 @@
import QueryBuilder from "./QueryBuilder";
import ErrorBuilder from "./ErrorBuilder";
class Middleware extends QueryBuilder {
/**
* Initialisation de la classe
* @param {Object} params
* @param {Object} models
*/
constructor(params, models) {
super(params, models);
this.params = params;
this.models = models;
this.includes = [];
}
/**
* Fonction permettant de créer un nouvel élément
* @param {Object} req
* @param {Function} callback
* @return {Boolean}
*/
createOne(req, callback) {
// On test les droits
if (!this.haveRight(req)) {
callback(new ErrorBuilder(401.1, "You're not allowed"));
return false;
}
// On teste le body
this.checkCreateOneValues(req, (err, value) => {
// Il manque un paramètre dans le body
if (err) {
callback(err, value);
return false;
}
this.insertItem(req, value, callback);
return true;
});
return true;
}
/**
* Fonction permettant de retourner une liste d'items
* @param {Object} req
* @param {Function} callback
*/
getAll(req, callback) {
// On spécifie le type de requête (list ou item)
req.getType = "list";
this.createQuery(req, (err, query, limit) => {
if (err) {
callback(err, query);
return false;
}
this.models[this.params.model]
.findAndCountAll(query)
.then(res => {
if (!res || res.count === 0) {
callback();
return false;
}
const rows = [];
for (let i = 0; i < res.rows.length; i += 1) {
const current = res.rows[i];
let found = false;
for (let j = 0; j < rows.length; j += 1) {
if (rows[j].id === current.id) {
found = true;
rows[j].assign({}, rows[j], current);
break;
}
}
if (!found || rows.length === 0) {
rows.push(current);
}
}
const total = rows.length;
const values = rows.slice(limit.start, limit.start + limit.limit);
callback(null, {
data: this.formatItems(req, values),
...QueryBuilder.createPagination(
req,
total,
limit.limit,
limit.start
)
});
return true;
})
.catch(callback);
return true;
});
}
/**
* Fonction permettant de retourner un item par son Id
* @param {Object} req
* @param {Function} callback
*/
getOne(req, callback) {
// On spécifie le type de requête (list ou item)
req.getType = "item";
this.createQuery(req, (err, query) => {
if (err) {
callback(err, query);
return false;
}
this.models[this.params.model]
.findOne(query)
.then(item => {
if (!item) {
callback(new ErrorBuilder(404, "Item not found"));
return false;
}
let formatedItem = item;
if (req.method === "GET") {
if (this.params.format) {
const formatRules = this.params.format[req.user.role];
if (formatRules) {
formatedItem = this.formatItem(item, formatRules);
}
}
}
callback(null, formatedItem);
return true;
})
.catch(callback);
return true;
});
}
/**
* Fonction permettant de mettre à jour un item
* @param {Object} req
* @param {Function} callback
* @return {Boolean}
*/
patchOne(req, callback) {
// On test les droits
if (!this.haveRight(req)) {
callback(new ErrorBuilder(401.1, "You're not allowed"));
return false;
}
// On teste les params
const { error, value } = this.params.validate.update.validate(req.body, {
abortEarly: false
});
// Un paramètre n'est pas bon dans les params
if (error) {
callback(new ErrorBuilder(406.1, error));
return false;
}
this.getOne(req, (err, item) => {
if (err) {
callback(err, item);
return false;
}
item
.update(value)
.then(updated => {
if (this.params.belongsToMany) {
const needCreate = [];
let created = 0;
const _next = errNext => {
if (errNext) {
// TODO: break and revert
} else {
created += 1;
if (created === needCreate.length) {
callback(null, updated);
}
}
};
Object.keys(this.params.belongsToMany).map(key => {
if (value[key]) {
needCreate.push(key);
}
return true;
});
if (needCreate.length > 0) {
for (let i = 0; i < needCreate.length; i += 1) {
const currentKey = needCreate[i];
const currentValue = value[currentKey];
this.createBelonsTo(
updated,
currentKey,
currentValue,
"add",
_next
);
}
} else {
callback(null, updated);
}
} else {
callback(null, updated);
}
})
.catch(callback);
return true;
});
return true;
}
/**
* Fonction permettant de supprimer un item
* @param {Object} req
* @param {Function} callback
* @return {Boolean}
*/
deleteOne(req, callback) {
// On test les droits
if (!this.haveRight(req)) {
callback(new ErrorBuilder(401.1, "You're not allowed"));
return false;
}
this.getOne(req, (err, item) => {
if (err) {
callback(err, item);
} else {
let deletedBelongs = 0;
const needToDeleteBelongs = [];
const _deleteItem = () => {
item
.destroy()
.then(() => {
callback(null, {});
})
.catch(callback);
};
const _next = () => {
deletedBelongs += 1;
if (deletedBelongs === needToDeleteBelongs.length) {
_deleteItem();
}
};
if (this.params.belongsToMany) {
Object.keys(this.params.belongsToMany).map(key => {
const collection = this.params.belongsToMany[key];
if (item[collection].length) {
needToDeleteBelongs.push(key);
}
return true;
});
if (needToDeleteBelongs.length > 0) {
for (let i = 0; i < needToDeleteBelongs.length; i += 1) {
const currentKey = needToDeleteBelongs[i];
const collection = this.params.belongsToMany[currentKey];
const valuesToDelete = [];
for (let j = 0; j < item[collection].length; j += 1) {
valuesToDelete.push(item[collection][j].id);
}
this.deleteBelongsTo(item, currentKey, valuesToDelete, _next);
}
} else {
_deleteItem();
}
} else {
_deleteItem();
}
}
});
return true;
}
}
export default Middleware;

666
libs/QueryBuilder.js Normal file
View file

@ -0,0 +1,666 @@
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;

53
libs/ResponseFormater.js Normal file
View file

@ -0,0 +1,53 @@
/**
* Fonction permettant de formater une réponse API
* @param {Object} req
* @param {Object} res
* @param {Functiont} next
* @param {Object} err
* @param {Object} response {code: Integer, res: Array/Object}
*/
const ResponseFormater = (req, res, next, err, response) => {
if (err) {
const code = err.errorCode || 500;
res
.status(Math.trunc(code))
.json({
code,
message: err.message
})
.end();
} else {
switch (req.method) {
case "GET":
res
.status(response ? 200 : 204)
.json(response)
.end();
break;
case "PATCH":
res
.status(200)
.json(response)
.end();
break;
case "DELETE":
res
.status(200)
.json(response)
.end();
break;
case "POST":
res
.status(201)
.json(response)
.end();
break;
default:
next(new Error("Not implemented"));
break;
}
}
};
export default ResponseFormater;

View file

@ -0,0 +1,28 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Brands", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: queryInterface => {
return queryInterface.dropTable("Brands");
}
};

View file

@ -0,0 +1,40 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Cars", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
year: {
type: Sequelize.INTEGER
},
active: {
type: Sequelize.BOOLEAN
},
brandId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: "Brands",
key: "id"
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: queryInterface => {
return queryInterface.dropTable("Cars");
}
};

View file

@ -0,0 +1,26 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Colors", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: queryInterface => {
return queryInterface.dropTable("Colors");
}
};

View file

@ -0,0 +1,39 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("CarsColors", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
CarId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: "Cars",
key: "id"
}
},
ColorId: {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: "Colors",
key: "id"
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: queryInterface => {
return queryInterface.dropTable("CarsColors");
}
};

View file

@ -0,0 +1,15 @@
module.exports = {
up: queryInterface => {
return queryInterface.addConstraint("CarsColors", ["CarId", "ColorId"], {
type: "unique",
name: "CarsColors_unique_carId_colorId"
});
},
down: queryInterface => {
return queryInterface.removeConstraint(
"CarsColors",
"CarsColors_unique_carId_colorId"
);
}
};

19
models/brands.js Normal file
View file

@ -0,0 +1,19 @@
module.exports = (sequelize, DataTypes) => {
const Brands = sequelize.define(
"Brands",
{
name: DataTypes.STRING
},
{}
);
Brands.associate = function(models) {
Brands.hasMany(models.Cars, {
as: "Cars",
foreignKey: "brandId",
sourceKey: "id"
});
};
return Brands;
};

28
models/cars.js Normal file
View file

@ -0,0 +1,28 @@
module.exports = (sequelize, DataTypes) => {
const Cars = sequelize.define(
"Cars",
{
name: DataTypes.STRING,
year: DataTypes.INTEGER,
active: DataTypes.BOOLEAN,
brandId: {
type: DataTypes.INTEGER,
references: {
model: "Brands",
key: "id"
}
}
},
{}
);
Cars.associate = function(models) {
Cars.belongsTo(models.Brands, {
as: "Brand",
foreignKey: "brandId"
});
Cars.belongsToMany(models.Colors, { through: models.CarsColors });
};
return Cars;
};

24
models/carscolors.js Normal file
View file

@ -0,0 +1,24 @@
module.exports = (sequelize, DataTypes) => {
const CarsColors = sequelize.define(
"CarsColors",
{
CarId: {
type: DataTypes.INTEGER,
references: {
model: "Cars",
key: "id"
}
},
ColorId: {
type: DataTypes.INTEGER,
references: {
model: "Colors",
key: "id"
}
}
},
{}
);
return CarsColors;
};

11
models/colors.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = (sequelize, DataTypes) => {
const Colors = sequelize.define(
"Colors",
{
name: DataTypes.STRING
},
{}
);
return Colors;
};

38
models/index.js Normal file
View file

@ -0,0 +1,38 @@
const fs = require("fs");
const path = require("path");
const Sequelize = require("sequelize");
const basename = path.basename(__filename);
const env = process.env.NODE_ENV;
// eslint-disable-next-line import/no-dynamic-require
const config = require(`${__dirname}/../config/config.json`)[env];
const db = {};
const sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
fs.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
})
.forEach(file => {
const model = sequelize.import(path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

116
package.json Normal file
View file

@ -0,0 +1,116 @@
{
"name": "sequelize-middleware",
"version": "0.1.0",
"description": "Middleware to automate tasks with Sequelize",
"main": "index.js",
"scripts": {
"undo:seed": "./node_modules/.bin/sequelize db:seed:undo:all",
"undo:migrate": "./node_modules/.bin/sequelize db:migrate:undo:all",
"undo:all": "yarn undo:seed && yarn undo:migrate",
"do:migrate": "./node_modules/.bin/sequelize db:migrate",
"do:seed": "./node_modules/.bin/sequelize db:seed:all",
"do:all": "yarn do:migrate && yarn do:seed",
"pretest": "yarn undo:all && yarn do:all",
"lint": "./node_modules/.bin/eslint . --fix",
"test": "jest --forceExit --detectOpenHandles --maxWorkers=10 --ci --coverage"
},
"repository": {
"type": "git",
"url": "git@framagit.org:dbroqua/sequelize-middleware.git"
},
"author": "Damien Broqua <contact@darkou.fr>",
"license": "ISC",
"engines": {
"node": "13.x",
"yarn": "1.x"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"./node_modules/.bin/eslint --fix",
"git add"
]
},
"jest": {
"moduleFileExtensions": [
"js"
],
"verbose": true,
"roots": [
"<rootDir>",
"<rootDir>/test/"
],
"transform": {
"^.+\\.js$": "babel-jest"
},
"transformIgnorePatterns": [
"<rootDir>/node_modules/(?!(jest-test))"
],
"testEnvironment": "node",
"setupFilesAfterEnv": [
"<rootDir>/test/utils/setup.js"
],
"collectCoverage": true,
"collectCoverageFrom": [
"**/*.js",
"*.js",
"!**/coverage/**",
"!**/migrations/**",
"!**/seeders/**",
"!**/node_modules/**",
"!**/test/**"
],
"coverageReporters": [
"text",
"html",
"cobertura"
],
"reporters": [
"default",
"jest-junit",
[
"./node_modules/jest-html-reporter",
{
"pageTitle": "Test Report",
"outputPath": "reports/html/test-results.html",
"includeFailureMsg": true,
"includeConsoleLog": true
}
]
]
},
"nodemonConfig": {
"ext": "*.js,*.json,*yaml"
},
"dependencies": {},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@hapi/joi": "^16.1.8",
"babel-eslint": "^10.0.3",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jest": "^23.7.0",
"eslint-plugin-prettier": "^3.1.2",
"husky": "^4.2.1",
"jest": "^25.1.0",
"jest-html-reporter": "^2.8.0",
"jest-junit": "^10.0.0",
"lint-staged": "^10.0.7",
"pg": "^7.18.1",
"pg-hstore": "^2.3.3",
"prettier": "^1.19.1",
"sequelize": "^5.21.3",
"sequelize-cli": "^5.5.1",
"sinon": "^8.1.1",
"supertest": "^4.0.2",
"uuid": "^3.4.0"
}
}

69
rules/Brands.js Normal file
View file

@ -0,0 +1,69 @@
import Joi from "@hapi/joi";
const Rules = {
model: "Brands",
crud: {
read: ["admin", "user"],
write: ["admin"],
edit: ["admin"],
delete: ["admin"]
},
includes: [
{
collection: "Cars",
requiredRole: ["admin", "user"]
}
],
format: {
user: {
id: "id",
name: "name"
},
admin: {
id: "id",
name: "name",
created: "createdAt",
updated: "updatedAt",
Cars: {
id: "id",
name: "name",
year: "year",
created: "createdAt",
updated: "updatedAt"
}
}
},
itemId: "brandId",
validate: {
create: Joi.object({
name: Joi.string().required()
}),
update: Joi.object({
name: Joi.string()
}),
item: Joi.object({
brandId: Joi.number().required()
}),
list: Joi.object({
limit: Joi.number()
.integer()
.min(1)
.max(50),
page: Joi.number()
.integer()
.min(1),
sort: Joi.string()
.valid("id", "name", "createdAt", "updatedAt")
.only(),
order: Joi.string()
.valid("asc", "desc")
.only()
})
.with("limit", "page")
.with("page", "limit")
.with("sort", "order")
.with("order", "sort")
}
};
export default Rules;

130
rules/Cars.js Normal file
View file

@ -0,0 +1,130 @@
import Joi from "@hapi/joi";
const Rules = {
model: "Cars",
crud: {
read: ["admin", "user"],
write: ["admin"],
edit: ["admin"],
delete: ["admin"]
},
includes: [
{
collection: "Brand",
requiredRole: ["admin", "user"]
},
{
collection: "Colors"
}
],
format: {
user: {
id: "id",
model: "model",
Brand: {
name: "name"
}
}
},
itemId: "carId",
validate: {
create: Joi.object({
name: Joi.string().required(),
colorsId: Joi.array().items(Joi.number().integer()),
active: Joi.boolean(),
year: Joi.number()
.integer()
.required(),
brandId: Joi.number().integer()
}),
update: Joi.object({
name: Joi.string(),
year: Joi.number().integer(),
colorsId: Joi.array().items(Joi.number().integer())
}),
item: Joi.object({
carId: Joi.number().required()
}),
list: Joi.object({
weirdfilter: Joi.string(),
"name.lk": Joi.string(),
name: Joi.string(),
year: Joi.number().integer(),
"year.lte": Joi.number().integer(),
"year.gte": Joi.number().integer(),
limit: Joi.number()
.integer()
.min(1)
.max(50),
page: Joi.number()
.integer()
.min(1),
sort: Joi.string()
.valid("id", "name", "createdAt", "updatedAt")
.only(),
order: Joi.string()
.valid("asc", "desc")
.only()
})
.with("year.lte", "year.gte")
.with("year.gte", "year.lte")
.with("limit", "page")
.with("page", "limit")
.with("sort", "order")
.with("order", "sort")
},
restrictOn: {
list: [
{
roles: ["user"],
type: "raw",
field: "active",
value: "true"
}
],
item: [
{
roles: ["user"],
type: "raw",
field: "active",
value: "true"
}
]
},
override: {
list: {
filters: {
weirdfilter: {
$or: [
{
active: true
},
{
year: 2003
}
]
},
"name.lk": {
name: {
$like: "%_TERM_%"
}
},
"year.lte": {
year: {
$lte: "_TERM_"
}
},
"year.gte": {
year: {
$gte: "_TERM_"
}
}
}
}
},
belongsToMany: {
colorsId: "Colors"
}
};
export default Rules;

View file

@ -0,0 +1,47 @@
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert(
"Colors",
[
{
name: "Red",
createdAt: new Date(),
updatedAt: new Date()
},
{
name: "Miami blue",
createdAt: new Date(),
updatedAt: new Date()
},
{
name: "Sirius yellow",
createdAt: new Date(),
updatedAt: new Date()
},
{
name: "Sunflower yellow",
createdAt: new Date(),
updatedAt: new Date()
},
{
name: "Royal blue",
createdAt: new Date(),
updatedAt: new Date()
},
{
name: "White",
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
);
},
down: queryInterface => {
return Promise.all([
queryInterface.bulkDelete("CarsColors", null, {}),
queryInterface.bulkDelete("Colors", null, {})
]);
}
};

5
test.sh Executable file
View file

@ -0,0 +1,5 @@
#! /bin/bash
export NODE_ENV="test";
yarn test

286
test/belongsToMany.test.js Normal file
View file

@ -0,0 +1,286 @@
/* eslint-disable jest/no-test-callback */
import uuid from "uuid/v4";
import { truncate } from "./utils/common";
import models from "../models";
import Cars from "../rules/Cars";
import Middelware from "../index";
const { Op } = models.Sequelize;
let colors = [];
const colorsId = [];
let car = {};
describe("createOne", () => {
beforeAll(done => {
models.Colors.findAll({
where: {
[Op.or]: [
{
name: "White"
},
{
name: "Miami blue"
},
{
name: "Sunflower yellow"
}
]
}
})
.then(items => {
colors = items;
colorsId.push(items[0].id);
colorsId.push(items[1].id);
done();
})
.catch(done);
});
afterAll(done => {
truncate(["Cars"], done);
});
test("It should return new item after creation", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "POST",
user: {
role: "admin"
},
body: {
name: uuid(),
year: 2004,
active: true,
colorsId
}
};
middleware.createOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.name).toBe(req.body.name);
expect(res.year).toBe(2004);
expect(res.active).toBe(true);
car = res;
done();
});
});
test("It should return item with relations", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "POST",
user: {
role: "admin"
},
params: {
carId: car.id
}
};
middleware.getOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.name).toBe(car.name);
expect(res.Colors.length).toBe(2);
expect(res.year).toBe(2004);
expect(res.Colors[0].id).toBe(colorsId[0]);
expect(res.Colors[1].id).toBe(colorsId[1]);
done();
});
});
test("It should return all items ", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "POST",
user: {
role: "admin"
},
params: {},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res.data[0].name).toBe(car.name);
expect(res.data[0].Colors.length).toBe(2);
expect(res.data[0].Colors[0].id).toBe(colorsId[0]);
expect(res.data[0].Colors[1].id).toBe(colorsId[1]);
done();
});
});
test("It should return ok when update item", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "PATCH",
user: {
role: "admin"
},
params: {
carId: car.id
},
query: {},
body: {
year: 1998,
colorsId: [colors[2].id]
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.name).toBe(car.name);
expect(res.year).toBe(1998);
done();
});
});
test("It should return updated item", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "PATCH",
user: {
role: "admin"
},
params: {
carId: car.id
},
query: {},
body: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.name).toBe(car.name);
expect(res.year).toBe(1998);
expect(res.Colors.length).toBe(3);
done();
});
});
test("It should return empty object after delete", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "DELETE",
user: {
role: "admin"
},
params: {
carId: car.id
},
query: {},
body: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.deleteOne(req, (err, res) => {
expect(err).toBeNull();
expect(res).toEqual({});
models.CarsColors.findAll({
where: {
CarId: car.id
}
})
.then(carsColors => {
expect(carsColors.length).toBe(0);
models.Cars.findAll({
where: {
id: car.id
}
}).then(items => {
expect(items.length).toBe(0);
done();
});
})
.catch(done);
});
});
test("It should return empty item when delete item with empty belongsTo collection", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "POST",
user: {
role: "admin"
},
body: {
name: uuid(),
year: 2004,
active: true
}
};
middleware.createOne(req, (err, newCar) => {
expect(err).toBeNull();
expect(newCar.name).toBe(req.body.name);
expect(newCar.year).toBe(2004);
expect(newCar.active).toBe(true);
const reqItem = {
method: "PATCH",
user: {
role: "admin"
},
params: {
carId: newCar.id
},
query: {},
body: {
year: 1998
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(reqItem, errPatch => {
expect(errPatch).toBeNull();
reqItem.method = "DELETE";
middleware.deleteOne(reqItem, errDelete => {
expect(errDelete).toBeNull();
done();
});
});
});
});
});

177
test/createOne.test.js Normal file
View file

@ -0,0 +1,177 @@
/* eslint-disable jest/no-test-callback */
import uuid from "uuid/v4";
import { truncate } from "./utils/common";
import models from "../models";
import Brands from "../rules/Brands";
import Cars from "../rules/Cars";
import Middelware from "../index";
let createdBrand = null;
describe("createOne", () => {
afterAll(done => {
truncate(["Brands", "Cars"], done);
});
test("It should return a 401 when guest tries to create new item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "POST",
user: null,
body: {
name: uuid()
}
};
middleware.createOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
});
});
test("It should return a 401 when unauthorized role tries to create new item", async done => {
const middleware = new Middelware(Brands, models);
middleware.createOne(
{
method: "POST",
user: {
role: "user"
},
body: {
name: uuid()
}
},
(err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
}
);
});
test("It should return a 406 when authorized role tries to create new item with missing required values", async done => {
const middleware = new Middelware(Brands, models);
middleware.createOne(
{
method: "POST",
user: {
role: "admin"
},
body: {}
},
(err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(406.1);
done();
}
);
});
test("It should return new item when authorized role create new item", async done => {
const middleware = new Middelware(Brands, models);
const name = uuid();
middleware.createOne(
{
method: "POST",
user: {
role: "admin"
},
body: {
name
}
},
(err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("id");
expect(res).toHaveProperty("created");
expect(res).toHaveProperty("updated");
expect(res).not.toHaveProperty("createdAt");
expect(res).not.toHaveProperty("updatedAt");
expect(res.name).toBe(name);
createdBrand = res;
done();
}
);
});
test("It should return errorCode 409.1 when authorized role tries to create new item with same unique field", async done => {
const middleware = new Middelware(Brands, models);
middleware.createOne(
{
method: "POST",
user: {
role: "admin"
},
body: {
name: createdBrand.name
}
},
(err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(409.1);
done();
}
);
});
test("It should return errorCode 406.2 when authorized role tries to create new item with bad foreign id", async done => {
const middleware = new Middelware(Cars, models);
const name = uuid();
middleware.createOne(
{
method: "POST",
user: {
role: "admin"
},
body: {
name,
year: 2004,
brandId: createdBrand.id + 2
}
},
(err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(406.2);
done();
}
);
});
test("It should return errorCode 500 when authorized role tries to create new item with too long value", async done => {
const middleware = new Middelware(Cars, models);
middleware.createOne(
{
method: "POST",
user: {
role: "admin"
},
body: {
name:
"ThohWaigohhieHogahthoxohpheeDah0geetai0cieNgu1The2foQueeloochoH9eulizieshuf1nivohkied8jei5oph2Lajem6ohviijai6booTh8ienaic9eipheixa4ki1iek2pheihe7een7nei7epahngaerieghoe3ahbeil3yied0ievee1moh8jeeN5quoh6uiph6HaeZ0Eiyohshafohniewaer7gaegiefi5eiquiequoow5ohtheiw6ZeihieMoM8Ejoh7leiNeavi7uapheiwoophitoi3queiBeVeip5too8cah9Ohpaetaogahw1tei0eibuyaef3aht8aighuma6ahK4huP4cew6ohd0aiSh2umeeng7Hizahtoo6xoocePhu4ahtheex3jaijooph9iexaiqu3Nu0Ebeich6iTe",
year: 2004
}
},
(err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(500.0);
done();
}
);
});
});

133
test/deleteOne.test.js Normal file
View file

@ -0,0 +1,133 @@
/* eslint-disable jest/no-test-callback */
import { createBrand, truncate } from "./utils/common";
import models from "../models";
import Brands from "../rules/Brands";
import Middelware from "../index";
let brand = {};
describe("deleteOne", () => {
beforeAll(done => {
createBrand((err, item) => {
if (err) {
done(err);
} else {
brand = item;
done();
}
});
});
afterAll(done => {
truncate(["Brands"], done);
});
test("It should return 401.1 if guest tries to delete item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "DELETE",
user: null,
params: {
brandId: brand.id
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.deleteOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
});
});
test("It should return 401.1 if bad role tries to delete item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "DELETE",
user: {
role: "user"
},
params: {
brandId: brand.id
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.deleteOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
});
});
test("It should return empty item if allowed user delete item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "DELETE",
user: {
role: "admin"
},
params: {
brandId: brand.id
},
query: {},
body: {
name: "TEST"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.deleteOne(req, (err, res) => {
expect(err).toBeNull();
expect(res).toEqual({});
done();
});
});
test("It should return 404.0 if allowed user tries to delete not found item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "DELETE",
user: {
role: "admin"
},
params: {
brandId: brand.id
},
query: {},
body: {
name: "TEST"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.deleteOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(404.0);
done();
});
});
});

436
test/getAll.complex.test.js Normal file
View file

@ -0,0 +1,436 @@
/* eslint-disable jest/no-test-callback */
import { createBrands, createBrand, createCar, truncate } from "./utils/common";
import models from "../models";
import Cars from "../rules/Cars";
import Brands from "../rules/Brands";
import Middelware from "../index";
const createdCars = [];
describe("getAll (with inclusions)", () => {
beforeAll(done => {
createBrands(6, err => {
if (err) {
done(err);
} else {
createBrand((errBrand, createdBrand) => {
if (errBrand) {
done(errBrand);
} else {
createCar(createdBrand.id, true, 2004, (errCar, car) => {
if (errCar) {
done(errCar);
}
createdCars.push(car);
createCar(createdBrand.id, false, 2004, (errCar2, car2) => {
if (errCar2) {
done(errCar2);
}
createdCars.push(car2);
createCar(createdBrand.id, true, 2003, (errCar3, car3) => {
if (errCar3) {
done(errCar3);
}
createdCars.push(car3);
createCar(createdBrand.id, true, 1998, (errCar4, car4) => {
if (errCar4) {
done(errCar4);
}
createdCars.push(car4);
createCar(createdBrand.id, false, 1998, (errCar5, car5) => {
if (errCar5) {
done(errCar5);
}
createdCars.push(car5);
createBrands(2, errLastBrand => {
if (errLastBrand) {
done(errLastBrand);
} else {
models.Cars.create({
name: "la renault fuego",
year: 1980,
active: false
})
.then(() => {
done();
})
.catch(done);
}
});
});
});
});
});
});
}
});
}
});
});
afterAll(done => {
truncate(["Brands", "Cars"], done);
});
test("It should return all items when allowed role call getAll without filters (except inactive)", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "user"
},
params: {},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(3);
expect(res.total).toBe(3);
expect(res.maxPage).toBe(1);
expect(res.paging.first).toBeUndefined();
expect(res.paging.prev).toBeUndefined();
expect(res.paging.next).toBeUndefined();
expect(res.paging.last).toBeUndefined();
done();
});
});
test("It should return all items when allowed role call getAll without filters", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(6);
expect(res.total).toBe(6);
expect(res.maxPage).toBe(1);
expect(res.paging.first).toBeUndefined();
expect(res.paging.prev).toBeUndefined();
expect(res.paging.next).toBeUndefined();
expect(res.paging.last).toBeUndefined();
done();
});
});
test("It should return 2 items when set complex filters", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
"year.lte": 2005,
"year.gte": 2004
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(2);
expect(res.total).toBe(2);
expect(res.maxPage).toBe(1);
done();
});
});
test("It should return 1 item when set complex filters", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "user"
},
params: {},
query: {
"year.lte": 2005,
"year.gte": 2004
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(1);
expect(res.total).toBe(1);
expect(res.maxPage).toBe(1);
done();
});
});
test("It should return 1 item when get all brands", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(9);
expect(res.total).toBe(9);
expect(res.maxPage).toBe(1);
expect(res.data[6].Cars.length).toBe(5);
done();
});
});
test("It should return 1 item when get all brands (without cars)", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(9);
expect(res.total).toBe(9);
expect(res.maxPage).toBe(1);
expect(res.data[6].Cars).not.toHaveProperty("Cars");
done();
});
});
test("It should return 3 items when admin send weirdfilter", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc",
weirdfilter: "letsgo"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(3);
expect(res.total).toBe(3);
expect(res.maxPage).toBe(1);
done();
});
});
test("It should return errorCode 406.1 when trying to send unallowed filter", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc",
id: 65
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(parseFloat(err.errorCode)).toBe(406.1);
expect(res).toBeUndefined();
done();
});
});
test("It should return one item when trying to get item with name contains 'renault'", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc",
"name.lk": "renault"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res.data.length).toBe(1);
expect(res.total).toBe(1);
expect(res.maxPage).toBe(1);
done();
});
});
test("It should return one item when trying to get item filtered by name", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc",
name: "la renault fuego"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res.data.length).toBe(1);
expect(res.total).toBe(1);
expect(res.maxPage).toBe(1);
done();
});
});
test("It should return empty data when trying to get item filtered by name", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {
sort: "id",
order: "asc",
name: "Clio"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeUndefined();
expect(res).toBeUndefined();
done();
});
});
});

142
test/getAll.test.js Normal file
View file

@ -0,0 +1,142 @@
/* eslint-disable jest/no-test-callback */
import { createBrands, truncate } from "./utils/common";
import models from "../models";
import Brands from "../rules/Brands";
import Cars from "../rules/Cars";
import Middelware from "../index";
describe("getAll", () => {
beforeAll(done => {
createBrands(40, done);
});
afterAll(done => {
truncate(["Brands", "Cars"], done);
});
test("It should return a 401 when guest tries to get all item", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: null,
params: {},
query: {}
};
middleware.getAll(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
});
});
test("It should return a empty result allowed role get items from empty collection", async done => {
const middleware = new Middelware(Cars, models);
const req = {
method: "GET",
user: {
role: "user"
},
params: {},
query: {}
};
middleware.getAll(req, (err, res) => {
expect(err).toBeUndefined();
expect(res).toBeUndefined();
done();
});
});
test("It should return all items when allowed role call getAll without filters", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "user"
},
params: {},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(40);
expect(res.total).toBe(40);
expect(res.maxPage).toBe(1);
expect(res.paging.first).toBeUndefined();
expect(res.paging.prev).toBeUndefined();
expect(res.paging.next).toBeUndefined();
expect(res.paging.last).toBeUndefined();
done();
});
});
test("It should return 5 items when allowed role call getAll with limit filter", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "user"
},
params: {},
query: {
limit: 5,
page: 3
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/?page=3&limit=5"
};
middleware.getAll(req, (err, res) => {
expect(err).toBeNull();
expect(res).toHaveProperty("data");
expect(res).toHaveProperty("paging");
expect(res).toHaveProperty("total");
expect(res).toHaveProperty("maxPage");
expect(res.data.length).toBe(5);
expect(res.total).toBe(40);
expect(res.maxPage).toBe(8);
expect(res.data[0]).toHaveProperty("id");
expect(res.data[0]).toHaveProperty("name");
expect(res.data[0]).not.toHaveProperty("createdAt");
expect(res.data[0]).not.toHaveProperty("created");
expect(res.data[0]).not.toHaveProperty("updatedAt");
expect(res.data[0]).not.toHaveProperty("updated");
expect(res.paging.first.href).toBe(
"http://internal.test/v1/?page=1&limit=5"
);
expect(res.paging.prev.href).toBe(
"http://internal.test/v1/?page=2&limit=5"
);
expect(res.paging.next.href).toBe(
"http://internal.test/v1/?page=4&limit=5"
);
expect(res.paging.last.href).toBe(
"http://internal.test/v1/?page=8&limit=5"
);
done();
});
});
});

166
test/getOne.test.js Normal file
View file

@ -0,0 +1,166 @@
/* eslint-disable jest/no-test-callback */
import { createBrands, createCar, truncate } from "./utils/common";
import models from "../models";
import Brands from "../rules/Brands";
import Middelware from "../index";
const createdCars = [];
let createdBrand = {};
describe("getOne", () => {
beforeAll(done => {
createBrands(4, (err, res) => {
if (err) {
done(err);
} else {
// eslint-disable-next-line prefer-destructuring
createdBrand = res[1];
createCar(createdBrand.id, true, 2004, (errCar, car) => {
if (errCar) {
done(errCar);
}
createdCars.push(car);
createCar(createdBrand.id, false, 2004, (errCar2, car2) => {
if (errCar2) {
done(errCar2);
}
createdCars.push(car2);
createCar(createdBrand.id, true, 2003, (errCar3, car3) => {
if (errCar3) {
done(errCar3);
}
createdCars.push(car3);
createCar(createdBrand.id, true, 1998, (errCar4, car4) => {
if (errCar4) {
done(errCar4);
}
createdCars.push(car4);
done();
});
});
});
});
}
});
});
afterAll(done => {
truncate(["Brands", "Cars"], done);
});
test("It should return one item with missing part", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "user"
},
params: {
brandId: createdBrand.id
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.id).toBe(createdBrand.id);
expect(res.name).toBe(createdBrand.name);
expect(res).not.toHaveProperty("Cars");
done();
});
});
test("It should return one item with all part", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {
brandId: createdBrand.id
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.id).toBe(createdBrand.id);
expect(res.name).toBe(createdBrand.name);
expect(res).toHaveProperty("Cars");
expect(res.Cars.length).toBe(4);
expect(res.Cars[0]).toHaveProperty("id");
expect(res.Cars[0]).toHaveProperty("name");
expect(res.Cars[0]).toHaveProperty("year");
expect(res.Cars[0]).toHaveProperty("created");
expect(res.Cars[0]).toHaveProperty("updated");
expect(res.Cars[0]).not.toHaveProperty("createdAt");
expect(res.Cars[0]).not.toHaveProperty("updatedAt");
done();
});
});
test("It should return errorCode 406.1 when missing itemId", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getOne(req, (err, res) => {
expect(parseFloat(err.errorCode)).toBe(406.1);
expect(res).toBeUndefined();
done();
});
});
test("It should return empty result when item not found", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "GET",
user: {
role: "admin"
},
params: {
brandId: createdBrand.id + 666
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.getOne(req, (err, res) => {
expect(parseFloat(err.errorCode)).toBe(404.0);
expect(res).toBeUndefined();
done();
});
});
});

163
test/patchOne.test.js Normal file
View file

@ -0,0 +1,163 @@
/* eslint-disable jest/no-test-callback */
import { createBrand, truncate } from "./utils/common";
import models from "../models";
import Brands from "../rules/Brands";
import Middelware from "../index";
let brand = {};
describe("patchOne", () => {
beforeAll(done => {
createBrand((err, item) => {
if (err) {
done(err);
} else {
brand = item;
done();
}
});
});
afterAll(done => {
truncate(["Brands"], done);
});
test("It should return 401.1 if guest tries to update item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "PATCH",
user: null,
params: {
brandId: brand.id
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
});
});
test("It should return 401.1 if bad role tries to update item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "PATCH",
user: {
role: "user"
},
params: {
brandId: brand.id
},
query: {},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(401.1);
done();
});
});
test("It should return 406.1 if allowed user tries to patch item with unallowed values", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "PATCH",
user: {
role: "admin"
},
params: {
brandId: brand.id
},
query: {},
body: {
created: "test",
name: "TEST"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseFloat(err.errorCode)).toBe(406.1);
done();
});
});
test("It should return updated item if allowed user patch item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "PATCH",
user: {
role: "admin"
},
params: {
brandId: brand.id
},
query: {},
body: {
name: "TEST"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(req, (err, res) => {
expect(err).toBeNull();
expect(res.name).toBe("TEST");
done();
});
});
test("It should return 404.1 if allowed user tries to patch not found item", async done => {
const middleware = new Middelware(Brands, models);
const req = {
method: "PATCH",
user: {
role: "admin"
},
params: {
brandId: brand.id + 1
},
query: {},
body: {
name: "TEST"
},
protocol: "http",
get: () => {
return "internal.test/";
},
originalUrl: "v1/"
};
middleware.patchOne(req, (err, res) => {
expect(res).toBeUndefined();
expect(parseInt(err.errorCode, 10)).toBe(404);
done();
});
});
});

49
test/utils/common.js Normal file
View file

@ -0,0 +1,49 @@
import uuid from "uuid/v4";
import models from "../../models";
import truncateDefault from "./truncate";
const _createCar = (brandId, active, year, done) => {
models.Cars.create({
name: uuid(),
year,
active,
brandId
})
.then(item => {
done(null, item);
})
.catch(done);
};
const _createBrand = done => {
models.Brands.create({
name: uuid()
})
.then(item => {
done(null, item);
})
.catch(done);
};
const _createBrands = (total, done) => {
const created = [];
const next = () => {
if (total === created.length) {
done(null, created);
}
};
for (let i = 0; i < total; i += 1) {
_createBrand((err, res) => {
if (!err) {
created.push(res);
next();
}
});
}
};
export const createBrand = _createBrand;
export const createBrands = _createBrands;
export const createCar = _createCar;
export const truncate = truncateDefault;

5
test/utils/setup.js Normal file
View file

@ -0,0 +1,5 @@
jest.setTimeout(3000);
afterAll(done => {
done();
});

45
test/utils/truncate.js Normal file
View file

@ -0,0 +1,45 @@
import models from "../../models";
const _truncateCars = (tables, done) => {
if (tables.indexOf("Cars") === -1) {
done(null);
return true;
}
models.CarsColors.destroy({
where: {}
})
.then(() => {
models.Cars.destroy({
where: {}
}).then(() => done(null));
})
.catch(err => done(err));
return true;
};
const _truncateBrands = (tables, done) => {
if (tables.indexOf("Brands") === -1) {
done(null);
return true;
}
models.Brands.destroy({
where: {}
})
.then(() => done(null))
.catch(err => done(err));
return true;
};
export default (tables, done) => {
_truncateCars(tables, errCars => {
if (errCars) {
done(errCars);
} else {
_truncateBrands(tables, errBrands => {
done(errBrands);
});
}
});
};

6138
yarn.lock Normal file

File diff suppressed because it is too large Load diff