diff --git a/models/Spell.js b/models/Spell.js deleted file mode 100644 index e69de29..0000000 diff --git a/repositories/ingredient-repository.js b/repositories/ingredient-repository.js new file mode 100644 index 0000000..285d539 --- /dev/null +++ b/repositories/ingredient-repository.js @@ -0,0 +1,27 @@ +'use strict' +// Bookshelf +const bookshelf = require('../database/connection').bookshelf + +const Spells = require('./spell-repository') + +class IngredientRepository { + + constructor() { + this.model = bookshelf.Model.extend({ + tableName: 'ingredient', + spells() { + return this.belongsToMany( Spells._model, 'spell_ingredient', 'ingredient_id', 'spell_id') + } + }) + } + + set model(model) { + this._model = model + } + + get model() { + return this._model + } +} + +module.exports = IngredientRepository \ No newline at end of file diff --git a/repositories/meta-school-repository.js b/repositories/meta-school-repository.js new file mode 100644 index 0000000..c8f8960 --- /dev/null +++ b/repositories/meta-school-repository.js @@ -0,0 +1,27 @@ +'use strict' +// Bookshelf +const bookshelf = require('../database/connection').bookshelf + +const Schools = require('./school-repository') + +class MetaSchoolRepository { + + constructor() { + this.model = bookshelf.Model.extend({ + tableName: 'meta_school', + schools() { + return this.hasMany( Schools._model ) + } + }) + } + + set model(model) { + this._model = model + } + + get model() { + return this._model + } +} + +module.exports = MetaSchoolRepository \ No newline at end of file diff --git a/repositories/school-repository.js b/repositories/school-repository.js new file mode 100644 index 0000000..f625679 --- /dev/null +++ b/repositories/school-repository.js @@ -0,0 +1,33 @@ +'use strict' +// Bookshelf +const bookshelf = require('../database/connection').bookshelf + +const Spells = require('./spell-repository') + +const MetaSchoolRepository = require('./meta-school-repository') +const MetaSchools = new MetaSchoolRepository() + +class SchoolRepository { + + constructor() { + this.model = bookshelf.Model.extend({ + tableName: 'school', + spells() { + return this.belongsToMany( Spells._model, 'spell_school', 'school_id', 'spell_id') + }, + meta_schools() { + return this.belongsTo( MetaSchools._model, 'meta_school_id') + } + }) + } + + set model(model) { + this._model = model + } + + get model() { + return this._model + } +} + +module.exports = SchoolRepository \ No newline at end of file diff --git a/repositories/spell-repository.js b/repositories/spell-repository.js new file mode 100644 index 0000000..fdf2c6c --- /dev/null +++ b/repositories/spell-repository.js @@ -0,0 +1,160 @@ +'use strict' + +// Bookshelf +const bookshelf = require('../database/connection').bookshelf + +const SchoolRepository = require('./school-repository') +const Schools = new SchoolRepository() + +const IngredientRepository = require('./ingredient-repository') +const Ingredients = new IngredientRepository() + +const VariableRepository = require('./variable-repository') +const Variables = new VariableRepository() + +// Model validation +const Validator = require('jsonschema').Validator +const v = new Validator() +const SpellModel = require("../models/SpellValidation") +v.addSchema(SpellModel, "/SpellModel") + +// Validations +const regexXSS = RegExp(/<[^>]*script/) + +// Error handling +const { HttpError } = require('../models/Errors') + +class SpellRepository { + + constructor() { + this.model = bookshelf.Model.extend({ + tableName: 'spell', + schools() { + return this.belongsToMany( Schools._model, 'spell_school', 'spell_id', 'school_id' ) + }, + variables() { + return this.belongsToMany( Variables._model, 'spell_variable', 'spell_id', 'variable_id' ) + }, + ingredients() { + return this.belongsToMany( Ingredients._model, 'spell_ingredient', 'spell_id', 'ingredient_id' ) + } + }) + } + + set model(model) { + this._model = model + } + + get model() { + return this._model + } + + getAll() { + return new Promise((resolve, reject) => { + this._model.forge() + .fetchAll({ withRelated: ['schools.meta_schools', 'variables', 'ingredients'] }) + .then(v => { + resolve(v.toJSON({ omitPivot: true })) + }) + .catch(err => { + console.log(err) + reject(new HttpError(500, "Couldn't get spells")) + }) + }) + } + + getOne(id) { + return new Promise((resolve, reject) => { + this._model.forge() + .where({ 'id' : id }) + .fetch({ withRelated: ['schools.meta_schools', 'variables', 'ingredients'] }) + .then(v => { + resolve(v.toJSON({ omitPivot: true })) + }) + .catch(err => { + console.log(err) + reject(new HttpError(500, "Couldn't get spell")) + }) + }) + } + + addOne(s) { + return new Promise((resolve, reject) => { + // Checks if body exists and if the model fits, and throws errors if it doesn't + if (this.isEmptyObject(s)) { + reject(new HttpError(403, "Error: Spell cannot be nothing !")) + } else if (!v.validate(s, SpellModel).valid) { + reject(new HttpError(403, "Error: Schema is not valid - " + v.validate(s, SpellModel).errors)) + } else if (this.isXSSAttempt(s.name) || this.isXSSAttempt(s.description) || this.isXSSAttempt(s.cost)) { + reject(new HttpError(403, 'Injection attempt detected, aborting the request.')) + } else { + bookshelf.transaction(t => { + return this._model.forge({ + 'name': s.name, + 'description': s.description, + 'level': s.level, + 'charge' : s.charge, + 'cost' : s.cost, + 'is_ritual' : s.is_ritual + }).save(null, { + transacting: t + }) + .tap(spell => { + return spell + .schools() + .attach(s.schools, { + transacting: t + }); + }) + .tap(spell => { + return spell + .variables() + .attach(s.variables, { + transacting: t + }); + }) + .tap(spell => { + return spell + .ingredients() + .attach(s.ingredients, { + transacting: t + }); + }) + .catch(err => { + throw err + }) + }) + .then(v => { + return v.load(['schools.meta_schools', 'variables', 'ingredients']) + }) + .then(v => { + resolve(this.getOne(v.id)) + }) + .catch(err => { + console.log(err) + reject(new HttpError(500, "Insert error")) + }) + } + }) + } + + // Check if object is null + isEmptyObject = (obj) => { + if (Object.keys(obj).length === 0 && obj.constructor === Object) { + return true + } else { + return false + } + } + + // Check if script injection attempt + isXSSAttempt = (string) => { + if (regexXSS.test(string)) { + return true + } else { + return false + } + } +} + +module.exports = SpellRepository \ No newline at end of file diff --git a/repositories/user-repository.js b/repositories/user-repository.js new file mode 100644 index 0000000..b6d1abc --- /dev/null +++ b/repositories/user-repository.js @@ -0,0 +1,85 @@ +'use strict' + +// Bookshelf +const bookshelf = require('../database/connection').bookshelf + +// Hashing and passwords +const bcrypt = require('bcrypt') + +// Model validation +const Validator = require('jsonschema').Validator +const v = new Validator() +const UserModel = require("../models/UserValidation") +v.addSchema(UserModel, "/UserModel") + +// Validations +const regexInt = RegExp(/^[1-9]\d*$/) +const regexXSS = RegExp(/<[^>]*script/) + +// Error handling +const { HttpError } = require('../models/Errors') + +class UserRepository { + constructor() { + this.model = bookshelf.Model.extend({ + tableName: 'user', + }) + } + + set model(model) { + this._model = model + } + + get model() { + return this._model + } + + getAll() { + return new Promise((resolve, reject) => { + this._model.forge() + .fetchAll() + .then(v => { + resolve(v.toJSON({ omitPivot: true })) + }) + .catch(err => { + console.log(err) + reject(new HttpError(500, "Couldn't get users")) + }) + }) + } + + getOne(id) { + return new Promise((resolve, reject) => { + this._model.forge() + .where({ 'id' : id }) + .fetch() + .then(v => { + resolve(v.toJSON({ omitPivot: true })) + }) + .catch(err => { + console.log(err) + reject(new HttpError(500, "Couldn't get user")) + }) + }) + } + + // Check if object is null + isEmptyObject = (obj) => { + if (Object.keys(obj).length === 0 && obj.constructor === Object) { + return true + } else { + return false + } + } + + // Check if script injection attempt + isXSSAttempt = (string) => { + if (regexXSS.test(string)) { + return true + } else { + return false + } + } +} + +module.exports = UserRepository \ No newline at end of file diff --git a/repositories/variable-repository.js b/repositories/variable-repository.js new file mode 100644 index 0000000..2be3ca1 --- /dev/null +++ b/repositories/variable-repository.js @@ -0,0 +1,27 @@ +'use strict' +// Bookshelf +const bookshelf = require('../database/connection').bookshelf + +const Spells = require('./spell-repository') + +class VariableRepository { + + constructor() { + this.model = bookshelf.Model.extend({ + tableName: 'variable', + spells() { + return this.belongsToMany( Spells._model, 'spell_variable', 'variable_id', 'spell_id') + } + }) + } + + set model(model) { + this._model = model + } + + get model() { + return this._model + } +} + +module.exports = VariableRepository \ No newline at end of file diff --git a/routes/spells.js b/routes/spells.js index d989d19..043b8ab 100644 --- a/routes/spells.js +++ b/routes/spells.js @@ -8,87 +8,18 @@ let router = express.Router() const connection = require('../database/connection') const db = connection.db -// Validations +// Repository +const SpellReposity = require('../repositories/spell-repository'); +const Spells = new SpellReposity(); + const regexInt = RegExp(/^[1-9]\d*$/) -const regexXSS = RegExp(/<[^>]*script/) - -// Bookshelf Models -const bookshelf = require('../database/connection').bookshelf - -const Spell = bookshelf.Model.extend({ - tableName: 'spell', - schools() { - return this.belongsToMany( School, 'spell_school', 'spell_id', 'school_id' ) - }, - variables() { - return this.belongsToMany( Variable, 'spell_variable', 'spell_id', 'variable_id' ) - }, - ingredients() { - return this.belongsToMany( Ingredient, 'spell_ingredient', 'spell_id', 'ingredient_id' ) - } -}) - -const MetaSchool = bookshelf.Model.extend({ - tableName: 'meta_school', - schools() { - return this.hasMany( School ) - } -}) - -const School = bookshelf.Model.extend({ - tableName: 'school', - spells() { - return this.belongsToMany( Spell, 'spell_school', 'school_id', 'spell_id') - }, - meta_school() { - return this.belongsTo( MetaSchool, 'meta_school_id') - } -}) - -const Ingredient = bookshelf.Model.extend({ - tableName: 'ingredient', - spells() { - return this.belongsToMany( Spell, 'spell_ingredient', 'ingredient_id', 'spell_id') - } -}) - -const Variable = bookshelf.Model.extend({ - tableName: 'variable', - spells() { - return this.belongsToMany( Spell, 'spell_variable', 'variable_id', 'spell_id') - } -}) - -// Model validation -const Validator = require('jsonschema').Validator -const v = new Validator() -const SpellModel = require("../models/SpellValidation") -v.addSchema(SpellModel, "/SpellModel") - -// Error handling -const { HttpError } = require('../models/Errors') // ROUTES // GET ALL ------------------ const getSpells = () => { - return new Promise((resolve, reject) => { - - Spell.forge() - .fetchAll({ withRelated: ['schools.meta_school', 'variables', 'ingredients'] }) - .then(v => { - resolve(v.toJSON({ omitPivot: true })) - }) - .catch(err => { - console.log(err) - if (err.message == "EmptyResponse") { - reject(new HttpError(404, "No spells matching this id were found")) - } else { - reject(new HttpError(500, err.message)) - } - }) - - }) + return Spells.getAll() .catch(err => { + console.log(err) throw err }) } @@ -111,20 +42,9 @@ router.get('/', async (req, res) => { // GET ONE ------------------ const getSpell = (id) => { - return new Promise((resolve, reject) => { - - Spell.forge() - .where({ 'id' : id }) - .fetch({ withRelated: ['schools.meta_school', 'variables', 'ingredients'] }) - .then(v => { - resolve(v.toJSON({ omitPivot: true })) - }) - .catch(err => { - reject(new HttpError(500, err.message)) - }) - - }) + return Spells.getOne(id) .catch(err => { + console.log(err) throw err }) } @@ -147,144 +67,9 @@ router.get('/:id/', async (req, res) => { // CREATE ONE ------------------ const addSpell = (s) => { - return new Promise(async (resolve, reject) => { - - // Checks if body exists and if the model fits, and throws errors if it doesn't - if (isEmptyObject(s)) { - reject(new HttpError(403, "Error: Spell cannot be nothing !")) - } else if (!v.validate(s, SpellModel).valid) { - reject(new HttpError(403, "Error: Schema is not valid - " + v.validate(s, SpellModel).errors)) - } else if (isXSSAttempt(s.name) || isXSSAttempt(s.description) || isXSSAttempt(s.cost)) { - reject(new HttpError(403, 'Injection attempt detected, aborting the request.')) - } else { - - Spell.forge({ - 'name': s.name, - 'description': s.description, - 'level': s.level, - }) - - // let query = - // 'INSERT INTO spell (name, description' - - // if (s.level !== null) { query += ', level' } - // if (s.charge !== null) { query += ', charge' } - // if (s.cost !== null) { query += ', cost' } - // if (s.is_ritual !== null) { query += ', is_ritual' } - - // query += `) VALUES (${db.escape(s.name)}, ${db.escape(s.description)}` - - // if (s.level !== null) { query += `, ${s.level}` } - // if (s.charge !== null) { query += `, ${s.charge}` } - // if (s.cost !== null) { query += `, ${db.escape(s.cost)}` } - // if (s.is_ritual !== null) { query += `, ${s.is_ritual}` } - - // query += ')' - - // db.query(query, async (err, result) => { - // if (err) { - // reject(new HttpError(500, 'Database error')) - // } - // console.log(`Inserted "${s.name}" with ID ${result.insertId}, affecting ${result.affectedRows} row(s)`) - - // const new_spell_id = result.insertId - - // let addSchoolsData = () => { - // return new Promise((resolve, reject) => { - // if (s.schools !== null) { - // if (s.schools.length > 0) { - // for (let i = 0; i < s.schools.length; i++) { - // if (!regexInt.test(s.schools[i].id)) { - // reject(new HttpError(403, 'Query error - School ID should be an integer !')) - // } - - // let insert_schools_query = `INSERT INTO spells_schools (id_spell, id_school) VALUES (${new_spell_id}, ${s.schools[i].id})` - - // db.query(insert_schools_query, async (err, result) => { - // if (err) { - // reject(new HttpError(404, 'Database error - No schools matching this ID')) - // } else { - // console.log(`Associated school ID ${s.schools[i].id} to spell ID ${new_spell_id}, affecting ${result.affectedRows} row(s)`) - // resolve() - // } - // }) - // } - // } else { - // resolve() - // } - // } else { - // resolve() - // } - // }) - // } - - // let addVariablesData = () => { - // return new Promise((resolve, reject) => { - // if (s.variables !== null) { - // if (s.variables.length > 0) { - // for (let i = 0; i < s.variables.length; i++) { - // if (!regexInt.test(s.variables[i].id)) { - // reject(new HttpError(403, 'Query error - Variable ID should be an integer !')) - // } - - // let insert_variables_query = `INSERT INTO spells_variables (id_spell, id_variable) VALUES (${new_spell_id}, ${s.variables[i].id})` - // db.query(insert_variables_query, async (err, result) => { - // if (err) { - // reject(new HttpError(404, 'Database error - No variables matching this ID')) - // } else { - // console.log(`Associated variable ID ${s.variables[i].id} to spell ID ${new_spell_id}, affecting ${result.affectedRows} row(s)`) - // resolve() - // } - // }) - // } - // } else { - // resolve() - // } - // } else { - // resolve() - // } - // }) - // } - - // let addIngredientsData = () => { - // return new Promise((resolve, reject) => { - // if (s.ingredients !== null) { - // if (s.ingredients.length > 0) { - // for (let i = 0; i < s.ingredients.length; i++) { - // if (!regexInt.test(s.ingredients[i].id)) { - // reject(new HttpError(403, 'Query error - Ingredient ID should be an integer !')) - // } - - // let insert_ingredients_query = `INSERT INTO spells_ingredients (id_spell, id_ingredient) VALUES (${new_spell_id}, ${s.ingredients[i].id})` - // db.query(insert_ingredients_query, async (err, result) => { - // if (err) { - // reject(new HttpError(404, 'Database error - No ingredients matching this ID')) - // } else { - // console.log(`Associated ingredient ID ${s.ingredients[i].id} to spell ID ${new_spell_id}, affecting ${result.affectedRows} row(s)`) - // resolve() - // } - // }) - // } - // } else { - // resolve() - // } - // } else { - // resolve() - // } - // }) - // } - - // Promise.all([addSchoolsData(), addVariablesData(), addIngredientsData()]) - // .then(() => { - // resolve(getSpell(new_spell_id)) - // }) - // .catch(err => { - // reject(err) - // }) - // }) - } - }).catch(err => { - throw err + return Spells.addOne(s) + .catch(err => { + console.log(err) }) } router.post('/', async (req, res) => { @@ -627,107 +412,4 @@ router.param('id', (req, res, next, id) => { } }) -// SHARED FUNCTIONS ------------------ - -// Builds the associated infos for a given spell object -const buildSpell = async (spell) => { - - // Fetches the spell's schools - let fetchSpellSchoolData = (s) => { - return new Promise((resolve, reject) => { - - if (s == null) { reject(new HttpError(404, "Error: No spell matching this ID"))} - - let query = - "SELECT school.id, school.name " + - "FROM spells_schools AS sc " + - "INNER JOIN school AS school ON sc.id_school = school.id " + - "WHERE sc.id_spell = " + s.id - - db.query(query, (err, result) => { - if (err) { - reject(new HttpError(500, 'Database error')) - } else { - s.schools = result - resolve(s) - } - }) - }) - } - - // Fetches the spell's variables - let fetchSpellVariablesData = (s) => { - return new Promise((resolve, reject) => { - - if (s == null) { reject(new HttpError(404, "Error: No spell matching this ID"))} - - let query = - "SELECT variable.id, variable.description " + - "FROM spells_variables AS sv " + - "INNER JOIN variable AS variable ON sv.id_variable = variable.id " + - "WHERE sv.id_spell = " + s.id - - db.query(query, (err, result) => { - if (err) { - reject(new HttpError(500, 'Database error')) - } else { - s.variables = result - resolve(s) - } - }) - }) - } - - // Fetches the spell's ingredients - let fetchSpellIngredientsData = (s) => { - return new Promise((resolve, reject) => { - - if (s == null) { reject(new HttpError(404, "Error: No spell matching this ID"))} - - let query = - "SELECT ingredient.id, ingredient.name " + - "FROM spells_ingredients AS si " + - "INNER JOIN ingredient AS ingredient ON si.id_ingredient = ingredient.id " + - "WHERE si.id_spell = " + s.id - - db.query(query, (err, result) => { - if (err) { - reject(new HttpError(500, 'Database error')) - } else { - s.ingredients = result - resolve(s) - } - }) - }) - } - - // Builds the spell and returns it - let s = await fetchSpellSchoolData(spell) - .then(s => { return fetchSpellVariablesData(s) }) - .then(s => { return fetchSpellIngredientsData(s) }) - .catch(err => { - throw err - }) - - return s -} - -// Check if object is null -const isEmptyObject = (obj) => { - if (Object.keys(obj).length === 0 && obj.constructor === Object) { - return true - } else { - return false - } -} - -// Check if script injection attempt -const isXSSAttempt = (string) => { - if (regexXSS.test(string)) { - return true - } else { - return false - } -} - module.exports = router diff --git a/routes/users.js b/routes/users.js index 793526b..3c932c1 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,3 +1,5 @@ +'use strict' + // Router const express = require('express') let router = express.Router() @@ -6,40 +8,18 @@ let router = express.Router() const connection = require('../database/connection') const db = connection.db -// Hashing and passwords -const bcrypt = require('bcrypt') +// Repository +const UserRepository = require('../repositories/user-repository'); +const Users = new UserRepository(); -// Validations const regexInt = RegExp(/^[1-9]\d*$/) -const regexXSS = RegExp(/<[^>]*script/) - -// Model validation -const Validator = require('jsonschema').Validator -const v = new Validator() -const User = require("../models/UserValidation") -v.addSchema(User, "/UserModel") - -// Error handling -const { HttpError } = require('../models/Errors') // ROUTES // GET ALL ------------------ const getUsers = () => { - return new Promise((resolve, reject) => { - - let query = "SELECT DISTINCT * FROM user" - - db.query(query, async (err, result) => { - if (err) { - reject(new HttpError(500, 'Database error')) - } else if (result.length == 0) { - reject(new HttpError(404, 'No users were found')) - } else { - resolve(result) - } - }) - }) + return Users.getAll() .catch(err => { + console.log(err) throw err }) } @@ -62,21 +42,9 @@ router.get('/', async (req, res) => { // GET ONE ------------------ const getUser = (id) => { - return new Promise((resolve, reject) => { - - let query = "SELECT * FROM user WHERE id = " + db.escape(id) - - db.query(query, async (err, result) => { - if (err) { - reject(new HttpError(500, 'Database error')) - } else if (result.length == 0) { - reject(new HttpError(404, 'No User matching this ID')) - } else { - resolve(result) - } - }) - }) + return Users.getOne(id) .catch(err => { + console.log(err) throw err }) } @@ -211,33 +179,11 @@ router.param('id', (req, res, next, id) => { if (regexInt.test(id)) { next() } else { - throw new HttpError(403, 'Provided ID must be an integer and not zero') + new Error } } catch (err) { - res.status(err.code).send(JSON.stringify( - { - "error": err.message, - }) - ) + throw new HttpError(403, 'Provided ID must be an integer and not zero') } }) -// Check if object is null -const isEmptyObject = (obj) => { - if (Object.keys(obj).length === 0 && obj.constructor === Object) { - return true - } else { - return false - } -} - -// Check if script injection attempt -const isXSSAttempt = (string) => { - if (regexXSS.test(string)) { - return true - } else { - return false - } -} - module.exports = router \ No newline at end of file