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 }