initial commit

This commit is contained in:
Roman Siverov 2026-06-04 13:36:52 +03:00
commit 46911314f2
16 changed files with 1733 additions and 0 deletions

22
src/db.ts Normal file
View file

@ -0,0 +1,22 @@
import { Pool } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
export const pgPool = new Pool({
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT || 5432),
database: process.env.POSTGRES_DB,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
});
export async function initDb(): Promise<void> {
await pgPool.query(`
CREATE TABLE IF NOT EXISTS motd (
id SERIAL PRIMARY KEY,
message TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
)
`);
}

85
src/http/motd.ts Normal file
View file

@ -0,0 +1,85 @@
import { Express } from 'express';
import { redisClient } from '../index.js';
import { pgPool } from '../db.js';
export async function httpMotd(app: Express) {
app.get('/motd', async (_req, res) => {
try {
const cached = await redisClient.get('motd:current');
if (cached) {
return res.json({
source: 'redis',
message: cached,
});
}
const result = await pgPool.query(
`
SELECT message
FROM motd
ORDER BY created_at DESC, id DESC
LIMIT 1
`
);
if (result.rowCount === 0) {
return res.status(404).json({
error: 'No MOTD set',
});
}
const message = result.rows[0].message as string;
await redisClient.set('motd:current', message, {
EX: 60,
});
return res.json({
source: 'postgres',
message,
});
} catch (error) {
console.error(error);
return res.status(500).json({
error: 'Internal server error',
});
}
});
app.post('/motd', async (req, res) => {
try {
const message = req.body?.message;
if (typeof message !== 'string' || message.trim() === '') {
return res.status(400).json({
error: 'message must be a non-empty string',
});
}
const trimmedMessage = message.trim();
await pgPool.query(
`
INSERT INTO motd (message)
VALUES ($1)
`,
[trimmedMessage]
);
await redisClient.set('motd:current', trimmedMessage, {
EX: 60,
});
return res.status(201).json({
message: 'MOTD updated',
motd: trimmedMessage,
});
} catch (error) {
console.error(error);
return res.status(500).json({
error: 'Internal server error',
});
}
});
}

36
src/index.ts Normal file
View file

@ -0,0 +1,36 @@
import dotenv from 'dotenv';
import express from 'express';
import { createClient } from 'redis';
import { initDb } from './db.js';
import { httpMotd } from './http/motd.js';
dotenv.config();
const app = express();
app.use(express.json());
const port = Number(3000);
export const redisClient = createClient({
url: process.env.REDIS_URL,
});
redisClient.on('error', (err) => {
console.error('Redis error:', err);
});
async function main(): Promise<void> {
await redisClient.connect();
await initDb();
await httpMotd(app);
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
}
main().catch((error) => {
console.error('Failed to start app:', error);
process.exit(1);
});