diff --git a/controllers/ArticleController.js b/controllers/ArticleController.js new file mode 100644 index 0000000..98ce530 --- /dev/null +++ b/controllers/ArticleController.js @@ -0,0 +1,92 @@ +import http from 'node:http'; +import fs from 'node:fs'; +import Response from '#app/Response'; + +/** + * Models + */ +import ArticleModel from '#models/ArticleModel'; + +class ArticleController { + /** + * Render page with list of articles + * + * @param {http.ClientRequest} req + * @param {Response} res + */ + static async index(req, res) { + try { + const info = fs.readFileSync('./package.json', 'utf8'); + const appInfo = JSON.parse(info.toString()); + const articles = await ArticleModel.getAll(); + + res.render('./views/index.hbs', { + title: appInfo.name, + articles, + }); + } catch (error) { + res.serverError(error); + } + } + + /** + * Render page with single article by `id` + * + * @param {http.ClientRequest} req + * @param {Response} res + */ + static async show(req, res) { + try { + /** + * Validate url `id` param + */ + const id = req.params.id; + if (!/^\d+$/.test(id)) { + res.render( + './views/404.hbs', + { title: '404 | Article not found.' }, + 404 + ); + return; + } + + const article = await ArticleModel.getById(id); + + /** + * Not found + */ + if (!article) { + res.render( + './views/404.hbs', + { title: '404 | Article not found.' }, + 404 + ); + return; + } + + res.render('./views/article.hbs', { + title: article.title, + article, + }); + } catch (error) { + res.serverError(error); + } + } + + /** + * Create new article + * + * @param {http.ClientRequest} req + * @param {Response} res + */ + static async create(req, res) { + try { + res.json({ data: `POST ${req.url}` }); + return; + } catch (error) { + res.serverError(error); + } + } +} + +export default ArticleController; diff --git a/handlers.js b/handlers.js deleted file mode 100644 index 0a354bf..0000000 --- a/handlers.js +++ /dev/null @@ -1,75 +0,0 @@ -import http from 'node:http'; -import fs from 'node:fs'; -import Response from './response.js'; -import db from './db.js'; - -/** - * Render index page - * - * @param {http.ClientRequest} req - * @param {Response} res - */ -export const indexPageHandler = async (req, res) => { - try { - const info = fs.readFileSync('./package.json', 'utf8'); - const appInfo = JSON.parse(info.toString()); - const data = await db.client.query('SELECT * FROM "articles"'); - - res.render('./views/index.hbs', { - title: appInfo.name, - articles: data.rows, - }); - } catch (error) { - res.serverError(error); - } -}; - -/** - * GET - Get article by ID - * - * @param {http.ClientRequest} req - * @param {Response} res - */ -export const getArticleHandler = async (req, res) => { - try { - /** - * Validate url `id` param - */ - if (!/^\d+$/.test(req.params.id)) { - res.render('./views/404.hbs', { title: '404 | Article not found.' }, 404); - return; - } - - const data = await db.client.query( - 'SELECT * from "articles" WHERE "id" = $1', - [req.params.id] - ); - - if (data.rows.length === 0) { - res.render('./views/404.hbs', { title: '404 | Article not found.' }, 404); - return; - } - - res.render('./views/article.hbs', { - title: data.rows[0].title, - article: data.rows[0], - }); - } catch (error) { - res.serverError(error); - } -}; - -/** - * POST - Create article - * - * @param {http.RequestOptions} req - * @param {Response} res - */ -export const createArticleHandler = async (req, res) => { - try { - res.json({ data: `POST ${req.url}` }); - return; - } catch (error) { - res.serverError(error); - } -}; diff --git a/index.js b/index.js index 594e1c6..081e40d 100644 --- a/index.js +++ b/index.js @@ -1,28 +1,9 @@ -import fs from 'node:fs'; -import path from 'node:path'; import Application from './server.js'; -import FileHelper from './helpers/file.js'; const app = new Application(); app.serve('public'); - -{ - const partials = FileHelper.readDirRecursive( - process.cwd() + '/views/partials' - ); - - /** - * Register partials from files - */ - for (const partial of partials) { - const fileName = path.basename(partial); - app.hbs.registerPartial( - path.parse(fileName).name, - fs.readFileSync(partial, 'utf8').toString() - ); - } -} +app.partials(process.cwd() + '/views/partials'); app.run().catch((error) => { console.error('[ERR]: Error on startup the application.', error); diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..dd72512 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "#app/*": ["./*"], + "#controllers/*": ["./controllers/*"], + "#models/*": ["./models/*"], + "#middlewares/*": ["./middlewares/*"], + "#helpers/*": ["./helpers/*"] + } + } +} diff --git a/middlewares.js b/middlewares.js deleted file mode 100644 index aabeb7a..0000000 --- a/middlewares.js +++ /dev/null @@ -1,19 +0,0 @@ -import http from 'node:http'; -import Response from './response.js'; - -/** - * This middleware waits for a few milliseconds - * - * @param {http.ClientRequest} req - * @param {Response} res - * @param {function} next - */ -export const waitMiddleware = async (req, res) => { - const wait = (ms = 1000) => { - return new Promise((res) => { - setTimeout(() => res(ms), ms); - }); - }; - - await wait(10); -}; diff --git a/middlewares/WaitMiddleware.js b/middlewares/WaitMiddleware.js new file mode 100644 index 0000000..dc896d2 --- /dev/null +++ b/middlewares/WaitMiddleware.js @@ -0,0 +1,13 @@ +class WaitMiddleware { + static async handle() { + const wait = (ms = 1000) => { + return new Promise((res) => { + setTimeout(() => res(ms), ms); + }); + }; + + await wait(10); + } +} + +export default WaitMiddleware; diff --git a/models/ArticleModel.js b/models/ArticleModel.js new file mode 100644 index 0000000..4ce5f55 --- /dev/null +++ b/models/ArticleModel.js @@ -0,0 +1,78 @@ +import db from '#app/db'; + +/** + * ArticleModel typedef + * @typedef {Object} Article + * @property {number} id + * @property {string} title + * @property {string} description + * @property {string} body + * @property {Date} created_at + * @property {Date} updated_at + */ + +class ArticleModel { + /** + * Get list of articles + * @returns {Promise>} List of articles + */ + static async getAll() { + try { + const data = await db.client.query('SELECT * FROM "articles"'); + return data.rows; + } catch (error) { + throw error; + } + } + + /** + * Get article by `id` + * + * @param {string} id Id of article + * @returns {Promise
} Founded article + */ + static async getById(id) { + try { + const data = await db.client.query( + 'SELECT * from "articles" WHERE "id" = $1', + [id] + ); + + if (data.rows.length === 0) return null; + + return data.rows[0]; + } catch (error) { + throw error; + } + } + + /** + * Create new article in database + * + * @param {{ title: string, description: string, body: string }} data Data for creating + * @returns {Promise
} Created article + */ + static async create(data) { + try { + const count = await db.client.query('SELECT COUNT(*) FROM "articles"'); + const values = Object.values({ + id: String(parseInt(count.rows[0].count, 10) + 1), + title: data.title, + description: data.description, + body: data.body, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }); + const article = await db.client.query( + `INSERT INTO "articles" ("id", "title", "description", "body", "created_at", "updated_at") VALUES($1, $2, $3, $4, $5, $6)`, + values + ); + + return article; + } catch (error) { + throw error; + } + } +} + +export default ArticleModel; diff --git a/package.json b/package.json index 0ee8ee1..a0bfe8b 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,12 @@ "mime-types": "^2.1.35", "path-to-regexp": "^6.2.0", "pg": "^8.7.3" + }, + "imports": { + "#app/*": "./*.js", + "#controllers/*": "./controllers/*.js", + "#models/*": "./models/*.js", + "#middlewares/*": "./middlewares/*.js", + "#helpers/*": "./helpers/*.js" } } diff --git a/preloads/routes.js b/preloads/routes.js index f47826b..6d6e518 100644 --- a/preloads/routes.js +++ b/preloads/routes.js @@ -1,19 +1,18 @@ -import router from '../router.js'; +/** + * Router + */ +import router from '#app/router'; /** - * Handlers + * Controllers */ -import { - createArticleHandler, - getArticleHandler, - indexPageHandler, -} from '../handlers.js'; +import ArticleController from '#controllers/ArticleController'; /** * Middlewares */ -import { waitMiddleware } from '../middlewares.js'; +import WaitMiddleware from '#middlewares/WaitMiddleware'; -router.get('/', indexPageHandler).middleware([waitMiddleware]); -router.get('/articles/:id', getArticleHandler).middleware([waitMiddleware]); -router.post('/articles', createArticleHandler); +router.get('/', ArticleController.index).middleware([WaitMiddleware.handle]); +router.get('/articles/:id', ArticleController.show); +router.post('/articles', ArticleController.create); diff --git a/response.js b/response.js index 751471c..42830a5 100644 --- a/response.js +++ b/response.js @@ -1,9 +1,6 @@ import http from 'node:http'; import fs from 'node:fs'; import hbs from 'handlebars'; -import mime from 'mime-types'; -import router from './router.js'; -import FileHelper from './helpers/file.js'; class Response { /** diff --git a/server.js b/server.js index 40bff72..6d4230b 100644 --- a/server.js +++ b/server.js @@ -1,13 +1,14 @@ import http from 'node:http'; import fs from 'node:fs'; import qs from 'node:querystring'; +import path from 'node:path'; import mime from 'mime-types'; import hbs from 'handlebars'; -import env from './env.js'; -import db from './db.js'; -import router from './router.js'; -import Response from './response.js'; -import FileHelper from './helpers/file.js'; +import env from '#app/env'; +import db from '#app/db'; +import router from '#app/router'; +import Response from '#app/response'; +import FileHelper from '#helpers/file'; import './preloads/index.js'; class Application { @@ -41,9 +42,19 @@ class Application { } #handle() { - this.server.on('request', (req, res) => { + this.server.on('request', async (req, res) => { const response = new Response(req, res); this.router.call(req, response); + + // const buffer = Buffer.alloc(parseInt(req.headers['content-length'], 10)); + + // req.on('data', (chunk) => { + // if (req.method === 'POST') { + // console.log(chunk); + // // console.log(buffer.toString()); + // // const buffer = Buffer.alloc(chunk.length) + // } + // }); }); this.server.listen(env.app.port, () => console.log(`[LOG]: Server is running on port "${env.app.port}"`) @@ -53,10 +64,10 @@ class Application { /** * Serve folder with static files * - * @param {string} path Path to static files + * @param {string} folder Path to static files */ - serve(path) { - const files = FileHelper.readDirRecursive(path); + serve(folder) { + const files = FileHelper.readDirRecursive(folder); /** * At the moment, this process is called only once. * It would be nice to watch for directory changes and declare new routes again @@ -71,6 +82,26 @@ class Application { }); } } + + /** + * Register handlebars partials + * + * @param {string} folder Path to `partials` folder + */ + partials(folder) { + const partials = FileHelper.readDirRecursive(folder); + + /** + * Register partials from files + */ + for (const partial of partials) { + const fileName = path.basename(partial); + this.hbs.registerPartial( + path.parse(fileName).name, + fs.readFileSync(partial, 'utf8').toString() + ); + } + } } export default Application;