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:
88
backend/src/mappers/race.ts
Normal file
88
backend/src/mappers/race.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/** Row shape returned by PostgreSQL (snake_case). */
|
||||
export interface RaceRow {
|
||||
id: string;
|
||||
race_date: string;
|
||||
title: string;
|
||||
distance_km: string;
|
||||
status: string | null;
|
||||
official_url: string | null;
|
||||
start_time: string | null;
|
||||
cluster_schedule: string | null;
|
||||
bib_pickup: string | null;
|
||||
bib_number: string | null;
|
||||
finish_time: string | null;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
/** API shape (camelCase). */
|
||||
export interface RaceDto {
|
||||
id: string;
|
||||
date: string;
|
||||
title: string;
|
||||
distanceKm: number;
|
||||
status: string | null;
|
||||
officialUrl: string | null;
|
||||
startTime: string | null;
|
||||
clusterSchedule: string | null;
|
||||
bibPickup: string | null;
|
||||
bibNumber: string | null;
|
||||
finishTime: string | null;
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
/** Convert a DB row to the API DTO (camelCase). */
|
||||
export function rowToDto(row: RaceRow): RaceDto {
|
||||
return {
|
||||
id: row.id,
|
||||
date: row.race_date,
|
||||
title: row.title,
|
||||
distanceKm: parseFloat(row.distance_km),
|
||||
status: row.status,
|
||||
officialUrl: row.official_url,
|
||||
startTime: row.start_time,
|
||||
clusterSchedule: row.cluster_schedule,
|
||||
bibPickup: row.bib_pickup,
|
||||
bibNumber: row.bib_number,
|
||||
finishTime: row.finish_time,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
/** Map incoming camelCase body fields to snake_case column names. */
|
||||
const FIELD_MAP: Record<string, string> = {
|
||||
date: "race_date",
|
||||
title: "title",
|
||||
distanceKm: "distance_km",
|
||||
status: "status",
|
||||
officialUrl: "official_url",
|
||||
startTime: "start_time",
|
||||
clusterSchedule: "cluster_schedule",
|
||||
bibPickup: "bib_pickup",
|
||||
bibNumber: "bib_number",
|
||||
finishTime: "finish_time",
|
||||
notes: "notes",
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a partial camelCase body into { columns, values, placeholders }
|
||||
* suitable for SQL INSERT / UPDATE.
|
||||
*/
|
||||
export function bodyToColumns(body: Record<string, unknown>) {
|
||||
const columns: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
|
||||
for (const [camel, snake] of Object.entries(FIELD_MAP)) {
|
||||
if (camel in body) {
|
||||
columns.push(snake);
|
||||
values.push(body[camel]);
|
||||
}
|
||||
}
|
||||
|
||||
return { columns, values };
|
||||
}
|
||||
Reference in New Issue
Block a user