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