From 23f717e6cbecf27971766c26aedc883a39e7ef43 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 17 Mar 2022 21:00:03 +0300 Subject: [PATCH] feat: rendering articles, added seeder --- handlers.js | 67 ++++++++++++++++++----------------- helpers/db.js | 26 ++++++++++++++ helpers/file.js | 7 ++++ index.js | 35 ++++++++++++++++++ initSchema.js | 16 ++------- package.json | 7 ++-- preloads/routes.js | 2 -- public/css/style.css | 44 +++++++++++++++++++++++ public/favicon.ico | Bin 0 -> 15086 bytes public/images/nginx-logo.png | Bin 0 -> 9670 bytes response.js | 18 ---------- router.js | 2 +- seed.js | 3 ++ server.js | 41 ++++++++++++++------- sql/schema_init.sql | 2 +- sql/seed.sql | 32 +++++++++++++++++ views/404.hbs | 10 ++---- views/500.hbs | 7 ++++ views/index.hbs | 43 +++++++++++++++++----- views/partials/footer.hbs | 33 +++++++++++++++++ views/partials/head.hbs | 8 +++++ 21 files changed, 304 insertions(+), 99 deletions(-) create mode 100644 helpers/db.js create mode 100644 index.js create mode 100644 public/favicon.ico create mode 100644 public/images/nginx-logo.png create mode 100644 seed.js create mode 100644 sql/seed.sql create mode 100644 views/500.hbs create mode 100644 views/partials/footer.hbs create mode 100644 views/partials/head.hbs 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 0000000000000000000000000000000000000000..0c939c7d4ba52a0a14eccc027823cbb0cdb81801 GIT binary patch literal 15086 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 { - 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