diff --git a/handlers.js b/handlers.js index adea050..f453ebd 100644 --- a/handlers.js +++ b/handlers.js @@ -1,5 +1,6 @@ import http from 'node:http'; import Response from './response.js'; +import db from './db.js'; /** * Get list of all artcles from database @@ -8,8 +9,34 @@ import Response from './response.js'; * @param {Response} res */ export const getArticlesHandler = async (req, res) => { - res.json([ - { id: 1, title: 'First artcile' }, - { id: 2, title: 'Second artcile' }, - ]); + const data = await db.client.query('SELECT * FROM "articles"'); + res.json({ data: data.rows }); +}; + +/** + * Get article by ID + * + * @param {http.ClientRequest} req + * @param {Response} res + */ +export const getArticleHandler = async (req, res) => { + /** + * Validate url `id` param + */ + if (!/^\d+$/.test(req.params.id)) { + res.json({ data: {}, error: 'Parameters is not valid!' }, 422); + return; + } + + const data = await db.client.query( + 'SELECT * from "articles" WHERE "id" = $1', + [req.params.id] + ); + + if (data.rows.length === 0) { + res.json({ data: {}, error: 'Article not found.' }, 404); + return; + } + + res.json({ data: data.rows }); }; diff --git a/package-lock.json b/package-lock.json index 8199ffb..c394ca5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -393,6 +393,18 @@ "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, + "handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -594,8 +606,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "ms": { "version": "2.1.3", @@ -603,6 +614,11 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "nodemon": { "version": "2.0.15", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz", @@ -682,6 +698,11 @@ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" }, + "path-to-regexp": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", + "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" + }, "pg": { "version": "8.7.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz", @@ -881,6 +902,11 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "split2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", @@ -960,6 +986,12 @@ "is-typedarray": "^1.0.0" } }, + "uglify-js": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.3.tgz", + "integrity": "sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==", + "optional": true + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1026,6 +1058,11 @@ "string-width": "^4.0.0" } }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 5424134..ebfd283 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ }, "dependencies": { "dotenv": "^16.0.0", + "handlebars": "^4.7.7", + "path-to-regexp": "^6.2.0", "pg": "^8.7.3" } } diff --git a/preloads/routes.js b/preloads/routes.js index 269845b..3f9b0ad 100644 --- a/preloads/routes.js +++ b/preloads/routes.js @@ -3,6 +3,7 @@ import router from '../router.js'; /** * Handlers */ -import { getArticlesHandler } from '../handlers.js'; +import { getArticleHandler, getArticlesHandler } from '../handlers.js'; router.define('/api/articles', getArticlesHandler); +router.define('/api/article/:id', getArticleHandler); diff --git a/response.js b/response.js index 20d1ccf..eef9e68 100644 --- a/response.js +++ b/response.js @@ -11,8 +11,8 @@ class Response { this.res = res; } - send(text) { - this.res.writeHead(200, { 'Content-Type': 'text/plain' }); + send(text, status = 200) { + this.res.writeHead(status, { 'Content-Type': 'text/plain' }); this.res.end(text); } @@ -21,8 +21,8 @@ class Response { this.res.end(JSON.stringify(data)); } - notFound(data) { - this.json(data, 404); + notFound(text) { + this.send(text, 404); } } diff --git a/router.js b/router.js index 56c1ebe..d93ef1c 100644 --- a/router.js +++ b/router.js @@ -1,29 +1,77 @@ import http from 'node:http'; +import { pathToRegexp } from 'path-to-regexp'; class Router { constructor() { /** - * Store the current list of routes for out API + * Store the current list of routes */ this.routes = {}; } /** + * This function checks for the existence of a route and calls its handler * * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @returns */ call(req, res) { + /** + * Find the exact route + */ if (this.routes.hasOwnProperty(req.url)) { - this.routes[req.url](req, res); + this.routes[req.url].handler(req, res); return true; } + + /** + * Find route by regular expression + */ + const keys = Object.keys(this.routes); + let i = keys.length; + + while (i--) { + const route = this.routes[keys[i]]; + if (!route.options.exact && route.options.regexp !== undefined) { + const match = req.url.match(route.options.regexp); + if (match) { + if (route.options.params !== undefined) { + req.params = {}; + route.options.params.forEach((param) => { + req.params[param.name] = match[1]; + }); + } + route.handler(req, res); + return true; + } + } + } + return false; } - define(url, handler) { - this.routes[url] = handler; + /** + * Add more routes to router store + * + * @param {string} url + * @param {function} handler + * @param {boolean} exact + */ + define( + url, + handler, + options = { exact: true, regexp: undefined, params: undefined } + ) { + if (url.includes(':')) { + options.params = []; + options.exact = false; + options.regexp = pathToRegexp(url, options.params); + } + this.routes[url] = { + handler, + options, + }; } } diff --git a/server.js b/server.js index 38e559e..71c03c2 100644 --- a/server.js +++ b/server.js @@ -47,7 +47,7 @@ class Application { /** * Undefined route */ - if (!handled) response.notFound({ data: 'Route not found!' }); + if (!handled) response.notFound('Route not found!'); return; }