mirror of
https://github.com/sergeyyarkov/node-docker-example.git
synced 2026-10-11 18:29:34 +03:00
feat: rendering articles, added seeder
This commit is contained in:
parent
9fa90d956b
commit
23f717e6cb
33
handlers.js
33
handlers.js
@ -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
26
helpers/db.js
Normal 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;
|
||||
@ -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
35
index.js
Normal 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();
|
||||
});
|
||||
@ -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');
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/nginx-logo.png
Normal file
BIN
public/images/nginx-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
18
response.js
18
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;
|
||||
|
||||
@ -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
3
seed.js
Normal file
@ -0,0 +1,3 @@
|
||||
import DbHelper from './helpers/db.js';
|
||||
|
||||
DbHelper.runQueryFromFile('sql/seed.sql');
|
||||
41
server.js
41
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;
|
||||
|
||||
@ -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
32
sql/seed.sql
Normal 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'
|
||||
)
|
||||
@ -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
7
views/500.hbs
Normal file
@ -0,0 +1,7 @@
|
||||
<html lang='en'>
|
||||
{{> head }}
|
||||
<body>
|
||||
<h1>{{ title }}</h1>
|
||||
<a href="/">return to home</a>
|
||||
</body>
|
||||
</html>
|
||||
@ -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
33
views/partials/footer.hbs
Normal 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
8
views/partials/head.hbs
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user