diff --git a/package-lock.json b/package-lock.json index 7572876..f56c08f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^8.2.1", + "express-validator": "^7.3.1", "jimp": "^0.22.8", "jsonwebtoken": "^9.0.0", "nanoid": "^5.1.5", @@ -4280,6 +4281,19 @@ "express": ">= 4.11" } }, + "node_modules/express-validator": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index f5d7a74..8cec8ad 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-rate-limit": "^8.2.1", + "express-validator": "^7.3.1", "jimp": "^0.22.8", "jsonwebtoken": "^9.0.0", "nanoid": "^5.1.5", diff --git a/src/controllers/dex.controller.ts b/src/controllers/dex.controller.ts index 690401b..264128c 100644 --- a/src/controllers/dex.controller.ts +++ b/src/controllers/dex.controller.ts @@ -3,6 +3,10 @@ import UserData from '@/interfaces/common/UserData.js'; import Currency from '@/schemes/Currency.js'; import Pair from '@/schemes/Pair.js'; import { Op } from 'sequelize'; +import GetAssetsPriceRatesBody from '@/interfaces/bodies/dex/GetAssetsPriceRatesBody.js'; +import GetAssetsPriceRatesRes, { + GetAssetsPriceRatesResPriceRate, +} from '@/interfaces/responses/dex/GetAssetsPriceRatesRes.js'; import User from '../schemes/User.js'; import ordersModel from '../models/Orders.js'; import dexModel from '../models/Dex.js'; @@ -104,10 +108,10 @@ class DexController { return res.status(200).send(result); } - async getAssetsPriceRates(req: Request, res: Response) { - const { assetsIds } = req.body; + getAssetsPriceRates = async (req: Request, res: Response) => { + const { assetsIds } = req.body as GetAssetsPriceRatesBody; - const currencysRows = await Currency.findAll({ + const currenciesRows = await Currency.findAll({ where: { asset_id: { [Op.in]: assetsIds, @@ -115,61 +119,42 @@ class DexController { }, }); - if (!currencysRows) { - return res.status(200).send({ - success: false, - data: 'Assets with this id doesn`t exists', - }); - } + const currencyIds = currenciesRows.map((currency) => currency.id); - const currencyIds = currencysRows.map((currency) => currency.id); - - const pairsRows = ( - (await Pair.findAll({ - where: { - first_currency_id: { - [Op.in]: currencyIds, - }, + const pairsRows = (await Pair.findAll({ + where: { + first_currency_id: { + [Op.in]: currencyIds, }, - include: [ - { - model: Currency, - as: 'first_currency', - required: true, - attributes: ['asset_id'], - }, - ], - })) || [] - ).map((pair) => ({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - asset_id: pair?.first_currency?.asset_id, - rate: pair.rate, - })); + }, + include: [ + { + model: Currency, + as: 'first_currency', + required: true, + attributes: ['asset_id'], + }, + ], + })) as (Pair & { first_currency: Currency })[]; - if (!pairsRows || pairsRows.length === 0) { - return res.status(200).send({ - success: false, - data: 'Assets with this id doesn`t exists', - }); - } + const priceRates: GetAssetsPriceRatesResPriceRate[] = pairsRows.map((pairRow) => { + const assetId = pairRow.first_currency.asset_id; - // const priceRates = await Promise.all(pairsRows.map(async (pair) => { - // const currency = await Currency.findOne({ where: { - // id: pair.first_currency_id - // }}) - - // return { - // asset_id: currency?.asset_id, - // rate: pair.rate - // } - // })) + return { + asset_id: assetId, + rate: pairRow?.rate ?? null, + day_change: pairRow?.coefficient ?? null, + day_volume: pairRow?.volume ?? null, + day_high: pairRow?.high ?? null, + day_low: pairRow?.low ?? null, + }; + }); return res.status(200).send({ success: true, - priceRates: pairsRows, + priceRates, }); - } + }; async findPairID(req: Request, res: Response) { const { first, second } = req.body; diff --git a/src/interfaces/bodies/dex/GetAssetsPriceRatesBody.ts b/src/interfaces/bodies/dex/GetAssetsPriceRatesBody.ts new file mode 100644 index 0000000..055bb0a --- /dev/null +++ b/src/interfaces/bodies/dex/GetAssetsPriceRatesBody.ts @@ -0,0 +1,14 @@ +import { body } from 'express-validator'; + +interface GetAssetsPriceRatesBody { + assetsIds: string[]; +} + +export const getAssetsPriceRatesValidator = [ + body('assetsIds') + .isArray({ min: 1 }) + .withMessage('assetsIds must be a non-empty array of strings'), + body('assetsIds.*').isString().withMessage('Each assetId must be a string'), +]; + +export default GetAssetsPriceRatesBody; diff --git a/src/interfaces/responses/dex/GetAssetsPriceRatesRes.ts b/src/interfaces/responses/dex/GetAssetsPriceRatesRes.ts new file mode 100644 index 0000000..ed9cb16 --- /dev/null +++ b/src/interfaces/responses/dex/GetAssetsPriceRatesRes.ts @@ -0,0 +1,24 @@ +export type GetAssetsPriceRatesResPriceRate = { + asset_id: string; + rate: number | null; + day_change: number | null; + day_volume: number | null; + day_high: number | null; + day_low: number | null; +}; + +export type GetAssetsPriceRatesSuccessRes = { + success: true; + priceRates: GetAssetsPriceRatesResPriceRate[]; +}; + +export enum GetAssetsPriceRatesErrorCode {} + +export type GetAssetsPriceRatesErrorRes = { + success: false; + data: GetAssetsPriceRatesErrorCode; +}; + +type GetAssetsPriceRatesRes = GetAssetsPriceRatesSuccessRes | GetAssetsPriceRatesErrorRes; + +export default GetAssetsPriceRatesRes; diff --git a/src/middleware/middleware.ts b/src/middleware/middleware.ts index 8e50f66..11f2432 100644 --- a/src/middleware/middleware.ts +++ b/src/middleware/middleware.ts @@ -1,4 +1,5 @@ import { NextFunction, Request, Response } from 'express'; +import { ValidationChain, validationResult } from 'express-validator'; import { rateLimit } from 'express-rate-limit'; import jwt from 'jsonwebtoken'; import User from '@/schemes/User'; @@ -49,6 +50,49 @@ class Middleware { defaultRateLimit = async (req: Request, res: Response, next: NextFunction) => defaultRateLimitMiddleware(req, res, next); + expressValidator(validators: ValidationChain[]) { + return [ + ...validators, + (req: Request, res: Response, next: NextFunction) => { + const errors = validationResult(req); + + if (!errors.isEmpty()) { + res.status(500).send({ + success: false, + data: 'Internal error', + }); + return; + } + + next(); + }, + ]; + } + + expressJSONErrorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => { + const isExpressJSONError = + err instanceof SyntaxError && 'status' in err && err.status === 400 && 'body' in err; + + if (isExpressJSONError) { + res.status(500).send({ + success: false, + data: 'Internal error', + }); + } else { + next(); + } + }; + + globalErrorHandler = (err: Error, req: Request, res: Response, _next: NextFunction) => { + console.error('Global error handler:'); + console.error(err); + res.status(500).send({ + success: false, + data: 'Internal error', + }); + }; + + resultGlobalErrorHandler = [this.expressJSONErrorHandler, this.globalErrorHandler]; } const middleware = new Middleware(); diff --git a/src/routes/dex.router.ts b/src/routes/dex.router.ts index ca0b6c7..7cba915 100644 --- a/src/routes/dex.router.ts +++ b/src/routes/dex.router.ts @@ -1,4 +1,5 @@ import express from 'express'; +import { getAssetsPriceRatesValidator } from '@/interfaces/bodies/dex/GetAssetsPriceRatesBody.js'; import dexController from '../controllers/dex.controller.js'; import middleware from '../middleware/middleware.js'; @@ -9,7 +10,11 @@ dexRouter.post('/dex/get-pairs-pages-amount', dexController.getPairsPagesAmount) dexRouter.post('/dex/get-pair', dexController.getPair); dexRouter.post('/dex/renew-bot', middleware.verifyToken, dexController.registerBot); dexRouter.post('/dex/volume-stats', dexController.volumeStats); -dexRouter.post('/dex/get-assets-price-rates', dexController.getAssetsPriceRates); +dexRouter.post( + '/dex/get-assets-price-rates', + middleware.expressValidator(getAssetsPriceRatesValidator), + dexController.getAssetsPriceRates, +); dexRouter.post('/dex/find-pair', dexController.findPairID); export default dexRouter; diff --git a/src/server.ts b/src/server.ts index ed5f774..4c20936 100644 --- a/src/server.ts +++ b/src/server.ts @@ -101,6 +101,8 @@ process.on('unhandledRejection', (reason, promise) => { res.send({ success: true, userData: req.body.userData }), ); + app.use(middleware.resultGlobalErrorHandler); + server.listen(PORT, () => console.log(`Server is running on port ${PORT}`)); })();