diff --git a/handlers.js b/handlers.js index eaa70fd..2fa5ea0 100644 --- a/handlers.js +++ b/handlers.js @@ -10,21 +10,19 @@ import db from './db.js'; * @param {Response} res */ export const indexPageHandler = async (req, res) => { - const info = fs.readFileSync('./package.json', 'utf8'); - const data = JSON.parse(info.toString()); + 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', { app: { name: data.name } }); -}; - -/** - * GET - Get list of all artcles from database - * - * @param {http.ClientRequest} req - * @param {Response} res - */ -export const getArticlesHandler = async (req, res) => { - const data = await db.client.query('SELECT * FROM "articles"'); - res.json({ data: data.rows }); + res.render('./views/index.hbs', { + title: appInfo.name, + articles: data.rows, + }); + } catch (error) { + console.error(error); + res.render('./views/500.hbs', { title: '500 | Internal server error ' }); + } }; /** @@ -34,25 +32,30 @@ export const getArticlesHandler = async (req, res) => { * @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; + 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.json({ data: data.rows }); + } catch (error) { + console.error(error); + res.render('./views/500.hbs', { title: '500 | Internal server error ' }); } - - 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/helpers/db.js b/helpers/db.js new file mode 100644 index 0000000..9f53e5b --- /dev/null +++ b/helpers/db.js @@ -0,0 +1,26 @@ +import fs from 'node:fs'; +import db from '../db.js'; + +class DbHelper { + /** + * Run query from `sql` file and close connection + * + * @param {string} path Path to `.sql` file + */ + static async runQueryFromFile(path) { + try { + const { client } = db; + client.connect(); + + const query = fs.readFileSync(path).toString(); + + await client.query(query); + console.log(`[LOG]: SQL: ${query}`); + await client.end(); + } catch (error) { + console.error(error); + } + } +} + +export default DbHelper; diff --git a/helpers/file.js b/helpers/file.js index 61d6126..4de5924 100644 --- a/helpers/file.js +++ b/helpers/file.js @@ -1,6 +1,13 @@ import fs from 'node:fs'; class FileHelper { + /** + * Get a list of paths of files + * + * @param {string} dir Path to folder + * @param {[string]} files List of paths of files + * @returns + */ static readDirRecursive(dir, files = []) { const entries = fs.readdirSync(dir); const result = files; diff --git a/index.js b/index.js new file mode 100644 index 0000000..594e1c6 --- /dev/null +++ b/index.js @@ -0,0 +1,35 @@ +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.run().catch((error) => { + console.error('[ERR]: Error on startup the application.', error); +}); + +process.on('SIGINT', () => { + console.log(`[LOG]: Graceefully stopping the server...`); + app.server.close(); + process.exit(); +}); diff --git a/initSchema.js b/initSchema.js index 2677a70..816c3ae 100644 --- a/initSchema.js +++ b/initSchema.js @@ -1,15 +1,3 @@ -import fs from 'node:fs'; -import db from './db.js'; +import DbHelper from './helpers/db.js'; -async function run() { - const { client } = db; - client.connect(); - - const query = fs.readFileSync('sql/schema_init.sql').toString(); - - await client.query(query); - console.log(`SQL: ${query}`); - await client.end(); -} - -await run(); +DbHelper.runQueryFromFile('sql/schema_init.sql'); diff --git a/package.json b/package.json index 8c60b8f..6c86f64 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,13 @@ "name": "node-docker-example", "version": "1.0.0", "description": "", - "main": "server.js", + "main": "index.js", "type": "module", "scripts": { - "start": "node server.js", + "start": "node index.js", + "dev": "nodemon index.js -e", "db:init": "node initSchema.js", - "dev": "nodemon server.js" + "db:seed": "node seed.js" }, "keywords": [ "nodejs", diff --git a/preloads/routes.js b/preloads/routes.js index 939b337..81828b9 100644 --- a/preloads/routes.js +++ b/preloads/routes.js @@ -6,7 +6,6 @@ import router from '../router.js'; import { createArticleHandler, getArticleHandler, - getArticlesHandler, indexPageHandler, } from '../handlers.js'; @@ -16,6 +15,5 @@ import { import { waitMiddleware } from '../middlewares.js'; router.get('/', indexPageHandler); -router.get('/articles', getArticlesHandler).middleware([waitMiddleware]); router.get('/articles/:id', getArticleHandler).middleware([waitMiddleware]); router.post('/articles', createArticleHandler); diff --git a/public/css/style.css b/public/css/style.css index 2206be4..735c381 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,3 +1,47 @@ body { font-family: monospace; + text-align: justify; + margin: 75px; + max-width: 600px; +} + +.app .app-heading { + display: flex; + justify-content: space-between; + align-items: center; +} + +.app .app-body article { + margin-top: 2rem; +} + +.app .app-footer { + text-align: center; +} + +.app .app-footer .app-footer__images { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} + +.app .app-footer { + margin-top: 2rem; + text-align: left; +} + +.app .app-footer img { + width: 11rem; + margin: 0 30px; +} + +.app .app-footer .nodejs-logo, +.app .app-footer .nginx-logo { + width: 7rem; +} + +.app .app-footer__description { + margin-top: 1rem; + text-align: center; } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..0c939c7 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/nginx-logo.png b/public/images/nginx-logo.png new file mode 100644 index 0000000..1069dfd Binary files /dev/null and b/public/images/nginx-logo.png differ diff --git a/response.js b/response.js index 8565665..dc1494b 100644 --- a/response.js +++ b/response.js @@ -47,24 +47,6 @@ class Response { throw error; } } - - /** - * Response with static files - * - * @param {string} path Path to static files - */ - serve(path) { - const files = FileHelper.readDirRecursive(path); - for (const file of files) { - router.define('GET', `/${file}`, (req, { res }) => { - const data = fs.readFileSync(file); - const contentType = mime.lookup(file); - - res.writeHead(200, { 'Content-Type': contentType }); - res.end(data); - }); - } - } } export default Response; diff --git a/router.js b/router.js index a6f6613..ae1ab16 100644 --- a/router.js +++ b/router.js @@ -68,7 +68,7 @@ class Router { } } - res.notFound('Page not found.'); + res.render('./views/404.hbs', { title: '404 | Page not found' }, 404); } /** diff --git a/seed.js b/seed.js new file mode 100644 index 0000000..5b275cd --- /dev/null +++ b/seed.js @@ -0,0 +1,3 @@ +import DbHelper from './helpers/db.js'; + +DbHelper.runQueryFromFile('sql/seed.sql'); diff --git a/server.js b/server.js index f168f19..0d18be6 100644 --- a/server.js +++ b/server.js @@ -1,8 +1,12 @@ import http from 'node:http'; +import fs from 'node:fs'; +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 './preloads/index.js'; class Application { @@ -11,6 +15,7 @@ class Application { this.server = http.createServer(); this.router = router; this.db = db; + this.hbs = hbs; } async run() { @@ -37,24 +42,34 @@ class Application { #handle() { this.server.on('request', (req, res) => { const response = new Response(req, res); - - response.serve('public'); this.router.call(req, response); }); this.server.listen(env.app.port, () => console.log(`[LOG]: Server is running on port "${env.app.port}"`) ); } + + /** + * Serve folder with static files + * + * @param {string} path Path to static files + */ + serve(path) { + const files = FileHelper.readDirRecursive(path); + /** + * At the moment, this process is called only once. + * It would be nice to watch for directory changes and declare new routes again + */ + for (const file of files) { + router.define('GET', `/${file}`, (req, { res }) => { + const data = fs.readFileSync(file); + const contentType = mime.lookup(file); + + res.writeHead(200, { 'Content-Type': contentType }); + res.end(data); + }); + } + } } -const app = new Application(); - -app.run().catch((error) => { - console.error('[ERR]: Error on startup the application.', error); -}); - -process.on('SIGINT', () => { - console.log(`[LOG]: Graceefully stopping the server...`); - app.server.close(); - process.exit(); -}); +export default Application; diff --git a/sql/schema_init.sql b/sql/schema_init.sql index 2e31506..c25f039 100644 --- a/sql/schema_init.sql +++ b/sql/schema_init.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS "articles"( "id" INTEGER PRIMARY KEY NOT NULL, "title" VARCHAR(255) NOT NULL, - "description" VARCHAR(255) NOT NULL, + "description" VARCHAR(1000), "body" TEXT NOT NULL, "created_at" TIMESTAMP(0) WITH TIME zone NOT NULL, "updated_at" TIMESTAMP(0) WITH TIME zone NOT NULL diff --git a/sql/seed.sql b/sql/seed.sql new file mode 100644 index 0000000..3012b52 --- /dev/null +++ b/sql/seed.sql @@ -0,0 +1,32 @@ +INSERT INTO "articles" ( + "id", + "title", + "description", + "body", + "created_at", + "updated_at" +) VALUES + ( + '1', + 'First article', + 'Lorem ipsum dolor, sit amet consectetur adipisicing elit. Officia dolorum quae voluptas odio quisquam nihil tempora quis accusamus reprehenderit qui.', + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam, cupiditate consequatur odit est voluptas praesentium minima? Obcaecati itaque exercitationem magnam maiores dolores, quae voluptate facere repellendus minus quas rem corporis. Cumque quidem repellendus voluptatem nostrum laudantium voluptates accusamus error exercitationem dolorem id tempora suscipit reiciendis, odit aliquid libero possimus sit similique quaerat unde. Ullam, iusto dicta magni dolore minima molestias, repellendus mollitia reiciendis praesentium illo corporis ipsam provident aut, corrupti pariatur nulla. Iste debitis perspiciatis itaque! Sed at ab dolores? Dolorum voluptate possimus deserunt sit libero similique doloremque? Mollitia esse nam obcaecati repellendus quaerat iure tempore fugiat. Id, eligendi dolorem!', + '2022-03-17', + '2022-03-17' + ), + ( + '2', + 'Second article', + 'Lorem ipsum dolor, sit amet consectetur adipisicing elit. Officia dolorum quae voluptas odio quisquam nihil tempora quis accusamus reprehenderit qui.', + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam, cupiditate consequatur odit est voluptas praesentium minima? Obcaecati itaque exercitationem magnam maiores dolores, quae voluptate facere repellendus minus quas rem corporis. Cumque quidem repellendus voluptatem nostrum laudantium voluptates accusamus error exercitationem dolorem id tempora suscipit reiciendis, odit aliquid libero possimus sit similique quaerat unde. Ullam, iusto dicta magni dolore minima molestias, repellendus mollitia reiciendis praesentium illo corporis ipsam provident aut, corrupti pariatur nulla. Iste debitis perspiciatis itaque! Sed at ab dolores? Dolorum voluptate possimus deserunt sit libero similique doloremque? Mollitia esse nam obcaecati repellendus quaerat iure tempore fugiat. Id, eligendi dolorem!', + '2022-03-17', + '2022-03-17' + ), + ( + '3', + 'Third article', + 'Lorem ipsum dolor, sit amet consectetur adipisicing elit. Officia dolorum quae voluptas odio quisquam nihil tempora quis accusamus reprehenderit qui.', + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Laboriosam, cupiditate consequatur odit est voluptas praesentium minima? Obcaecati itaque exercitationem magnam maiores dolores, quae voluptate facere repellendus minus quas rem corporis. Cumque quidem repellendus voluptatem nostrum laudantium voluptates accusamus error exercitationem dolorem id tempora suscipit reiciendis, odit aliquid libero possimus sit similique quaerat unde. Ullam, iusto dicta magni dolore minima molestias, repellendus mollitia reiciendis praesentium illo corporis ipsam provident aut, corrupti pariatur nulla. Iste debitis perspiciatis itaque! Sed at ab dolores? Dolorum voluptate possimus deserunt sit libero similique doloremque? Mollitia esse nam obcaecati repellendus quaerat iure tempore fugiat. Id, eligendi dolorem!', + '2022-03-17', + '2022-03-17' + ) diff --git a/views/404.hbs b/views/404.hbs index 94604b2..d474659 100644 --- a/views/404.hbs +++ b/views/404.hbs @@ -1,11 +1,7 @@ - - - - - 404 | Not found - + {{> head }} - Page not found. +

{{ title }}

+ return to home \ No newline at end of file diff --git a/views/500.hbs b/views/500.hbs new file mode 100644 index 0000000..d474659 --- /dev/null +++ b/views/500.hbs @@ -0,0 +1,7 @@ + + {{> head }} + +

{{ title }}

+ return to home + + \ No newline at end of file diff --git a/views/index.hbs b/views/index.hbs index 12c5e84..7ac3183 100644 --- a/views/index.hbs +++ b/views/index.hbs @@ -1,12 +1,39 @@ - - - - - {{app.name}} - - + {{> head }} - Index Page. +
+
+

Articles (5)

+ {{title}} +
+
+

This app showcases a website built with NodeJS. The site has a set of + articles, each article has a separate page, all data about the article + is stored in the PostgreSQL database. Docker is used to deploy the + application.

+
+ +
+
+ {{#unless articles.length }} +

+ No articles found. +

+ {{/unless}} + {{#each articles}} + +
+ {{/each}} +
+ {{> footer }} +
\ No newline at end of file diff --git a/views/partials/footer.hbs b/views/partials/footer.hbs new file mode 100644 index 0000000..7f2b1d7 --- /dev/null +++ b/views/partials/footer.hbs @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/views/partials/head.hbs b/views/partials/head.hbs new file mode 100644 index 0000000..4f38817 --- /dev/null +++ b/views/partials/head.hbs @@ -0,0 +1,8 @@ + + + + + {{title}} + + + \ No newline at end of file