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
*/
export const indexPageHandler = async (req, res) => {
try {
const info = fs.readFileSync('./package.json', 'utf8');
const data = 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 appInfo = JSON.parse(info.toString());
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
*/
export const getArticleHandler = async (req, res) => {
try {
/**
* Validate url `id` param
*/
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;
}
@ -48,11 +47,15 @@ export const getArticleHandler = async (req, res) => {
);
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;
}
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';
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;

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

View File

@ -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",

View File

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

View File

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

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;
}
}
/**
* 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;

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

View File

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

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'>
<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>
{{> head }}
<body>
Page not found.
<h1>{{ title }}</h1>
<a href="/">return to home</a>
</body>
</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'>
<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>
{{> head }}
<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>
</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>