Skip to content

Commit

Permalink
Add basic api and static server
Browse files Browse the repository at this point in the history
  • Loading branch information
DemianParkhomenko committed Sep 23, 2023
1 parent 482c3e4 commit 458da3e
Show file tree
Hide file tree
Showing 26 changed files with 1,664 additions and 42 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"env": {
"es2021": true,
"node": true
"node": true,
"browser": true
},
"extends": "eslint:recommended",
"parserOptions": {
Expand Down
12 changes: 12 additions & 0 deletions api/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"parserOptions": {
"sourceType": "module"
},
"rules": {
"strict": ["error", "never"]
},
"globals": {
"db": "readonly",
"common": "readonly"
}
}
1 change: 1 addition & 0 deletions api/city.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = ({ db }) => db('city')
14 changes: 14 additions & 0 deletions api/country.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = ({ db }) => {
const country = db('country')

return {
async read(id) {
return await country.read(id)
},

async find(mask) {
const sql = 'SELECT * from country where name like $1'
return await country.query(sql, [mask])
},
}
}
28 changes: 28 additions & 0 deletions api/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports = ({ db, common }) => {
const user = db('users')

return {
async read(id) {
return await user.read(id, ['id', 'login'])
},

async create({ login, password }) {
const passwordHash = await common.hash(password)
return await user.create({ login, password: passwordHash })
},

async update(id, { login, password }) {
const passwordHash = await common.hash(password)
return await user.update(id, { login, password: passwordHash })
},

async delete(id) {
return await user.delete(id)
},

async find(mask) {
const sql = 'SELECT login from users where login like $1'
return await user.query(sql, [mask])
},
}
}
20 changes: 20 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
db: {
port: 5432,
database: 'example',
user: 'marcus',
password: 'marcus',
},
api: {
port: 8001,
transport: 'ws',
},
static: {
port: 8000,
},
load: {
timeout: 5000,
displayErrors: false,
},
logger: { name: 'pino', fsPath: './log', console: true },
}
53 changes: 53 additions & 0 deletions db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict'

const pg = require('pg')

const crud = (pool) => (table) => ({
async query(sql, args) {
return await pool.query(sql, args)
},

async read(id, fields = ['*']) {
const names = fields.join(', ')
const sql = `SELECT ${names} FROM ${table}`
if (!id) return pool.query(sql)
return await pool.query(`${sql} WHERE id = $1`, [id])
},

async create({ ...record }) {
const keys = Object.keys(record)
const nums = new Array(keys.length)
const data = new Array(keys.length)
let i = 0
for (const key of keys) {
data[i] = record[key]
nums[i] = `$${++i}`
}
const fields = '"' + keys.join('", "') + '"'
const params = nums.join(', ')
const sql = `INSERT INTO "${table}" (${fields}) VALUES (${params})`
return await pool.query(sql, data)
},

async update(id, { ...record }) {
const keys = Object.keys(record)
const updates = new Array(keys.length)
const data = new Array(keys.length)
let i = 0
for (const key of keys) {
data[i] = record[key]
updates[i] = `${key} = $${++i}`
}
const delta = updates.join(', ')
const sql = `UPDATE ${table} SET ${delta} WHERE id = $${++i}`
data.push(id)
return await pool.query(sql, data)
},

async delete(id) {
const sql = `DELETE FROM ${table} WHERE id = $1`
return await pool.query(sql, [id])
},
})

module.exports = (config) => crud(new pg.Pool(config))
25 changes: 25 additions & 0 deletions db/data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
INSERT INTO "users" ("login", "password") VALUES
('admin', 'ypMEd9FwvtlOjcvH94iICQ==:V6LnSOVwXzENxeLCJk59Toadea7oaA1IxYulAOtKkL9tBxjEPOw085vYalEdLDoe8xbrXQlhh7QRGzrSe8Bthw=='),
('marcus', 'dpHw0OUNBz76nuqrXZbeYQ==:wpvUVgi8Yp9rJ0yZyBWecaWP2EL/ahpxZY74KOVfhAYbAZSq6mWqjsQwtCvIPcSKZqUVpVb13JcSciB2fA+6Tg=='),
('user', 'r8zb8AdrlPSh5wNy6hqOxg==:HyO5rvOFLtwzU+OZ9qFi3ADXlVccDJWGSfUS8mVq43spJ6sxyliUdW3i53hOPdkFAtDn3EAQMttOlIoJap1lTQ=='),
('iskandar', 'aqX1O4bKXiwC/Jh2EKNIYw==:bpE4TARNg09vb2Libn1c00YRxcvoklB9zVSbD733LwQQFUuAm7WHP85PbZXwEbbeOVPIFHgflR4cvEmvYkr76g==');

-- Examples login/password
-- admin/123456
-- marcus/marcus
-- user/nopassword
-- iskandar/zulqarnayn

INSERT INTO "country" ("name") VALUES
('Soviet Union'),
('People''s Republic of China'),
('Vietnam'),
('Cuba');

INSERT INTO "city" ("name", "country") VALUES
('Beijing', 2),
('Wuhan', 2),
('Kiev', 1),
('Havana', 4),
('Hanoi', 3),
('Kaliningrad', 1);
5 changes: 5 additions & 0 deletions db/install.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DROP DATABASE IF EXISTS example;
DROP USER IF EXISTS marcus;
CREATE USER marcus WITH PASSWORD 'marcus';
CREATE DATABASE example OWNER marcus;

3 changes: 3 additions & 0 deletions db/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
psql -f install.sql -U postgres
PGPASSWORD=marcus psql -d example -f structure.sql -U marcus
PGPASSWORD=marcus psql -d example -f data.sql -U marcus
41 changes: 41 additions & 0 deletions db/structure.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
CREATE TABLE "users" (
"id" bigint generated always as identity,
"login" varchar NOT NULL,
"password" varchar NOT NULL
);

ALTER TABLE "users" ADD CONSTRAINT pkUsers PRIMARY KEY (id);
CREATE UNIQUE INDEX akUsersLogin ON "users" (login);

CREATE TABLE "session" (
"id" bigint generated always as identity,
"user" integer NOT NULL,
"token" varchar(64) NOT NULL,
"ip" varchar(45) NOT NULL,
"data" text
);

ALTER TABLE "session" ADD CONSTRAINT pkSession PRIMARY KEY (id);
CREATE UNIQUE INDEX akSession ON "session" (token);
ALTER TABLE "session" ADD CONSTRAINT fkSessionUserId FOREIGN KEY ("user") REFERENCES "users" (id) ON DELETE CASCADE;

CREATE TABLE "country" (
"id" bigint generated always as identity,
"name" varchar NOT NULL
);

ALTER TABLE "country" ADD CONSTRAINT "pkCountry" PRIMARY KEY ("id");

CREATE UNIQUE INDEX "akCountry" ON "country" ("name");

CREATE TABLE "city" (
"id" bigint generated always as identity,
"name" varchar NOT NULL,
"country" bigint NOT NULL
);

ALTER TABLE "city" ADD CONSTRAINT "pkCity" PRIMARY KEY ("id");

CREATE UNIQUE INDEX "akCity" ON "city" ("name");

ALTER TABLE "city" ADD CONSTRAINT "fkCityCountry" FOREIGN KEY ("country") REFERENCES "country" ("id") ON DELETE CASCADE;
14 changes: 14 additions & 0 deletions hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'

const crypto = require('node:crypto')

const hash = (password) =>
new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16).toString('base64')
crypto.scrypt(password, salt, 64, (err, result) => {
if (err) reject(err)
resolve(salt + ':' + result.toString('base64'))
})
})

module.exports = hash
12 changes: 0 additions & 12 deletions index.js

This file was deleted.

40 changes: 40 additions & 0 deletions logger/pino.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const pino = require('pino')

module.exports = (config) => {
const targets = []
if (config.console) {
targets.push({
level: 'trace',
target: 'pino-pretty',
})
}
if (config.fsPath) {
targets.push({
level: 'trace',
target: 'pino/file',
options: {
destination: `${config.fsPath}/${config.name}/combined.log`,
},
})
}
const transport = pino.transport({
targets,
})
const logger = pino(transport)

return {
log(...args) {
// TODO: issue https://github.com/pinojs/pino-pretty/issues/455
logger.info(...args)
},
info(...args) {
logger.info(...args)
},
warn(...args) {
logger.warn(...args)
},
error(...args) {
logger.error(...args)
},
}
}
42 changes: 42 additions & 0 deletions logger/winston.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const winston = require('winston')

module.exports = (config) => {
const transports = []
if (config.console) {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
})
)
}
if (config.fsPath) {
transports.push(
new winston.transports.File({
filename: `${config.fsPath}/${config.name}/combined.log`,
})
)
}
const logger = winston.createLogger({
format: winston.format.json(),
level: 'verbose',
transports,
})

return {
log(...args) {
logger.verbose(...args)
},
info(...args) {
logger.info(...args)
},
warn(...args) {
logger.warn(...args)
},
error(...args) {
logger.error(...args)
},
}
}
31 changes: 31 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict'

const fsp = require('node:fs').promises
const path = require('node:path')
const config = require('./config.js')
const server = require(`./transport/${config.api.transport}.js`)
const staticServer = require('./static.js')
const db = require('./db.js')(config.db)
const hash = require('./hash.js')
const logger = require(`./logger/${config.logger.name}.js`)
const console = logger(config.logger)

const injection = {
console: Object.freeze(console),
db: Object.freeze(db),
common: Object.freeze({ hash }),
}
const apiPath = path.join(process.cwd(), './api')
const routing = {}

;(async () => {
const files = await fsp.readdir(apiPath)
for (const fileName of files) {
if (!fileName.endsWith('.js')) continue
const filePath = path.join(apiPath, fileName)
const serviceName = path.basename(fileName, '.js')
routing[serviceName] = require(filePath)(injection)
}
staticServer('./static', config.static.port, console)
server(routing, config.api.port, console)
})()
Loading

0 comments on commit 458da3e

Please sign in to comment.