myheats

Live heats, scoring and leaderboard for sport events
git clone https://git.in0rdr.ch/myheats.git
Log | Files | Refs | Pull requests | README | LICENSE

db.cjs (11199B)


      1 const postgres = require('postgres');
      2 let scoreSubscription = null
      3 
      4 // Keep track of connected websocket clients
      5 // https://javascript.info/websocket
      6 const clients = new Set();
      7 
      8 require('dotenv').config({
      9   // Configure common config files and modes
     10   // - https://www.npmjs.com/package/dotenv
     11   // - https://vitejs.dev/guide/env-and-mode
     12   path: [
     13     '.env.local',
     14     '.env.development', '.env.development.local',
     15     '.env.test',        '.env.test.local',
     16     '.env.production',  '.env.production.local',
     17     '.env'
     18   ]
     19 });
     20 
     21 // Use psql environment variables
     22 // https://github.com/porsager/postgres?tab=readme-ov-file#environmental-variables
     23 sql = postgres({
     24   publications: [
     25     'myheats_realtime',
     26   ]
     27 })
     28 
     29 async function invalidateToken(token) {
     30   try {
     31     const users = await sql`
     32       update public.judges set
     33         token = null,
     34         expires_at = null
     35       where token = ${token}
     36     `
     37     return users
     38   } catch (error) {
     39     console.error('Error occurred in invalidateToken:', error);
     40     throw error
     41   }
     42 }
     43 
     44 async function lookupToken(token) {
     45   try {
     46     const users = await sql`
     47       select * from public.judges where
     48       token = ${token}
     49     `
     50     return users
     51   } catch (error) {
     52     console.error('Error occurred in lookupToken:', error);
     53     throw error
     54   }
     55 }
     56 
     57 async function saveToken(id, token, exp) {
     58   try {
     59     const users = await sql`
     60       update public.judges set
     61         token = ${token},
     62         expires_at = ${exp}
     63       where id = ${id}
     64     `
     65     return users
     66   } catch (error) {
     67     console.error('Error occurred in saveToken:', error);
     68     throw error
     69   }
     70 }
     71 
     72 // Request a signed magic link
     73 // https://clerk.com/blog/magic-links#building-a-magic-link-system
     74 async function getUser(email) {
     75   try {
     76     const users = await sql`
     77       select
     78         id,
     79         firstname,
     80         lastname,
     81         token,
     82         email
     83       from public.judges
     84       where email = ${email}
     85     `
     86     return users
     87   } catch (error) {
     88     console.error('Error occurred in getUser:', error);
     89     throw error
     90   }
     91 }
     92 
     93 async function allHeats() {
     94   try {
     95     const heats = await sql`
     96       select * from heats
     97     `
     98     return heats
     99   } catch (error) {
    100     console.error('Error occurred in allHeats:', error);
    101     throw error
    102   }
    103 }
    104 
    105 async function getHeat(heatId) {
    106   try {
    107     const heat = await sql`
    108       select * from heats
    109       where id = ${heatId}
    110     `
    111     return heat
    112   } catch (error) {
    113     console.error('Error occurred in getHeat:', error);
    114     throw error
    115   }
    116 }
    117 
    118 async function allAthletes() {
    119   try {
    120     const athletes = await sql`
    121       select * from athletes
    122     `
    123     return athletes
    124   } catch (error) {
    125     console.error('Error occurred in allAthletes:', error);
    126     throw error
    127   }
    128 }
    129 
    130 async function allJudges() {
    131   try {
    132     const athletes = await sql`
    133       select * from judges
    134     `
    135     return athletes
    136   } catch (error) {
    137     console.error('Error occurred in allJudges:', error);
    138     throw error
    139   }
    140 }
    141 
    142 async function addAthleteToHeat(athlete, heat) {
    143   try {
    144     const startlist = await sql`
    145       insert into startlist (
    146         athlete,
    147         heat
    148       )
    149       values (
    150         ${athlete},
    151         ${heat}
    152       )
    153       returning *
    154     `
    155     return startlist
    156   } catch (error) {
    157     console.error('Error occurred in addAthleteToHeat:', error);
    158     throw error
    159   }
    160 }
    161 
    162 async function addAthlete(nr, firstname, lastname, birthday, school) {
    163   try {
    164     const athlete = await sql`
    165       insert into athletes (
    166         nr,
    167         firstname,
    168         lastname,
    169         birthday,
    170         school
    171       )
    172       values (
    173         ${nr},
    174         ${firstname},
    175         ${lastname},
    176         ${birthday},
    177         ${school}
    178       )
    179       returning *
    180     `
    181     return athlete
    182   } catch (error) {
    183     console.error('Error occurred in addAthlete:', error);
    184     throw error
    185   }
    186 }
    187 
    188 async function removeAthlete(id) {
    189   try {
    190     const athlete = await sql`
    191       delete from athletes where id = ${id}
    192       returning *
    193     `
    194     return athlete
    195   } catch (error) {
    196     console.error('Error occurred in removeAthlete:', error);
    197     throw error
    198   }
    199 }
    200 
    201 async function addJudge(email, firstname, lastname) {
    202   try {
    203     const judge = await sql`
    204       insert into judges (
    205         email,
    206         firstname,
    207         lastname
    208       )
    209       values (
    210         ${email},
    211         ${firstname},
    212         ${lastname}
    213       )
    214       returning *
    215     `
    216     return judge
    217   } catch (error) {
    218     console.error('Error occurred in addJudge:', error);
    219     throw error
    220   }
    221 }
    222 
    223 async function removeJudge(id) {
    224   try {
    225     const judge = await sql`
    226       delete from judges where id = ${id}
    227       returning *
    228     `
    229     return judge
    230   } catch (error) {
    231     console.error('Error occurred in removeJudge:', error);
    232     throw error
    233   }
    234 }
    235 
    236 async function removeAthleteFromHeat(startlistId) {
    237   try {
    238     const startlist = await sql`
    239       delete from startlist
    240       where id = ${startlistId}
    241       returning *
    242     `
    243     return startlist
    244   } catch (error) {
    245     console.error('Error occurred in removeAthleteFromHeat:', error);
    246     throw error
    247   }
    248 }
    249 
    250 async function newHeat(name, heatLocation, plannedStart) {
    251   try {
    252     const heat = await sql`
    253       insert into heats (
    254         name,
    255         location,
    256         planned_start
    257       )
    258       values (
    259         ${name},
    260         ${heatLocation},
    261         ${plannedStart}
    262       )
    263       returning *
    264     `
    265     return heat
    266   } catch (error) {
    267     console.error('Error occurred in newHeat:', error);
    268     throw error
    269   }
    270 }
    271 
    272 async function removeHeat(heatId) {
    273   try {
    274     const heat = await sql`
    275       delete from heats where id = ${heatId}
    276       returning *
    277     `
    278     return heat
    279   } catch (error) {
    280     console.error('Error occurred in removeHeat:', error);
    281     throw error
    282   }
    283 }
    284 
    285 async function distinctStartlist(heatIds) {
    286   try {
    287     const startlist = await sql`
    288       select
    289         id,
    290         athlete,
    291         nr,
    292         firstname,
    293         lastname,
    294         birthday,
    295         school
    296       from distinct_startlist(${heatIds}) as athlete
    297     `
    298     return startlist
    299   } catch (error) {
    300     console.error('Error occurred in distinctStartlist:', error);
    301     throw error
    302   }
    303 }
    304 
    305 async function startlistWithAthletes(heatId) {
    306   try {
    307     // left outer join, to fetch heats with no athletes
    308     // https://www.postgresql.org/docs/current/tutorial-join.html
    309     const startlist = await sql`
    310       select
    311         s.id as startlist_id,
    312         s.heat,
    313         s.athlete,
    314         a.id as athlete_id,
    315         a.nr,
    316         a.firstname,
    317         a.lastname,
    318         a.birthday,
    319         a.school
    320       from startlist as s
    321       left outer join athletes as a
    322       on s.athlete = a.id
    323       where s.heat = ${heatId}
    324     `
    325     return startlist
    326   } catch (error) {
    327     console.error('Error occurred in startlistWithAthletes:', error);
    328     throw error
    329   }
    330 }
    331 
    332 async function scoresForHeatAndAthlete(heat, athlete) {
    333   try {
    334     const score = await sql`
    335       select
    336         id,
    337         athlete,
    338         judge,
    339         score
    340       from scores where heat = ${heat} and athlete = ${athlete}
    341     `
    342     return score
    343   } catch (error) {
    344     console.error('Error occurred in scoresForHeatAndAthlete:', error);
    345     throw error
    346   }
    347 }
    348 
    349 async function scoreSummaryForHeatAndAthlete(heat, athlete) {
    350   try {
    351     const summary = await sql`
    352       select score_summary
    353       from score_summary where heat_id = ${heat} and athlete_id = ${athlete}
    354     `
    355     return summary
    356   } catch (error) {
    357     console.error('Error occurred in scoreSummaryForHeatAndAthlete:', error);
    358     throw error
    359   }
    360 }
    361 
    362 async function getScore(heat, athlete, judge) {
    363   try {
    364     const scores = await sql`
    365       select * from scores
    366       where heat = ${heat} and athlete = ${athlete} and judge = ${judge}
    367     `
    368     return scores
    369   } catch (error) {
    370     console.error('Error occurred in getScore:', error);
    371     throw error
    372   }
    373 }
    374 
    375 // "upsert" score (https://www.postgresql.org/docs/current/sql-insert.html)
    376 // For ON CONFLICT DO UPDATE, a conflict_target must be provided.
    377 async function setScore(heat, athlete, judge, score) {
    378   try {
    379     const scores = await sql`
    380       insert into public.scores as s (athlete, judge, score, heat)
    381         values (${athlete}, ${judge}, ${score}, ${heat})
    382       on conflict (athlete, judge, heat) do update
    383       set score = ${score}
    384       where s.heat = ${heat} and s.athlete = ${athlete} and s.judge = ${judge}
    385       returning *
    386     `
    387     return scores
    388   } catch (error) {
    389     console.error('Error occurred in setScore:', error);
    390     throw error
    391   }
    392 }
    393 
    394 async function getSetting(name) {
    395   try {
    396     const setting = await sql`
    397       select * from settings
    398       where name = ${name}
    399     `
    400     return setting
    401   } catch (error) {
    402     console.error('Error occurred in getSetting:', error);
    403     throw error
    404   }
    405 }
    406 
    407 async function allSettings() {
    408   try {
    409     const settings = await sql`
    410       select * from settings
    411     `
    412     return settings
    413   } catch (error) {
    414     console.error('Error occurred in allSettings:', error);
    415     throw error
    416   }
    417 }
    418 
    419 async function removeSetting(name) {
    420   try {
    421     const setting = await sql`
    422       delete from settings where name = ${name}
    423       returning *
    424     `
    425     return setting
    426   } catch (error) {
    427     console.error('Error occurred in removeSetting:', error);
    428     throw error
    429   }
    430 }
    431 
    432 // "upsert" setting
    433 async function updateSetting(name, value) {
    434   try {
    435     const setting = await sql`
    436       insert into public.settings as s (name, value)
    437         values (${name}, ${value})
    438       on conflict (name) do update
    439       set value = ${value}
    440       where s.name = ${name}
    441       returning *
    442     `
    443     return setting
    444   } catch (error) {
    445     console.error('Error occurred in updateSetting:', error);
    446     throw error
    447   }
    448 }
    449 
    450 function removeClient(sock) {
    451   console.log("- Client removed")
    452   clients.delete(sock)
    453 }
    454 
    455 function addClient(sock) {
    456   console.log("+ Client added")
    457   clients.add(sock)
    458 }
    459 
    460 // add client to subscription
    461 async function watchScores(sock) {
    462   if (scoreSubscription !== null) {
    463     // we are already subscribed to this publication
    464     return scoreSubscription
    465   }
    466 
    467   // subscribe to score publication
    468   try {
    469     scoreSubscription = await sql.subscribe(
    470       '*:scores',
    471       (row, { command, relation, key, old }) => {
    472         // distributed score updates to all connected clients
    473         for(let c of clients) {
    474           c.send(JSON.stringify({
    475             "message": "Received new scores",
    476             "data": row,
    477           }))
    478         }
    479       },
    480       () => {
    481         // callback on initial connect and potential reconnects
    482         console.log("~ Start watching scores, created scoreSubscription")
    483       }
    484     )
    485     return scoreSubscription
    486   } catch (error) {
    487     console.error('Error occurred in watchScores:', error);
    488     throw error
    489   }
    490 }
    491 
    492 module.exports = {
    493   sql,
    494   invalidateToken,
    495   lookupToken,
    496   saveToken,
    497   getUser,
    498   allHeats,
    499   getHeat,
    500   allAthletes,
    501   allJudges,
    502   newHeat,
    503   removeHeat,
    504   distinctStartlist,
    505   startlistWithAthletes,
    506   scoresForHeatAndAthlete,
    507   scoreSummaryForHeatAndAthlete,
    508   getScore,
    509   setScore,
    510   watchScores,
    511   removeClient,
    512   addClient,
    513   addAthleteToHeat,
    514   addAthlete,
    515   removeAthlete,
    516   addJudge,
    517   removeJudge,
    518   removeAthleteFromHeat,
    519   getSetting,
    520   allSettings,
    521   updateSetting,
    522   removeSetting,
    523 }