Spaces:
Paused
Paused
Download src/raceEventsApi.js from Shaurya2020/paddock-api: direct link, hf CLI and curl.
- Browser
- Download file 6.29 kB
-
https://huggingface.co/spaces/Shaurya2020/paddock-api/resolve/main/src/raceEventsApi.js
- Command line
-
hf download hf://spaces/Shaurya2020/paddock-api/src/raceEventsApi.js
-
curl -L -o raceEventsApi.js https://huggingface.co/spaces/Shaurya2020/paddock-api/resolve/main/src/raceEventsApi.js
6.29 kB
| const { Router } = require('express'); | |
| const { db, newId, h, bad, conflict, scope, ownerFor, loadEvent, participants } = require('./core.js'); | |
| const r = Router(); | |
| /** Legal lifecycle moves. Anything not listed here is rejected. */ | |
| const TRANSITIONS = { | |
| draft: ['open', 'void'], | |
| open: ['closed', 'void'], | |
| closed: ['resulted', 'void', 'open'], // reopening is allowed while nothing is settled | |
| resulted: ['settled', 'void', 'closed'], // result can be corrected before settlement | |
| settled: [], // terminal β money has moved | |
| void: [] // terminal | |
| }; | |
| const shape = e => ({ | |
| id: e.id, owner_admin_id: e.owner_admin_id, name: e.name, venue: e.venue, | |
| race_no: e.race_no, distance_m: e.distance_m, going: e.going, race_class: e.race_class, | |
| post_time: e.post_time, status: e.status, odds_version: e.current_odds_version, | |
| void_reason: e.void_reason, created_at: e.created_at | |
| }); | |
| /* 1 ββ GET /race-events βββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| r.get('/', h((req, res) => { | |
| const s = scope(req.admin, 'e'); | |
| const where = [s.sql], params = [...s.params]; | |
| if (req.query.status) { where.push('e.status = ?'); params.push(req.query.status); } | |
| if (req.query.venue) { where.push('e.venue LIKE ?'); params.push(`%${req.query.venue}%`); } | |
| const rows = db.prepare(` | |
| SELECT e.*, | |
| (SELECT count(*) FROM race_participants p WHERE p.event_id = e.id AND p.is_scratched = 0) AS runners, | |
| (SELECT coalesce(sum(b.stake),0) FROM bets b WHERE b.event_id = e.id AND b.status <> 'cancelled') AS held | |
| FROM race_events e | |
| WHERE ${where.join(' AND ')} | |
| ORDER BY e.post_time`).all(...params); | |
| res.json({ data: rows.map(e => ({ ...shape(e), runners: e.runners, total_staked: e.held })) }); | |
| })); | |
| /* 2 ββ GET /race-events/:id βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| r.get('/:id', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| res.json({ data: { ...shape(e), field: participants(e.id) } }); | |
| })); | |
| /* 3 ββ POST /race-events ββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| r.post('/', h((req, res) => { | |
| const b = req.body || {}; | |
| if (!b.name) bad('name is required.'); | |
| if (!b.venue) bad('venue is required.'); | |
| if (!b.distance_m) bad('distance_m is required.'); | |
| if (!b.post_time) bad('post_time is required.'); | |
| const id = newId('evt'); | |
| db.prepare(`INSERT INTO race_events | |
| (id, owner_admin_id, name, venue, race_no, distance_m, going, race_class, post_time, status) | |
| VALUES (?,?,?,?,?,?,?,?,?, 'draft')`) | |
| .run(id, ownerFor(req.admin, b.owner_admin_id), b.name, b.venue, | |
| b.race_no ?? 1, b.distance_m, b.going ?? null, b.race_class ?? null, b.post_time); | |
| // Runners can arrive with the race or be added later. | |
| for (const p of b.field || []) { | |
| db.prepare(`INSERT INTO race_participants | |
| (id, event_id, cloth_no, horse_name, jockey_id, team_id, carried_weight, recent_form) | |
| VALUES (?,?,?,?,?,?,?,?)`) | |
| .run(`${id}_p${p.cloth_no}`, id, p.cloth_no, p.horse_name, | |
| p.jockey_id ?? null, p.team_id ?? null, p.carried_weight ?? null, p.recent_form ?? null); | |
| } | |
| res.status(201).json({ data: { ...shape(loadEvent(req.admin, id)), field: participants(id) } }); | |
| })); | |
| /* 4 ββ PUT /race-events/:id βββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| r.put('/:id', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| if (['settled', 'void'].includes(e.status)) conflict(`A ${e.status} race cannot be edited.`); | |
| const allowed = ['name', 'venue', 'race_no', 'distance_m', 'going', 'race_class', 'post_time']; | |
| const sets = [], params = []; | |
| for (const k of allowed) if (k in (req.body || {})) { sets.push(`${k} = ?`); params.push(req.body[k]); } | |
| if (!sets.length) bad('No editable fields supplied.', { allowed }); | |
| sets.push(`updated_at = datetime('now')`); | |
| db.prepare(`UPDATE race_events SET ${sets.join(', ')} WHERE id = ?`).run(...params, e.id); | |
| res.json({ data: shape(loadEvent(req.admin, e.id)) }); | |
| })); | |
| /* 5 ββ PATCH /race-events/:id/status ββββββββββββββββββββββββββββββββββββββ */ | |
| r.patch('/:id/status', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| const next = (req.body || {}).status; | |
| if (!next) bad('status is required.'); | |
| const legal = TRANSITIONS[e.status] || []; | |
| if (!legal.includes(next)) | |
| conflict(`Cannot move a race from ${e.status} to ${next}.`); | |
| // Opening betting on a race with no priced runners would take money on nothing. | |
| if (next === 'open') { | |
| const priced = db.prepare(` | |
| SELECT count(*) n FROM race_participants p | |
| JOIN odds_entries oe ON oe.participant_id = p.id | |
| JOIN odds_versions ov ON ov.id = oe.version_id AND ov.version_no = ? | |
| WHERE p.event_id = ? AND p.is_scratched = 0`).get(e.current_odds_version, e.id).n; | |
| if (priced < 2) conflict('At least two priced runners are needed before betting can open.'); | |
| } | |
| db.prepare(`UPDATE race_events SET status = ?, updated_at = datetime('now') WHERE id = ?`).run(next, e.id); | |
| res.json({ data: shape(loadEvent(req.admin, e.id)) }); | |
| })); | |
| /* 6 ββ DELETE /race-events/:id ββββββββββββββββββββββββββββββββββββββββββββ */ | |
| r.delete('/:id', h((req, res) => { | |
| const e = loadEvent(req.admin, req.params.id); | |
| const bets = db.prepare(`SELECT count(*) n FROM bets WHERE event_id = ?`).get(e.id).n; | |
| if (bets) conflict(`This race has ${bets} bets against it. Void it instead of deleting.`); | |
| if (e.status !== 'draft') conflict('Only a draft race can be deleted.'); | |
| db.prepare('DELETE FROM race_events WHERE id = ?').run(e.id); | |
| res.json({ data: { id: e.id, deleted: true } }); | |
| })); | |
| module.exports = r; | |