feat: rendering articles, added seeder

This commit is contained in:
Sergey Yarkov 2022-03-17 21:00:03 +03:00
parent 9fa90d956b
commit 23f717e6cb
21 changed files with 304 additions and 99 deletions

View File

@ -10,21 +10,19 @@ import db from './db.js';
* @param {Response} res * @param {Response} res
*/ */
export const indexPageHandler = async (req, res) => { export const indexPageHandler = async (req, res) => {
try {
const info = fs.readFileSync('./package.json', 'utf8'); const info = fs.readFileSync('./package.json', 'utf8');
const data = JSON.parse(info.toString()); const appInfo = JSON.parse(info.toString());
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"'); 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,11 +32,12 @@ export const getArticlesHandler = async (req, res) => {
* @param {Response} res * @param {Response} res
*/ */
export const getArticleHandler = async (req, res) => { export const getArticleHandler = async (req, res) => {
try {
/** /**
* Validate url `id` param * Validate url `id` param
*/ */
if (!/^\d+$/.test(req.params.id)) { if (!/^\d+$/.test(req.params.id)) {
res.json({ data: {}, error: 'Parameters is not valid!' }, 422); res.render('./views/404.hbs', { title: '404 | Article not found.' }, 404);
return; return;
} }
@ -48,11 +47,15 @@ export const getArticleHandler = async (req, res) => {
); );
if (data.rows.length === 0) { if (data.rows.length === 0) {
res.json({ data: {}, error: 'Article not found.' }, 404); res.render('./views/404.hbs', { title: '404 | Article not found.' }, 404);
return; return;
} }
res.json({ data: data.rows }); res.json({ data: data.rows });
} catch (error) {
console.error(error);
res.render('./views/500.hbs', { title: '500 | Internal server error ' });
}
}; };
/** /**

26
helpers/db.js Normal file
View File

@ -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;

View File

@ -1,6 +1,13 @@
import fs from 'node:fs'; import fs from 'node:fs';
class FileHelper { 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 = []) { static readDirRecursive(dir, files = []) {
const entries = fs.readdirSync(dir); const entries = fs.readdirSync(dir);
const result = files; const result = files;

35
index.js Normal file
View File

@ -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();
});

View File

@ -1,15 +1,3 @@
import fs from 'node:fs'; import DbHelper from './helpers/db.js';
import db from './db.js';
async function run() { DbHelper.runQueryFromFile('sql/schema_init.sql');
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();

View File

@ -2,12 +2,13 @@
"name": "node-docker-example", "name": "node-docker-example",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "server.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node server.js", "start": "node index.js",
"dev": "nodemon index.js -e",
"db:init": "node initSchema.js", "db:init": "node initSchema.js",
"dev": "nodemon server.js" "db:seed": "node seed.js"
}, },
"keywords": [ "keywords": [
"nodejs", "nodejs",

View File

@ -6,7 +6,6 @@ import router from '../router.js';
import { import {
createArticleHandler, createArticleHandler,
getArticleHandler, getArticleHandler,
getArticlesHandler,
indexPageHandler, indexPageHandler,
} from '../handlers.js'; } from '../handlers.js';
@ -16,6 +15,5 @@ import {
import { waitMiddleware } from '../middlewares.js'; import { waitMiddleware } from '../middlewares.js';
router.get('/', indexPageHandler); router.get('/', indexPageHandler);
router.get('/articles', getArticlesHandler).middleware([waitMiddleware]);
router.get('/articles/:id', getArticleHandler).middleware([waitMiddleware]); router.get('/articles/:id', getArticleHandler).middleware([waitMiddleware]);
router.post('/articles', createArticleHandler); router.post('/articles', createArticleHandler);

View File

@ -1,3 +1,47 @@
body { body {
font-family: monospace; 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;
} }

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -47,24 +47,6 @@ class Response {
throw error; 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; export default Response;

View File

@ -68,7 +68,7 @@ class Router {
} }
} }
res.notFound('Page not found.'); res.render('./views/404.hbs', { title: '404 | Page not found' }, 404);
} }
/** /**

3
seed.js Normal file
View File

@ -0,0 +1,3 @@
import DbHelper from './helpers/db.js';
DbHelper.runQueryFromFile('sql/seed.sql');

View File

@ -1,8 +1,12 @@
import http from 'node:http'; 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 env from './env.js';
import db from './db.js'; import db from './db.js';
import router from './router.js'; import router from './router.js';
import Response from './response.js'; import Response from './response.js';
import FileHelper from './helpers/file.js';
import './preloads/index.js'; import './preloads/index.js';
class Application { class Application {
@ -11,6 +15,7 @@ class Application {
this.server = http.createServer(); this.server = http.createServer();
this.router = router; this.router = router;
this.db = db; this.db = db;
this.hbs = hbs;
} }
async run() { async run() {
@ -37,24 +42,34 @@ class Application {
#handle() { #handle() {
this.server.on('request', (req, res) => { this.server.on('request', (req, res) => {
const response = new Response(req, res); const response = new Response(req, res);
response.serve('public');
this.router.call(req, response); this.router.call(req, response);
}); });
this.server.listen(env.app.port, () => this.server.listen(env.app.port, () =>
console.log(`[LOG]: Server is running on port "${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(); export default 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();
});

View File

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS "articles"( CREATE TABLE IF NOT EXISTS "articles"(
"id" INTEGER PRIMARY KEY NOT NULL, "id" INTEGER PRIMARY KEY NOT NULL,
"title" VARCHAR(255) NOT NULL, "title" VARCHAR(255) NOT NULL,
"description" VARCHAR(255) NOT NULL, "description" VARCHAR(1000),
"body" TEXT NOT NULL, "body" TEXT NOT NULL,
"created_at" TIMESTAMP(0) WITH TIME zone NOT NULL, "created_at" TIMESTAMP(0) WITH TIME zone NOT NULL,
"updated_at" TIMESTAMP(0) WITH TIME zone NOT NULL "updated_at" TIMESTAMP(0) WITH TIME zone NOT NULL

32
sql/seed.sql Normal file
View File

@ -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'
)

View File

@ -1,11 +1,7 @@
<html lang='en'> <html lang='en'>
<head> {{> head }}
<meta charset='UTF-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>404 | Not found</title>
</head>
<body> <body>
Page not found. <h1>{{ title }}</h1>
<a href="/">return to home</a>
</body> </body>
</html> </html>

7
views/500.hbs Normal file
View File

@ -0,0 +1,7 @@
<html lang='en'>
{{> head }}
<body>
<h1>{{ title }}</h1>
<a href="/">return to home</a>
</body>
</html>

View File

@ -1,12 +1,39 @@
<html lang='en'> <html lang='en'>
<head> {{> head }}
<meta charset='UTF-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{{app.name}}</title>
<link rel='stylesheet' href='public/css/style.css' />
</head>
<body> <body>
Index Page. <main class='app'>
<div class='app-heading'>
<h1>Articles (5)</h1>
<small>{{title}}</small>
</div>
<div class='app-description'>
<p>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.</p>
</div>
<hr />
<div class="app-body">
{{#unless articles.length }}
<p style="text-align: center;margin: 30px 0;">
No articles found.
</p>
{{/unless}}
{{#each articles}}
<article>
<h2>
<a href='articles/{{this.id}}'>
{{ this.title }}
</a>
</h2>
<p>{{ this.description }}</p>
<time>{{ this.created_at }}</time>
</article>
<hr />
{{/each}}
</div>
{{> footer }}
</main>
</body> </body>
</html> </html>

33
views/partials/footer.hbs Normal file
View File

@ -0,0 +1,33 @@
<div class='app-footer'>
<span>Used technologies:</span>
<div class='app-footer__images'>
<a href='https://www.docker.com' target='_blank'>
<img
class='docker-logo'
src='/public/images/docker-logo.png'
alt='Docker Logo'
/>
</a>
<a href='https://nodejs.org' target='_blank'>
<img
class='nodejs-logo'
src='/public/images/nodejs-logo.png'
alt='NodeJS Logo'
/>
</a>
<a href='https://nginx.org' target='_blank'>
<img
class='nginx-logo'
src='/public/images/nginx-logo.png'
alt='NodeJS Logo'
/>
</a>
</div>
<div class='app-footer__description'>
<a
href='https://github.com/sergeyyarkov/node-docker-example'
target='_blank'
>GitHub</a>
</div>
</div>

8
views/partials/head.hbs Normal file
View File

@ -0,0 +1,8 @@
<head>
<meta charset='UTF-8' />
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>{{title}}</title>
<link rel='stylesheet' href='/public/css/style.css' />
<link rel='icon' type='image/x-icon' href='/public/favicon.ico' />
</head>