diff --git a/api/v1/index.js b/api/v1/index.js index 42ee87a..bb59336 100644 --- a/api/v1/index.js +++ b/api/v1/index.js @@ -1,63 +1,110 @@ const router = require("express").Router(); -const response = require("./response-wrapper"); -// const config = require('../../config-handler'); +const fs = require("fs"); +const config = require('../../config-handler'); +const database = require('../../database'); +const bodyParser = require('body-parser'); // Парсинг куки //router.use(require('cookie-parser')()); -// Загрузка музыки при помощи спец. метода -function muzicLoad(req, res) { - res.setHeader("Content-Type", "audio/mpeg"); - global.database.muzic.get( - (err, data) => { - data = data[0]?.data; - if (err) { - res.send(Buffer.from([])); - } else { - res.send(!data ? Buffer.from([]) : data); - } - }, - { where: { id: !Number.isInteger(+req.query.id) ? 0 : +req.query.id } }, - ); -} +// Парсим JSON +router.use(bodyParser.json({ type: 'application/*+json' })) +router.use(bodyParser.json({ type: 'application/json' })) +const unknownError = (err) => { + const stackId = new Date().getTime(); + let errorLoggingFolder = config().error_logs_folder; + errorLoggingFolder = !["/", "\\"].includes(errorLoggingFolder.at(-1)) + ? errorLoggingFolder + "/" + : errorLoggingFolder; + fs.writeFileSync( + `${errorLoggingFolder}error_${stackId}.log`, + `ERROR: +Date: ${new Date()} +Name: ${err.name} +Message: ${err.message} +Stack: +${err.stack}`, + ); + + return { + error: "UNKNOWN_ERROR", + details: { + trace_id: stackId, + }, + }; +}; + +// Загрузка музыки при помощи спец. метода +router.get('/music', async (req, res) => { + res.setHeader("Content-Type", "audio/mpeg"); + database.music.findAll({ + where: { id: !Number.isInteger(+req.query.id) ? 0 : +req.query.id }, raw: true + }).then((data) => { + data = data[0]?.data; + res.send(!data ? Buffer.from([]) : data); + }).catch((err) => { + res.send(Buffer.from([])); + }); +}); + +// middleware (Обработка типа ответа) +router.use((req, res, next) => { + const unknownResponseFormat = "UNKNOWN_RESPONSE_FORMAT"; + + if (!req.query.response_format) { + res.send(unknownResponseFormat); + } + else if (req.query.response_format === 'json') { + res.result = function (data, isErr = false, code = 200) { + this.status(code).json(!isErr ? { + response : data + } : { + error : data + }); + }; + next(); + } + else { + res.send(unknownResponseFormat); + } +}); + +// Методы router.use( require('./methods') ); -// Подгрузка с файла -/*router.use("/:method_name", async (req, res, next, ...etc) => { - if (req.params.method_name === "muzic") { - muzicLoad(req, res); - return; - } - - try { - const methodFunct = require(`./methods/${req.params.method_name}`); - response(methodFunct, req, res); - } catch (e) { - if (e.message.includes("Cannot find module")) { - const ApiError = require("./errorClass"); - res.status(400).sendModed( - await response( - (req, res) => { - throw new ApiError("METHOD_NOT_FOUNDED"); - }, - req, - res, - ), - ); - } else { - response( - async () => { - throw e; - }, - req, - res, - ); +// Обработка ошибок +router.use( + async function (err, req, res, next) { + if (err) { + if (err.name === "ApiError") { + req.logger.log( + new Date(), + req.ip, + `Ошибка API.\n${err.stack}`, + true + ); + return res.result({ + error : err.message, + details : err.details + }, true, 400); + } + else { + req.logger.log( + new Date(), + req.ip, + `Критическая ошибка API\n${err.stack}`, + true + ); + return res.result({ + error : unknownError(err) + }, true, 500); + } } + else next(); } -}); -*/ +); module.exports = router; diff --git a/api/v1/methods/create-item.js b/api/v1/methods/create-item.js index 15e31cb..0e03dbf 100644 --- a/api/v1/methods/create-item.js +++ b/api/v1/methods/create-item.js @@ -1,87 +1,75 @@ const ApiError = require("../errorClass"); const config = require("../../../config-handler"); +const database = require("../../../database"); +const apiTypes = require("../typeChecker"); -async function isAuthorExists(authorId) { - if (!authorId) return false; - return ( - ( - await global.database.authors - .promiseMode() - .get({ where: { id: authorId } }) - ).length !== 0 - ); -} +const router = require('express').Router(); async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля type. - ["author", "muzic"].indexOf(req.json?.type) !== -1 && // Проверка поля name. - typeof req.json.name === "string" && - req.json.name.length >= 2 && - (req.json.type === "muzic" - ? true - : config().authors_blacklist.filter((blacklisted) => { - return req.json.name - .toLowerCase() - .includes(blacklisted.toLowerCase()); - }).length === 0) && // Дополнительные поля для muzic - (req.json.type === "muzic" - ? !!( - // Для `muzic` - ( - // Проверка поля author_id. - (await isAuthorExists(req.json.author_id)) && // Проверка поля data. (Передаётся либо ничего, либо строка с base64) - (req.json.data === undefined - ? true - : typeof req.json.data === "string") - ) - ) - : true) - ); + if (req.body === undefined || !req.headers['content-type'].includes('json')) { + throw new ApiError("METHOD_MUST_BE_POST_JSON", { method : 'create-item', no_post : false }); + } + const checker = new apiTypes.TypeChecker(); + + await checker.checkRequired(req.body.type, apiTypes.ItemType, 'type'); + await checker.checkRequired(req.body.name, apiTypes.NameType, 'name'); + + if (req.body.type === 'music') { + await checker.checkRequired(req.body.author_id, apiTypes.AuthorType, 'author_id'); + await checker.checkAdditional(req.body.data, apiTypes.Base64FileType, 'data'); + } + + const result = await checker.calculate(); + if (!result.success) { + throw new ApiError("UNSYNTAX_PARAMS_OR_MISSED_REQUIRED_PARAMS", result); + } } -module.exports = async (req, res) => { - if (req.json === undefined) { - // console.log(req.headers); - throw new ApiError("METHOD_MUST_BE_POST_JSON", { - request_method: req.method, - "content-type": !req.headers["content-type"] - ? null - : req.headers["content-type"], - }); +router.post('/create-item', async (req, res, next) => { + try { + await checkSyntaxArgs(req, res); + + if (req.body.type === "author") { + if ( + config().authors_blacklist.filter((blacklisted) => { + return req.body.name + .toLowerCase() + .includes(blacklisted.toLowerCase()); + }).length !== 0 + ) { + throw new ApiError("AUTHOR_BLACKLISTED"); + } + + const result = await database.authors.create({ + name: req.body.name, + time: Math.round(new Date().getTime() / 1000), + }); + res.result(result.dataValues.id); + } else { + const result = await database.music.create({ + name: req.body.name, + author_id: req.body.author_id, + data: + (req.body.data === undefined || req.body.data === null) + ? undefined + : Buffer.from(req.body.data, "base64"), + time: Math.round(new Date().getTime() / 1000), + }); + res.result(result.dataValues.id); + } } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - type: req.json?.type === undefined ? null : req.json.type, - name: req.json?.name === undefined ? null : req.json.name, - ...(req.json?.type === "muzic" - ? { - author_id: - req.json?.author_id === undefined ? null : req.json?.author_id, - data: req.json?.data === undefined ? null : req.json.data, - } - : {}), - }, - }); + catch (err) { + return next(err); } - if (req.json.type === "author") { - let result = await global.database.authors.promiseMode().add({ - name: req.json.name, - time: Math.round(new Date().getTime() / 1000), - }); - return result.dataValues.id; - } else { - let result = await global.database.muzic.promiseMode().add({ - name: req.json.name, - author_id: req.json.author_id, - data: - req.json.data === undefined - ? undefined - : Buffer.from(req.json.data, "base64"), - time: Math.round(new Date().getTime() / 1000), - }); - return result.dataValues.id; +}); + +router.use('/create-item', async (req, res, next) => { + try { + return next(new ApiError("METHOD_MUST_BE_POST_JSON", { method : 'create-item', no_post : true })); } -}; \ No newline at end of file + catch (err) { + return next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/v1/methods/edit-item.js b/api/v1/methods/edit-item.js index 7599fb0..9ee5b61 100644 --- a/api/v1/methods/edit-item.js +++ b/api/v1/methods/edit-item.js @@ -1,90 +1,88 @@ const ApiError = require("../errorClass"); +const database = require("../../../database"); +const config = require("../../../config-handler"); +const apiTypes = require("../typeChecker"); -async function isAuthorExists(id) { - if (!id || !Number.isInteger(id)) return false; - return ( - (await global.database.authors.promiseMode().get({ where: { id } })) - .length !== 0 - ); -} - -async function isMuzicExists(id) { - if (!id || !Number.isInteger(id)) return false; - let result = - (await global.database.muzic.promiseMode().get({ where: { id } })) - .length !== 0; - console.log("isMuzicExists", result); - return result; -} +const router = require('express').Router(); async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля type - ["author", "muzic"].includes(req.json.type) && // Проверка поля name - (req.json.name === undefined - ? true - : typeof req.json.name === "string" && req.json.name.length >= 2) && // Проверка id - (req.json.type === "author" - ? await isAuthorExists(req.json.id) - : await isMuzicExists(req.json.id)) && // Проверка при type=muzic - (req.json.type === "muzic" - ? // Проверка поля author_id - (req.json.author_id === undefined - ? true - : await isAuthorExists(req.json.author_id)) && // Проверка поля data. (Передаётся либо ничего, либо строка с base64) - ((req.json.data === undefined || req.json.data === null) ? true : typeof req.json.data === "string") - : true) - ); + if (req.body === undefined || !req.headers['content-type'].includes('json')) { + throw new ApiError("METHOD_MUST_BE_POST_JSON", { method : 'edit-item', no_post : false }); + } + const checker = new apiTypes.TypeChecker(); + + await checker.checkRequired(req.body.type, apiTypes.ItemType, 'type'); + await checker.checkAdditional(req.body.name, apiTypes.NameType, 'name'); + + if (req.body.type === 'music') { + await checker.checkRequired(req.body.id, apiTypes.MusicType, 'id'); + + await checker.checkAdditional(req.body.author_id, apiTypes.AuthorType, 'author_id'); + await checker.checkAdditional(req.body.data, apiTypes.Base64FileType, 'data'); + } + else if (req.body.type === 'author') { + await checker.checkRequired(req.body.id, apiTypes.AuthorType, 'id'); + } + + const result = await checker.calculate(); + if (!result.success) { + throw new ApiError("UNSYNTAX_PARAMS_OR_MISSED_REQUIRED_PARAMS", result); + } } -module.exports = async (req, res) => { - if (req.method !== "POST") { - throw new ApiError("METHOD_MUST_BE_POST_JSON", { - request_method: req.method, - }); +router.post('/edit-item', async (req, res, next) => { + try { + await checkSyntaxArgs(req, res); + + if (req.body.type === "author") { + if ( + config().authors_blacklist.filter((blacklisted) => { + return req.body.name + .toLowerCase() + .includes(blacklisted.toLowerCase()); + }).length !== 0 + ) { + throw new ApiError("AUTHOR_BLACKLISTED"); + } + + await database.authors.update( + { + name: req.body.name, + }, + { + where: { + id: req.body.id, + }, + }, + ); + } else { + await database.music.update( + { + name: req.body.name, + author_id: req.body.author_id, + data: !req.body.data ? (req.body.data === null ? null : undefined) : Buffer.from(req.body.data, "base64"), + }, + { + where: { + id: req.body.id, + }, + }, + ); + } + res.result("ok"); } - console.log(await checkSyntaxArgs(req, res)); - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - id: req.json.id === undefined ? null : req.json.id, - type: req.json.type === undefined ? null : req.json.type, - name: req.json.name === undefined ? null : req.json.name, - ...(req.json.type === "muzic" - ? { - author_id: req.json.author_id, - data: req.json.data, - } - : {}), - }, - }); + catch (err) { + return next(err); } +}); - if (req.json.type === "author") { - await global.database.authors.promiseMode().edit( - { - name: req.json.name, - }, - { - where: { - id: req.json.id, - }, - }, - ); - } else { - await global.database.muzic.promiseMode().edit( - { - name: req.json.name, - author_id: req.json.author_id, - data: !req.json.data ? (req.json.data === null ? null : undefined) : Buffer.from(req.json.data, "base64"), - }, - { - where: { - id: req.json.id, - }, - }, - ); +router.use('/edit-item', async (req, res, next) => { + try { + return next(new ApiError("METHOD_MUST_BE_POST_JSON", { method : 'edit-item', no_post : true })); } - return "ok"; -}; \ No newline at end of file + catch (err) { + return next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/v1/methods/get-authors.js b/api/v1/methods/get-authors.js index 1f55801..9bcd0bd 100644 --- a/api/v1/methods/get-authors.js +++ b/api/v1/methods/get-authors.js @@ -1,81 +1,109 @@ +const { Op } = require("sequelize"); const ApiError = require("../errorClass"); +const database = require("../../../database"); +const apiTypes = require("../typeChecker"); + +const router = require('express').Router(); async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля offset - (req.query.id === undefined ? true : Number.isInteger(+req.query.offset)) && // Проверка поля offset - (req.query.offset === undefined - ? true - : Number.isInteger(+req.query.offset)) && // Проверка поля count - (req.query.count === undefined - ? true - : Number.isInteger(+req.query.count)) && // Проверка поля min_date - (req.query.min_date === undefined - ? true - : Number.isInteger(+req.query.min_date)) && // Проверка поля max_date - (req.query.max_date === undefined - ? true - : Number.isInteger(+req.query.max_date)) && // Проверка поля q. (Ключевые слова для поиска) - true // (Проверки нет, query всегда строка, необязательный параметр) - ); + const checker = new apiTypes.TypeChecker(false); + + await checker.checkAdditional(req.query.id, apiTypes.AuthorType, 'id'); + await checker.checkAdditional(req.query.offset, apiTypes.IntType, 'offset'); + await checker.checkAdditional(req.query.count, apiTypes.IntType, 'count'); + await checker.checkAdditional(req.query.min_date, apiTypes.IntType, 'min_date'); + await checker.checkAdditional(req.query.max_date, apiTypes.IntType, 'max_date'); + await checker.checkAdditional(req.query.q, apiTypes.StrType, 'q'); + + const result = await checker.calculate(); + if (!result.success) { + throw new ApiError("UNSYNTAX_PARAMS_OR_MISSED_REQUIRED_PARAMS", result); + } } -module.exports = async (req, res) => { - if (req.method !== "GET") { - throw new ApiError("METHOD_MUST_BE_GET", { - request_method: req.method, - }); - } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - id: req.query?.id === undefined ? null : req.query.id, - q: req.query?.q === undefined ? null : req.query.q, - offset: req.query?.offset === undefined ? null : req.query.offset, - count: req.query?.count === undefined ? null : req.query.count, - min_date: req.query?.min_date === undefined ? null : req.query.min_date, - max_date: req.query?.max_date === undefined ? null : req.query.max_date, - }, - }); - } - - const offset = req.query.offset === undefined ? 0 : +req.query.offset; - const countFromNull = - req.query.count === undefined ? undefined : offset + +req.query.count; - - let result = ( - await global.database.authors.promiseMode().get( - req.query.id === undefined - ? {} - : { - id: +req.query.id, - }, - ) - ).map((i) => ({ - id: i.id, - name: i.name, - date: i.time, - })); - // Если просят выборку по времени - if (req.query.min_date || req.query.max_date) { +router.get('/get-authors', async (req, res, next) => { + try { + await checkSyntaxArgs(req, res); + + const offset = req.query.offset === undefined ? 0 : +req.query.offset; + const countFromNull = + req.query.count === undefined ? undefined : offset + +req.query.count; + + // Выборка по времени + const requiredTimeSearch = req.query.min_date || req.query.max_date; const minDate = req.query.min_date === undefined ? 0 : +req.query.min_date; - const maxDate = - req.query.max_date === undefined ? 1e32 : +req.query.max_date; + const maxDate = req.query.max_date === undefined ? 9999999999 : +req.query.max_date; + + let result = ( + await database.authors.findAll( + req.query.id === undefined + ? { + order: [ + ['id', 'ASC'], + ['name', 'ASC'], + ], + attributes: ['id', 'name', 'time'], + ...(requiredTimeSearch ? ({ + // Запрос на выборку по времени + where : { + time : { + [Op.gte] : minDate, + [Op.lte] : maxDate + } + } + }) : {}), + raw : true + } + : { + order: [ + ['id', 'ASC'], + ['name', 'ASC'], + ], + attributes: ['id', 'name', 'time'], + where : { + id: +req.query.id, + ...(requiredTimeSearch ? ({ + // Запрос на выборку по времени + time : { + [Op.gte] : minDate, + [Op.lte] : maxDate + } + }) : {}) + }, + raw : true + }, + ) + ).map((i) => ({ + id: i.id, + name: i.name, + date: i.time, + })); - result = result.filter((res) => minDate <= res.date && res.date <= maxDate); - } + if (req.query?.q !== undefined) { + result = result.filter((i) => { + const search = req.query.q.toLowerCase(); + return i.name.toLowerCase().includes(search); + }); + } - if (req.query?.q !== undefined) { - result = result.filter((i) => { - const search = req.query.q.toLowerCase(); - return i.name.toLowerCase().includes(search); + result = result.slice(offset, countFromNull); + res.result({ + count: result.length, + items: result, }); } + catch (err) { + return next(err); + } +}); - result = result.slice(offset, countFromNull); - return { - count: result.length, - items: result, - }; -}; \ No newline at end of file +router.use('/get-authors', async (req, res, next) => { + try { + return next(new ApiError("METHOD_MUST_BE_GET", { method : 'get-authors' })); + } + catch (err) { + return next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/v1/methods/get-music.js b/api/v1/methods/get-music.js new file mode 100644 index 0000000..eb50ce6 --- /dev/null +++ b/api/v1/methods/get-music.js @@ -0,0 +1,127 @@ +const { Op } = require("sequelize"); +const ApiError = require("../errorClass"); +const database = require("../../../database"); +const apiTypes = require("../typeChecker"); + +const router = require('express').Router(); + +async function checkSyntaxArgs(req, res) { + const checker = new apiTypes.TypeChecker(false); + + await checker.checkAdditional(req.query.id, apiTypes.MusicType, 'id'); + await checker.checkAdditional(req.query.author, apiTypes.AuthorType, 'author'); + await checker.checkAdditional(req.query.authors, apiTypes.AuthorsType, 'authors'); + await checker.checkAdditional(req.query.offset, apiTypes.IntType, 'offset'); + await checker.checkAdditional(req.query.count, apiTypes.IntType, 'count'); + await checker.checkAdditional(req.query.min_date, apiTypes.IntType, 'min_date'); + await checker.checkAdditional(req.query.max_date, apiTypes.IntType, 'max_date'); + await checker.checkAdditional(req.query.q, apiTypes.StrType, 'q'); + await checker.checkAdditional(req.query.searchByAuthor, apiTypes.StrType, 'searchByAuthor'); + await checker.checkAdditional(req.query.searchByName, apiTypes.StrType, 'searchByName'); + + const result = await checker.calculate(); + if (!result.success) { + throw new ApiError("UNSYNTAX_PARAMS_OR_MISSED_REQUIRED_PARAMS", result); + } +} + +router.get('/get-music', async (req, res, next) => { + try { + await checkSyntaxArgs(req, res); + + // Оптимизация: флаг searchByAuthor выставится на 0, если была запрошена выборка по музыке определённого автора + // Тем самым, мы ускоряем время ответа, поскольку мы не перебираем массив searchedAuthors с целью поиска совпадений + let searchByAuthor = + req.query.author === undefined ? req.query.searchByAuthor : "0"; + + const offset = req.query.offset === undefined ? 0 : +req.query.offset; + const countFromNull = + req.query.count === undefined ? undefined : offset + +req.query.count; + + const searchAuthors = !req.query.authors ? [] : [...req.query.authors.split(',').map(i => +i)]; + if (req.query.author) searchAuthors.push(+req.query.author); + + const minDate = req.query.min_date === undefined ? 0 : +req.query.min_date; + const maxDate = req.query.max_date === undefined ? 9999999999 : +req.query.max_date; + + let searchedParams = []; + if (searchAuthors > 0) { + searchAuthors.forEach(author_id => { + console.log(author_id); + searchedParams.push({ [Op.and] : [ + req.query.id !== undefined ? {id : +req.query.id} : undefined, + { author_id }, + {time : { + [Op.gte]: minDate, + [Op.lte]: maxDate, + }}, + ] }); + }); + } + else { + searchedParams.push({ [Op.and] : [ + req.query.id !== undefined ? {id : +req.query.id} : undefined, + {time : { + [Op.gte]: minDate, + [Op.lte]: maxDate, + }}, + ] }); + } + + console.log('searchedParams', searchedParams); + + let result = ( + await database.music.findAll({ + where : { [Op.or]: searchedParams }, + raws : true + }) + ).map((i) => ({ + id: i.id, + name: i.name, + author_id: i.author_id, + is_data_exists: i.data !== null && i.data.length > 0, + date: i.time, + })); + + if (req.query?.q !== undefined) { + let authors = await database.authors.findAll({raws : true}); + let searchedAuthors = result.map((i) => { + let author_id = i.author_id; + return authors.filter((a) => a.id === author_id)[0]; + }); + result = result.filter((i) => { + const search = req.query.q.toLowerCase(); + return ( + (req.query.searchByName !== "0" && + i.name.toLowerCase().includes(search)) || + !!( + searchByAuthor !== "0" && + searchedAuthors + .filter((a) => a.id === i.author_id)[0] + ?.name.toLowerCase() + .includes(search) + ) + ); + }); + } + result = result.slice(offset, countFromNull); + res.result({ + count: result.length, + items: result, + }); + } + catch (err) { + return next(err); + } +}); + +router.use('/get-music', async (req, res, next) => { + try { + return next(new ApiError("METHOD_MUST_BE_GET", { method : 'get-music' })); + } + catch (err) { + return next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/v1/methods/get-muzic.js b/api/v1/methods/get-muzic.js deleted file mode 100644 index e1ad971..0000000 --- a/api/v1/methods/get-muzic.js +++ /dev/null @@ -1,142 +0,0 @@ -const ApiError = require("../errorClass"); - -async function isAuthorExists(authorId) { - if (!authorId) return false; - return ( - ( - await global.database.authors - .promiseMode() - .get({ where: { id: authorId } }) - ).length !== 0 - ); -} - -async function isAuthorsExists(authorsId) { - if (!Array.isArray(authorsId)) return false; - const authors = (await global.database.authors.promiseMode().get()).map( - (i) => i.id, - ); - return authorsId.map((authorId) => authors.includes(authorId)); -} - -async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля author. - (req.query.author === undefined - ? true - : await isAuthorExists(+req.query.author)) && // Проверка поля authors. - (req.query.authors === undefined - ? true - : await isAuthorsExists(req.query.authors.split(",").map((i) => +i))) && // Проверка поля offset - (req.query.offset === undefined - ? true - : Number.isInteger(+req.query.offset)) && // Проверка поля count - (req.query.count === undefined - ? true - : Number.isInteger(+req.query.count)) && // Проверка поля min_date - (req.query.min_date === undefined - ? true - : Number.isInteger(+req.query.min_date)) && // Проверка поля max_date - (req.query.max_date === undefined - ? true - : Number.isInteger(+req.query.max_date)) && // Проверка поля q. (Ключевые слова для поиска) - true && // (Проверки нет, query всегда строка, необязательный параметр) // Проверка поля searchByAuthor (Флаг для 'q'. 0 - отключить) - true && // (Проверки нет, query всегда строка, необязательный параметр) // Проверка поля searchByName (Флаг для 'q'. 0 - отключить) - true // (Проверки нет, query всегда строка, необязательный параметр) - ); -} - -module.exports = async (req, res) => { - if (req.method !== "GET") { - throw new ApiError("METHOD_MUST_BE_GET", { - request_method: req.method, - }); - } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - author: req.query?.author === undefined ? null : req.query.author, - authors: req.query?.authors === undefined ? null : req.query.authors, - q: req.query?.q === undefined ? null : req.query.q, - offset: req.query?.offset === undefined ? null : req.query.offset, - count: req.query?.count === undefined ? null : req.query.count, - searchByAuthor: - req.query?.searchByAuthor === undefined - ? null - : req.query.searchByAuthor, - searchByName: - req.query?.searchByName === undefined ? null : req.query.searchByName, - min_date: req.query?.min_date === undefined ? null : req.query.min_date, - max_date: req.query?.max_date === undefined ? null : req.query.max_date, - }, - }); - } - - // Оптимизация: флаг searchByAuthor выставится на 0, если была запрошена выборка по музыке определённого автора - // Тем самым, мы ускоряем время ответа, поскольку мы не перебираем массив searchedAuthors с целью поиска совпадений - let searchByAuthor = - req.query.author === undefined ? req.query.searchByAuthor : "0"; - - const offset = req.query.offset === undefined ? 0 : +req.query.offset; - const countFromNull = - req.query.count === undefined ? undefined : offset + +req.query.count; - - let result = ( - await global.database.muzic.promiseMode().get( - req.query?.author === undefined - ? {} - : { - where: { - author_id: +req.query.author, - }, - }, - ) - ).map((i) => ({ - id: i.id, - name: i.name, - author_id: i.author_id, - is_data_exists: i.data !== null && i.data.length > 0, - date: i.time, - })); - // Если просят выборку по authors - if (req.query.authors) { - let authors = req.query.authors.split(",").map((author) => +author); - result = result.filter((res) => authors.includes(res.author_id)); - } - // Если просят выборку по времени - if (req.query.min_date || req.query.max_date) { - const minDate = req.query.min_date === undefined ? 0 : +req.query.min_date; - const maxDate = - req.query.max_date === undefined ? 1e32 : +req.query.max_date; - - result = result.filter((res) => minDate <= res.date && res.date <= maxDate); - } - - if (req.query?.q !== undefined) { - let authors = await global.database.authors.promiseMode().get(); - let searchedAuthors = result.map((i) => { - let author_id = i.author_id; - return authors.filter((a) => a.id === author_id)[0]; - }); - result = result.filter((i) => { - const search = req.query.q.toLowerCase(); - return ( - (req.query.searchByName !== "0" && - i.name.toLowerCase().includes(search)) || - !!( - searchByAuthor !== "0" && - searchedAuthors - .filter((a) => a.id === i.author_id)[0] - ?.name.toLowerCase() - .includes(search) - ) - ); - }); - } - result = result.slice(offset, countFromNull); - return { - count: result.length, - items: result, - }; -}; \ No newline at end of file diff --git a/api/v1/methods/index.js b/api/v1/methods/index.js index 3418369..eeba932 100644 --- a/api/v1/methods/index.js +++ b/api/v1/methods/index.js @@ -1,3 +1,30 @@ const router = require('express').Router(); -router.use(); +router.use( + require('./create-item') +); + +router.use( + require('./edit-item') +); + +router.use( + require('./remove-item') +); + +router.use( + require('./get-authors') +); + +router.use( + require('./get-music') +); + +router.use('*', async (req, res) => { + res.result({ + error : "UNKNOWN_METHOD", + details : {} + }, true, 400); +}); + +module.exports = router; \ No newline at end of file diff --git a/api/v1/methods/remove-item.js b/api/v1/methods/remove-item.js index b0c288a..996b286 100644 --- a/api/v1/methods/remove-item.js +++ b/api/v1/methods/remove-item.js @@ -1,72 +1,68 @@ const ApiError = require("../errorClass"); -const config = require("../../../config-handler"); +const database = require("../../../database"); +const apiTypes = require("../typeChecker"); -async function isAuthorExists(authorId) { - if (!authorId) return false; - return ( - ( - await global.database.authors - .promiseMode() - .get({ where: { id: authorId } }) - ).length !== 0 - ); -} +const router = require('express').Router(); async function checkSyntaxArgs(req, res) { - return !!( - // Проверка поля type. - ["author", "muzic"].indexOf(req.json?.type) !== -1 && // Проверка поля id. - (Number.isInteger(req.json.id) && req.json.type === "author" - ? await isAuthorExists(req.json.id) - : true) - ); + if (req.body === undefined || !req.headers['content-type'].includes('json')) { + throw new ApiError("METHOD_MUST_BE_POST_JSON", { method : 'remove-item', no_post : false }); + } + const checker = new apiTypes.TypeChecker(); + + await checker.checkRequired(req.body.type, apiTypes.ItemType, 'type'); + + if (req.body.type === 'music') { + await checker.checkRequired(req.body.id, apiTypes.MusicType, 'id'); + } + else if (req.body.type === 'author') { + await checker.checkRequired(req.body.id, apiTypes.AuthorType, 'id'); + } + + const result = await checker.calculate(); + if (!result.success) { + throw new ApiError("UNSYNTAX_PARAMS_OR_MISSED_REQUIRED_PARAMS", result); + } } -module.exports = async (req, res) => { - if (req.json === undefined) { - // console.log(req.headers); - throw new ApiError("METHOD_MUST_BE_POST_JSON", { - request_method: req.method, - "content-type": !req.headers["content-type"] - ? null - : req.headers["content-type"], - }); +router.post('/remove-item', async (req, res, next) => { + try { + await checkSyntaxArgs(req, res); + + if (req.body.type === "author") { + // Удаляем всю музыку этого исполнителя + await database.music.destroy({ + where: { + author_id: req.body.id, + }, + }); + // Удаляем исполнителя + await database.authors.destroy({ + where: { + id: req.body.id, + }, + }); + } else { + await database.music.destroy({ + where: { + id: req.body.id, + }, + }); + } + res.result("ok"); } - if (!(await checkSyntaxArgs(req, res))) { - throw new ApiError("INVALID_OR_UNSYNTAX_PARAMS", { - request_method: req.method, - params: { - type: req.json?.type === undefined ? null : req.json.type, - name: req.json?.name === undefined ? null : req.json.name, - ...(req.json?.type === "muzic" - ? { - author_id: - req.json?.author_id === undefined ? null : req.json?.author_id, - data: req.json?.data === undefined ? null : req.json.data, - } - : {}), - }, - }); + catch (err) { + return next(err); } - if (req.json.type === "author") { - // Удаляем всю музыку этого исполнителя - await global.database.muzic.promiseMode().remove({ - where: { - author_id: req.json.id, - }, - }); - // Удаляем исполнителя - await global.database.authors.promiseMode().remove({ - where: { - id: req.json.id, - }, - }); - } else { - await global.database.muzic.promiseMode().remove({ - where: { - id: req.json.id, - }, - }); +}); + +router.use('/remove-item', async (req, res, next) => { + try { + return next(new ApiError("METHOD_MUST_BE_POST_JSON", { method : 'remove-item', no_post : true })); } - return "ok"; -}; \ No newline at end of file + catch (err) { + return next(err); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/api/v1/response-wrapper.js b/api/v1/response-wrapper.js deleted file mode 100644 index c40a381..0000000 --- a/api/v1/response-wrapper.js +++ /dev/null @@ -1,61 +0,0 @@ -const fs = require("fs"); -const config = require("../../config-handler"); - -const unknownError = (err) => { - const stackId = new Date().getTime(); - let errorLoggingFolder = config().error_logs_folder; - errorLoggingFolder = !["/", "\\"].includes(errorLoggingFolder.at(-1)) - ? errorLoggingFolder + "/" - : errorLoggingFolder; - fs.writeFileSync( - `${errorLoggingFolder}error_${stackId}.log`, - `ERROR: -Date: ${new Date()} -Name: ${err.name} -Message: ${err.message} -Stack: -${err.stack}`, - ); - - return { - error: "UNKNOWN_ERROR", - details: { - trace_id: stackId, - }, - }; -}; - -const unknownResponseFormat = "UNKNOWN_RESPONSE_FORMAT"; - -async function handlingError(funct, success, error) { - try { - success(await funct()); - } catch (e) { - // console.log('error', e); - error( - e.name === "ApiError" - ? { - error: e.message, - details: e.details, - } - : unknownError(e), - ); - } -} - -module.exports = async (method, req, res) => { - if (req.query.response_format === "json") { - handlingError( - async () => ({ response: await method(req, res) }), - async (data) => { - res.sendModed(data); - }, - async (errBody) => { - res.errorModeOn(); - res.status(400).sendModed(errBody); - }, - ); - } else { - res.status(400).sendModed(unknownResponseFormat); - } -}; \ No newline at end of file diff --git a/api/v1/typeChecker.js b/api/v1/typeChecker.js index 6bdefba..b53949c 100644 --- a/api/v1/typeChecker.js +++ b/api/v1/typeChecker.js @@ -1,35 +1,203 @@ +const { Op } = require("sequelize"); +const database = require("../../database"); + +// TypeChecker class +class TypeChecker { + constructor (strictMode = true) { + this.strict = strictMode; + + this.requiredMissed = []; + this.additionalMissed = []; + + this.requiredUnsyntax = []; + this.additionalUnsyntax = []; + + this.unsyntax = false; + } + + async checkRequired (param, Type, paramName) { + if (param === undefined) { + this.requiredMissed.push(paramName); + this.unsyntax = true; + } + else { + const type = new Type(param); + if (this.strict && !(await type.checkStrict())) { + this.requiredUnsyntax.push(paramName); + this.unsyntax = true; + } + else if (!this.strict && !(await type.checkNoStrict())) { + this.requiredUnsyntax.push(paramName); + this.unsyntax = true; + } + } + } + + async checkAdditional (param, Type, paramName) { + if (param === undefined) { + this.additionalMissed.push(paramName); + } + else { + const type = new Type(param); + if (this.strict && !(await type.checkStrict())) { + this.additionalUnsyntax.push(paramName); + this.unsyntax = true; + } + else if (!this.strict && !(await type.checkNoStrict())) { + this.additionalUnsyntax.push(paramName); + this.unsyntax = true; + } + } + } + + async calculate () { + return { + success : !this.unsyntax, + missed : { + required : this.requiredMissed, + additional : this.additionalMissed + }, + unsyntax : { + required : this.requiredUnsyntax, + additional : this.additionalUnsyntax + } + } + } +} + + +// Types + class ParamType { constructor (value) { this.value = value; } - checkNoStrict () { + async checkNoStrict () { return true; } - checkStrict () { + async checkStrict () { return true; } } class StrType extends ParamType { - checkStrict () { + async checkStrict () { + return typeof this.value === 'string'; + } + + async checkNoStrict () { return typeof this.value === 'string'; } } class IntType extends ParamType { - checkNoStrict () { + async checkNoStrict () { return Number.isInteger(+this.value); } - checkStrict () { + async checkStrict () { return Number.isInteger(this.value); } } +// Custom types + +class ItemType extends StrType { + async checkStrict () { + if (await super.checkStrict()) return ['music', 'author'].includes(this.value); + return false; + } + + async checkNoStrict () { + if (await super.checkNoStrict()) return ['music', 'author'].includes(this.value); + return false; + } +} + +class NameType extends StrType { + async checkStrict () { + if (await super.checkStrict()) return this.value.length >= 2; + return false; + } + + async checkNoStrict () { + if (await super.checkNoStrict()) return this.value.length >= 2; + return false; + } +} + +class AuthorType extends IntType { + async checkStrict () { + if (await super.checkStrict()) { + const authorId = this.value; + return (await database.authors.findAll({ where: { id: authorId }, raw : true })).length !== 0; + } + return false; + } + + async checkNoStrict () { + if (await super.checkNoStrict()) { + const authorId = +this.value; + return (await database.authors.findAll({ where: { id: authorId }, raw : true })).length !== 0; + } + return false; + } +} + +class AuthorsType extends AuthorType { + async checkStrict () { + if (Array.isArray(this.value) && !this.value.map(i => Number.isInteger(i)).includes(false)) { + // return (await database.authors.findAll({ where: [Op.or]: [ + // { id: 12 }, + // { id: 13 } + // ], raw : true })).length === this.value; + return (await database.authors.findAll({ where: { [Op.or]: this.value.map(id => ({ id })) }, raw : true })).length === this.value; + } + return false; + } + + async checkNoStrict () { + if (await(new StrType(this.value)).checkStrict()) { + const values = this.value.split(',').map(i => +i); + if (!values.map(i => Number.isInteger(i)).includes(false)) { + return (await database.authors.findAll({ where: { [Op.or]: values.map(id => ({ id })) }, raw : true })).length === this.value.length; + } + return false; + } + return false; + } +} + +class MusicType extends IntType { + async checkStrict () { + if (await super.checkStrict()) { + const musicId = this.value; + return (await database.music.findAll({ where: { id: musicId }, raw : true })).length !== 0; + } + return false; + } + + async checkNoStrict () { + if (await super.checkNoStrict()) { + const musicId = +this.value; + return (await database.music.findAll({ where: { id: musicId }, raw : true })).length !== 0; + } + return false; + } +} + +class Base64FileType extends StrType { + async checkStrict () { + return this.value === null || (await super.checkStrict()); + } +} + module.exports = { ParamType, - StrType, IntType + StrType, IntType, + ItemType, NameType, AuthorType, AuthorsType, MusicType, Base64FileType, + TypeChecker }; diff --git a/db-dump/install-it.sql b/db-dump/install-it.sql index c033bb6..30df35b 100644 Binary files a/db-dump/install-it.sql and b/db-dump/install-it.sql differ diff --git a/frontend/main.html b/frontend/main.html index 144d6c4..d64eb03 100644 --- a/frontend/main.html +++ b/frontend/main.html @@ -1,7 +1,7 @@
-Пусто. Добавьте произведения
"; } return; @@ -30,48 +30,48 @@ function showAudio(settingsAuthors, settingsAudio) { el.hidden = !showedAuthors.includes(el.id); }); - muzic.items + music.items .sort((a, b) => a.date - b.date) - .forEach((muzic) => { + .forEach((music) => { document.getElementById( - `loader-muzic-${muzic.author_id}`, + `loader-music-${music.author_id}`, ).hidden = true; - if (muzic.is_data_exists) + if (music.is_data_exists) document.getElementById( - `playlist-author-${muzic.author_id}`, - ).innerHTML += `