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 (10276B)


      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 allHeats:', error);
    126     throw error
    127   }
    128 }
    129 
    130 async function addAthleteToHeat(athlete, heat) {
    131   try {
    132     const startlist = await sql`
    133       insert into startlist (
    134         athlete,
    135         heat
    136       )
    137       values (
    138         ${athlete},
    139         ${heat}
    140       )
    141       returning *
    142     `
    143     return startlist
    144   } catch (error) {
    145     console.error('Error occurred in addAthleteToHeat:', error);
    146     throw error
    147   }
    148 }
    149 
    150 async function addAthlete(nr, firstname, lastname, birthday, school) {
    151   try {
    152     const athlete = await sql`
    153       insert into athletes (
    154         nr,
    155         firstname,
    156         lastname,
    157         birthday,
    158         school
    159       )
    160       values (
    161         ${nr},
    162         ${firstname},
    163         ${lastname},
    164         ${birthday},
    165         ${school}
    166       )
    167       returning *
    168     `
    169     return athlete
    170   } catch (error) {
    171     console.error('Error occurred in addAthlete:', error);
    172     throw error
    173   }
    174 }
    175 
    176 async function removeAthlete(id) {
    177   try {
    178     const athlete = await sql`
    179       delete from athletes where id = ${id}
    180       returning *
    181     `
    182     return athlete
    183   } catch (error) {
    184     console.error('Error occurred in removeAthlete:', error);
    185     throw error
    186   }
    187 }
    188 
    189 async function removeAthleteFromHeat(startlistId) {
    190   try {
    191     const startlist = await sql`
    192       delete from startlist
    193       where id = ${startlistId}
    194       returning *
    195     `
    196     return startlist
    197   } catch (error) {
    198     console.error('Error occurred in removeAthleteFromHeat:', error);
    199     throw error
    200   }
    201 }
    202 
    203 async function newHeat(name, heatLocation, plannedStart) {
    204   try {
    205     const heat = await sql`
    206       insert into heats (
    207         name,
    208         location,
    209         planned_start
    210       )
    211       values (
    212         ${name},
    213         ${heatLocation},
    214         ${plannedStart}
    215       )
    216       returning *
    217     `
    218     return heat
    219   } catch (error) {
    220     console.error('Error occurred in newHeat:', error);
    221     throw error
    222   }
    223 }
    224 
    225 async function removeHeat(heatId) {
    226   try {
    227     const heat = await sql`
    228       delete from heats where id = ${heatId}
    229       returning *
    230     `
    231     return heat
    232   } catch (error) {
    233     console.error('Error occurred in removeHeat:', error);
    234     throw error
    235   }
    236 }
    237 
    238 async function distinctStartlist(heatIds) {
    239   try {
    240     const startlist = await sql`
    241       select
    242         id,
    243         athlete,
    244         nr,
    245         firstname,
    246         lastname,
    247         birthday,
    248         school
    249       from distinct_startlist(${heatIds}) as athlete
    250     `
    251     return startlist
    252   } catch (error) {
    253     console.error('Error occurred in distinctStartlist:', error);
    254     throw error
    255   }
    256 }
    257 
    258 async function startlistWithAthletes(heatId) {
    259   try {
    260     // left outer join, to fetch heats with no athletes
    261     // https://www.postgresql.org/docs/current/tutorial-join.html
    262     const startlist = await sql`
    263       select
    264         s.id as startlist_id,
    265         s.heat,
    266         s.athlete,
    267         a.id as athlete_id,
    268         a.nr,
    269         a.firstname,
    270         a.lastname,
    271         a.birthday,
    272         a.school
    273       from startlist as s
    274       left outer join athletes as a
    275       on s.athlete = a.id
    276       where s.heat = ${heatId}
    277     `
    278     return startlist
    279   } catch (error) {
    280     console.error('Error occurred in startlistWithAthletes:', error);
    281     throw error
    282   }
    283 }
    284 
    285 async function scoresForHeatAndAthlete(heat, athlete) {
    286   try {
    287     const score = await sql`
    288       select
    289         id,
    290         athlete,
    291         judge,
    292         score
    293       from scores where heat = ${heat} and athlete = ${athlete}
    294     `
    295     return score
    296   } catch (error) {
    297     console.error('Error occurred in scoresForHeatAndAthlete:', error);
    298     throw error
    299   }
    300 }
    301 
    302 async function scoreSummaryForHeatAndAthlete(heat, athlete) {
    303   try {
    304     const summary = await sql`
    305       select score_summary
    306       from score_summary where heat_id = ${heat} and athlete_id = ${athlete}
    307     `
    308     return summary
    309   } catch (error) {
    310     console.error('Error occurred in scoreSummaryForHeatAndAthlete:', error);
    311     throw error
    312   }
    313 }
    314 
    315 async function getScore(heat, athlete, judge) {
    316   try {
    317     const scores = await sql`
    318       select * from scores
    319       where heat = ${heat} and athlete = ${athlete} and judge = ${judge}
    320     `
    321     return scores
    322   } catch (error) {
    323     console.error('Error occurred in getScore:', error);
    324     throw error
    325   }
    326 }
    327 
    328 // "upsert" score (https://www.postgresql.org/docs/current/sql-insert.html)
    329 // For ON CONFLICT DO UPDATE, a conflict_target must be provided.
    330 async function setScore(heat, athlete, judge, score) {
    331   try {
    332     const scores = await sql`
    333       insert into public.scores as s (athlete, judge, score, heat)
    334         values (${athlete}, ${judge}, ${score}, ${heat})
    335       on conflict (athlete, judge, heat) do update
    336       set score = ${score}
    337       where s.heat = ${heat} and s.athlete = ${athlete} and s.judge = ${judge}
    338       returning *
    339     `
    340     return scores
    341   } catch (error) {
    342     console.error('Error occurred in setScore:', error);
    343     throw error
    344   }
    345 }
    346 
    347 async function getSetting(name) {
    348   try {
    349     const setting = await sql`
    350       select * from settings
    351       where name = ${name}
    352     `
    353     return setting
    354   } catch (error) {
    355     console.error('Error occurred in getSetting:', error);
    356     throw error
    357   }
    358 }
    359 
    360 async function allSettings() {
    361   try {
    362     const settings = await sql`
    363       select * from settings
    364     `
    365     return settings
    366   } catch (error) {
    367     console.error('Error occurred in allSettings:', error);
    368     throw error
    369   }
    370 }
    371 
    372 async function removeSetting(name) {
    373   try {
    374     const setting = await sql`
    375       delete from settings where name = ${name}
    376       returning *
    377     `
    378     return setting
    379   } catch (error) {
    380     console.error('Error occurred in removeSetting:', error);
    381     throw error
    382   }
    383 }
    384 
    385 // "upsert" setting
    386 async function updateSetting(name, value) {
    387   try {
    388     const setting = await sql`
    389       insert into public.settings as s (name, value)
    390         values (${name}, ${value})
    391       on conflict (name) do update
    392       set value = ${value}
    393       where s.name = ${name}
    394       returning *
    395     `
    396     return setting
    397   } catch (error) {
    398     console.error('Error occurred in updateSetting:', error);
    399     throw error
    400   }
    401 }
    402 
    403 function removeClient(sock) {
    404   console.log("- Client removed")
    405   clients.delete(sock)
    406 }
    407 
    408 function addClient(sock) {
    409   console.log("+ Client added")
    410   clients.add(sock)
    411 }
    412 
    413 // add client to subscription
    414 async function watchScores(sock) {
    415   if (scoreSubscription !== null) {
    416     // we are already subscribed to this publication
    417     return scoreSubscription
    418   }
    419 
    420   // subscribe to score publication
    421   try {
    422     scoreSubscription = await sql.subscribe(
    423       '*:scores',
    424       (row, { command, relation, key, old }) => {
    425         // distributed score updates to all connected clients
    426         for(let c of clients) {
    427           c.send(JSON.stringify({
    428             "message": "Received new scores",
    429             "data": row,
    430           }))
    431         }
    432       },
    433       () => {
    434         // callback on initial connect and potential reconnects
    435         console.log("~ Start watching scores, created scoreSubscription")
    436       }
    437     )
    438     return scoreSubscription
    439   } catch (error) {
    440     console.error('Error occurred in watchScores:', error);
    441     throw error
    442   }
    443 }
    444 
    445 module.exports = {
    446   sql,
    447   invalidateToken,
    448   lookupToken,
    449   saveToken,
    450   getUser,
    451   allHeats,
    452   getHeat,
    453   allAthletes,
    454   newHeat,
    455   removeHeat,
    456   distinctStartlist,
    457   startlistWithAthletes,
    458   scoresForHeatAndAthlete,
    459   scoreSummaryForHeatAndAthlete,
    460   getScore,
    461   setScore,
    462   watchScores,
    463   removeClient,
    464   addClient,
    465   addAthleteToHeat,
    466   addAthlete,
    467   removeAthlete,
    468   removeAthleteFromHeat,
    469   getSetting,
    470   allSettings,
    471   updateSetting,
    472   removeSetting,
    473 }