File size: 4,694 Bytes
552668e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const { Router } = require('express');
const { db, newId, h, bad, conflict, notFound, scope, ownerFor } = require('./core.js');

const r = Router();

/**
 * Teams and jockeys are the same shape of resource, so one factory builds both
 * routers. Four endpoints each: list, create, update, delete β€” eight in total.
 */
function crud({ table, prefix, label, fields, shape }) {
  const rt = Router();

  /* list */
  rt.get('/', h((req, res) => {
    const s = scope(req.admin, 'x');
    const where = [s.sql], params = [...s.params];
    if (req.query.q) { where.push('x.name LIKE ?'); params.push(`%${req.query.q}%`); }
    if (req.query.active === 'true') where.push('x.is_active = 1');

    const rows = db.prepare(`SELECT x.*, a.label AS operator FROM ${table} x
      JOIN admins a ON a.id = x.owner_admin_id
      WHERE ${where.join(' AND ')} ORDER BY x.name`).all(...params);
    res.json({ data: rows.map(shape), meta: { count: rows.length } });
  }));

  /* create */
  rt.post('/', h((req, res) => {
    const b = req.body || {};
    if (!b.name) bad('name is required.');
    const owner = ownerFor(req.admin, b.owner_admin_id);

    if (db.prepare(`SELECT 1 FROM ${table} WHERE owner_admin_id = ? AND name = ?`).get(owner, b.name))
      conflict(`This operator already has a ${label} called "${b.name}".`);

    const id = newId(prefix);
    const cols = ['id', 'owner_admin_id', 'name', ...fields, 'stats_json'];
    const vals = [id, owner, b.name, ...fields.map(f => b[f] ?? null), JSON.stringify(b.stats || {})];
    db.prepare(`INSERT INTO ${table} (${cols.join(',')}) VALUES (${cols.map(() => '?').join(',')})`).run(...vals);

    res.status(201).json({ data: shape(db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(id)) });
  }));

  /* update */
  rt.put('/:id', h((req, res) => {
    const s = scope(req.admin, 'x');
    const row = db.prepare(`SELECT x.* FROM ${table} x WHERE x.id = ? AND ${s.sql}`).get(req.params.id, ...s.params);
    if (!row) notFound(`${label} not found.`);

    const sets = [], params = [];
    for (const f of ['name', ...fields, 'is_active'])
      if (f in (req.body || {})) { sets.push(`${f} = ?`); params.push(req.body[f]); }
    if ('stats' in (req.body || {})) { sets.push('stats_json = ?'); params.push(JSON.stringify(req.body.stats)); }
    if (!sets.length) bad('No editable fields supplied.', { allowed: ['name', ...fields, 'is_active', 'stats'] });

    db.prepare(`UPDATE ${table} SET ${sets.join(', ')} WHERE id = ?`).run(...params, row.id);
    res.json({ data: shape(db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(row.id)) });
  }));

  /* delete */
  rt.delete('/:id', h((req, res) => {
    const s = scope(req.admin, 'x');
    const row = db.prepare(`SELECT x.* FROM ${table} x WHERE x.id = ? AND ${s.sql}`).get(req.params.id, ...s.params);
    if (!row) notFound(`${label} not found.`);

    const col = table === 'teams' ? 'team_id' : 'jockey_id';
    const used = db.prepare(`SELECT count(*) n FROM race_participants WHERE ${col} = ?`).get(row.id).n;

    // Deleting would orphan historical racecards, so a used record is retired instead.
    if (used) {
      db.prepare(`UPDATE ${table} SET is_active = 0 WHERE id = ?`).run(row.id);
      return res.json({ data: { id: row.id, deactivated: true,
        message: `This ${label} appears on ${used} racecard${used > 1 ? 's' : ''}, so it was retired rather than deleted.` } });
    }
    db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(row.id);
    res.json({ data: { id: row.id, deleted: true } });
  }));

  return rt;
}

/* ── 1-4: teams ──────────────────────────────────────────────────────────── */
r.use('/teams', crud({
  table: 'teams', prefix: 'tm', label: 'stable', fields: ['silks', 'base'],
  shape: t => ({ id: t.id, owner_admin_id: t.owner_admin_id, operator: t.operator,
    name: t.name, silks: t.silks, base: t.base, is_active: !!t.is_active,
    stats: JSON.parse(t.stats_json || '{}'), created_at: t.created_at })
}));

/* ── 5-8: jockeys ────────────────────────────────────────────────────────── */
r.use('/jockeys', crud({
  table: 'jockeys', prefix: 'jk', label: 'jockey', fields: ['claim_kg', 'riding_weight'],
  shape: j => ({ id: j.id, owner_admin_id: j.owner_admin_id, operator: j.operator,
    name: j.name, claim_kg: j.claim_kg, riding_weight: j.riding_weight, is_active: !!j.is_active,
    stats: JSON.parse(j.stats_json || '{}'), created_at: j.created_at })
}));

module.exports = r;