Spaces:
Paused
Paused
Download src/core.js from Shaurya2020/paddock-api: direct link, hf CLI and curl.
- Browser
- Download file 5.32 kB
-
https://huggingface.co/spaces/Shaurya2020/paddock-api/resolve/main/src/core.js
- Command line
-
hf download hf://spaces/Shaurya2020/paddock-api/src/core.js
-
curl -L -o core.js https://huggingface.co/spaces/Shaurya2020/paddock-api/resolve/main/src/core.js
5.32 kB
| const crypto = require('node:crypto'); | |
| const { open } = require('../horse-racing-migrate.js'); | |
| const db = open(); | |
| const newId = p => p + '_' + crypto.randomBytes(6).toString('hex'); | |
| /* ββ errors ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| class ApiError extends Error { | |
| constructor(status, message, detail) { super(message); this.status = status; this.detail = detail; } | |
| } | |
| const bad = (m, d) => { throw new ApiError(400, m, d); }; | |
| const notFound = m => { throw new ApiError(404, m); }; | |
| const forbid = m => { throw new ApiError(403, m); }; | |
| const conflict = m => { throw new ApiError(409, m); }; | |
| /** Wraps an async handler so thrown ApiErrors become clean JSON responses. */ | |
| const h = fn => (req, res, next) => Promise.resolve(fn(req, res)).catch(next); | |
| /* ββ auth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| // Demo token format: "admin:<id>". A real deployment verifies a signed JWT here | |
| // and pulls id + role from the claims. Everything downstream is unchanged. | |
| function authenticate(req, _res, next) { | |
| const raw = (req.headers.authorization || '').replace(/^Bearer\s+/i, ''); | |
| if (!raw) return next(new ApiError(401, 'Missing bearer token.')); | |
| const id = raw.startsWith('admin:') ? raw.slice(6) : raw; | |
| const admin = db.prepare('SELECT id, name, role, parent_admin_id, label, status FROM admins WHERE id = ?').get(id); | |
| if (!admin) return next(new ApiError(401, 'Token does not match a known admin.')); | |
| if (admin.status !== 'active') return next(new ApiError(403, 'This admin is suspended.')); | |
| req.admin = admin; | |
| next(); | |
| } | |
| /** | |
| * The scope filter. This is the whole multi-tenant guarantee, and it lives in | |
| * the WHERE clause on purpose β a role-2 admin who forges another label's id in | |
| * the URL gets an empty result set from the database, not a row that | |
| * application code then has to remember to reject. | |
| * | |
| * Returns { sql, params } to splice into a query: | |
| * const s = scope(req.admin, 'e'); | |
| * db.prepare(`SELECT * FROM race_events e WHERE ${s.sql}`).all(...s.params) | |
| */ | |
| function scope(admin, alias = '', column = 'owner_admin_id') { | |
| const col = alias ? `${alias}.${column}` : column; | |
| if (admin.role === 1) return { sql: '1=1', params: [] }; // platform sees all | |
| return { sql: `${col} = ?`, params: [admin.id] }; // white label sees its own | |
| } | |
| /** For writes: the row a role-2 admin creates is always owned by that admin. */ | |
| function ownerFor(admin, requested) { | |
| if (admin.role === 2) { | |
| if (requested && requested !== admin.id) forbid('A white label cannot create rows for another operator.'); | |
| return admin.id; | |
| } | |
| if (!requested) bad('owner_admin_id is required when acting as super admin.'); | |
| const target = db.prepare('SELECT id, role FROM admins WHERE id = ?').get(requested); | |
| if (!target) bad('owner_admin_id does not match a known admin.', { owner_admin_id: requested }); | |
| return requested; | |
| } | |
| /** Fetch an event the caller is allowed to touch, or 404/403. */ | |
| function loadEvent(admin, eventId) { | |
| const s = scope(admin, 'e'); | |
| const row = db.prepare(`SELECT e.* FROM race_events e WHERE e.id = ? AND ${s.sql}`).get(eventId, ...s.params); | |
| if (!row) notFound('Race event not found.'); | |
| return row; | |
| } | |
| /* ββ bet type rules (server side is the authority) βββββββββββββββββββββββββ */ | |
| const BET_TYPES = { | |
| win: { picks: 1, ordered: false, exotic: false, takeout: 0 }, | |
| place: { picks: 1, ordered: false, exotic: false, takeout: 0 }, | |
| show: { picks: 1, ordered: false, exotic: false, takeout: 0 }, | |
| exacta: { picks: 2, ordered: true, exotic: true, takeout: 0.18 }, | |
| trifecta: { picks: 3, ordered: true, exotic: true, takeout: 0.22 }, | |
| superfecta: { picks: 4, ordered: true, exotic: true, takeout: 0.25 } | |
| }; | |
| /** Live prices for an event's runners, from the current odds version. */ | |
| function currentOdds(eventId) { | |
| return db.prepare(` | |
| SELECT oe.participant_id, oe.decimal_odds, oe.previous_odds | |
| FROM odds_entries oe | |
| JOIN odds_versions ov ON ov.id = oe.version_id | |
| JOIN race_events e ON e.id = ov.event_id AND e.current_odds_version = ov.version_no | |
| WHERE ov.event_id = ?`).all(eventId); | |
| } | |
| function participants(eventId) { | |
| const prices = Object.fromEntries(currentOdds(eventId).map(o => [o.participant_id, o])); | |
| return db.prepare(` | |
| SELECT p.*, j.name AS jockey_name, t.name AS team_name | |
| FROM race_participants p | |
| LEFT JOIN jockeys j ON j.id = p.jockey_id | |
| LEFT JOIN teams t ON t.id = p.team_id | |
| WHERE p.event_id = ? ORDER BY p.cloth_no`).all(eventId) | |
| .map(p => ({ | |
| ...p, | |
| is_scratched: !!p.is_scratched, | |
| decimal_odds: prices[p.id]?.decimal_odds ?? null, | |
| previous_odds: prices[p.id]?.previous_odds ?? null, | |
| stats: JSON.parse(p.stats_json || '{}') | |
| })); | |
| } | |
| module.exports = { | |
| db, newId, ApiError, bad, notFound, forbid, conflict, h, | |
| authenticate, scope, ownerFor, loadEvent, BET_TYPES, currentOdds, participants | |
| }; | |