feat(backend): implement REST API for races calendar

Express + TypeScript backend with PostgreSQL: CRUD endpoints for /races (GET list with year/month filters, GET by id, POST, PATCH, DELETE), health/readiness probes, SQL migration runner, seed script with upsert from CSV, camelCase/snake_case mapper, CORS, env validation, docker-compose, and API docs for frontend.

Made-with: Cursor
This commit is contained in:
Anton
2026-04-01 14:47:53 +03:00
parent 88a448dd8e
commit 698ae37553
17 changed files with 2242 additions and 0 deletions

74
backend/src/seed.ts Normal file
View File

@@ -0,0 +1,74 @@
import fs from "fs";
import path from "path";
import { parse } from "csv-parse/sync";
import { pool } from "./db";
interface CsvRow {
date: string;
month: string;
day: string;
event: string;
distance_km: string;
}
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[«»"]/g, "")
.replace(/[^a-zа-яё0-9]+/gi, "-")
.replace(/(^-|-$)/g, "")
.substring(0, 60);
}
function makeId(date: string, title: string): string {
return `${date}-${slugify(title)}`;
}
async function seed() {
const csvPath = path.resolve(__dirname, "../../import/races_2026_calendar.csv");
if (!fs.existsSync(csvPath)) {
console.error(`[seed] CSV not found: ${csvPath}`);
process.exit(1);
}
const raw = fs.readFileSync(csvPath, "utf-8");
const records: CsvRow[] = parse(raw, {
columns: true,
skip_empty_lines: true,
trim: true,
});
console.log(`[seed] Parsed ${records.length} rows from CSV`);
const client = await pool.connect();
try {
for (const row of records) {
const id = makeId(row.date, row.event);
const distanceKm = parseFloat(row.distance_km);
await client.query(
`INSERT INTO races (id, race_date, title, distance_km, status)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE SET
race_date = EXCLUDED.race_date,
title = EXCLUDED.title,
distance_km = EXCLUDED.distance_km,
status = EXCLUDED.status,
updated_at = NOW()`,
[id, row.date, row.event, distanceKm, "planned"],
);
console.log(`[seed] Upserted: ${id}`);
}
console.log("[seed] Done.");
} finally {
client.release();
await pool.end();
}
}
seed().catch((err) => {
console.error("[seed] FAILED:", err.message);
process.exit(1);
});