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 }