commit
ce45ef847e
@ -0,0 +1 @@
|
||||
export DATABASE_URL="postgresql://ibihf2:ibihf@localhost/ibihf"
|
@ -0,0 +1 @@
|
||||
/target
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "ibihf"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.12" }
|
||||
axum-macros = "0.3.7"
|
||||
chrono = { version = "0.4.24", features = ["serde"] }
|
||||
serde = "1.0.158"
|
||||
serde_plain = "1.0.1"
|
||||
serde_tuple = "0.5.0"
|
||||
sql-builder = "3.1.1"
|
||||
sqlx = { version = "0.6.3", features = ["postgres", "runtime-tokio-rustls", "time", "chrono"] }
|
||||
static_assertions = "1.1.0"
|
||||
tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.2"
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE leagues;
|
@ -0,0 +1,7 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS leagues (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
start_date TIMESTAMPTZ NOT NULL,
|
||||
end_date TIMESTAMPTZ
|
||||
);
|
@ -0,0 +1 @@
|
||||
DROP TABLE divisions;
|
@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS divisions (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
league INTEGER NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
CONSTRAINT league_fk
|
||||
FOREIGN KEY(league)
|
||||
REFERENCES leagues(id)
|
||||
ON DELETE RESTRICT
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE IF EXISTS teams;
|
@ -0,0 +1,10 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
division INTEGER NOT NULL,
|
||||
CONSTRAINT division_fk
|
||||
FOREIGN KEY(division)
|
||||
REFERENCES divisions(id)
|
||||
ON DELETE RESTRICT
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE players;
|
@ -0,0 +1,7 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
height_cm INTEGER,
|
||||
weight_kg INTEGER
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE games;
|
@ -0,0 +1,17 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
-- a possibly null name for the game; this allows there to be special names like "Gold Medal Game"
|
||||
name VARCHAR(255),
|
||||
team_home INTEGER NOT NULL,
|
||||
team_away INTEGER NOT NULL,
|
||||
-- home and away teams need to actually be teams
|
||||
CONSTRAINT team_home_fk
|
||||
FOREIGN KEY(team_home)
|
||||
REFERENCES teams(id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT team_away_fk
|
||||
FOREIGN KEY(team_away)
|
||||
REFERENCES teams(id)
|
||||
ON DELETE RESTRICT
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DELETE FROM leagues WHERE id=1;
|
@ -0,0 +1,5 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO leagues
|
||||
(id, name, start_date, end_date)
|
||||
VALUES
|
||||
(1, '2022 Canadian National Blind Hockey Tournament', '2022-03-24', '2022-03-26');
|
@ -0,0 +1,2 @@
|
||||
DELETE FROM divisions
|
||||
WHERE id BETWEEN 1 AND 3;
|
@ -0,0 +1,6 @@
|
||||
INSERT INTO divisions
|
||||
(id, name, league)
|
||||
VALUES
|
||||
(1, 'Low Vision & Development Division', 1),
|
||||
(2, 'Open Division', 1),
|
||||
(3, 'Children Division', 1);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DELETE FROM teams WHERE id=1 OR id=2;
|
@ -0,0 +1,6 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO teams
|
||||
(id, name, division)
|
||||
VALUES
|
||||
(1, 'Bullseye', 1),
|
||||
(2, 'See Cats', 1);
|
@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DELETE FROM players
|
||||
WHERE id BETWEEN 1 AND 31;
|
@ -0,0 +1,35 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO players
|
||||
(id, name)
|
||||
VALUES
|
||||
(1, 'Tait Hoyem'),
|
||||
(2, 'Hillary Scanlon'),
|
||||
(3, 'Nelson Rego'),
|
||||
(4, 'Carrie Anton'),
|
||||
(5, 'Salamaan Chaudhri'),
|
||||
(6, 'Ben Ho Lung'),
|
||||
(7, 'Brian MacLean'),
|
||||
(8, 'Shannon Murphy'),
|
||||
(9, 'Joseph Robinson'),
|
||||
(10, 'Drexcyl Sison'),
|
||||
(11, 'Ginny Sweet'),
|
||||
(12, 'Catharine Lemay'),
|
||||
(13, 'Thomas Stewart'),
|
||||
(14, 'Maurice Clement-Lafrance'),
|
||||
(15, 'Jennifer Fancy'),
|
||||
(16, 'Allyssa Foulds'),
|
||||
(17, 'Ryan Kucy'),
|
||||
(18, 'Denis LeBlanc'),
|
||||
(19, 'Bob Lowe'),
|
||||
(20, 'Ted Moritsugu'),
|
||||
(21, 'Dave Poidevin'),
|
||||
(22, 'Jillian Stewart'),
|
||||
(23, 'Laura Mark'),
|
||||
(24, 'Matt Arnold'),
|
||||
(25, 'Rory Kucy'),
|
||||
(26, 'Jeff Stewart'),
|
||||
(27, 'Dylan Brown'),
|
||||
(28, 'Codi Isaac'),
|
||||
(29, 'Richard Isaac'),
|
||||
(30, 'Tyler McGuffin'),
|
||||
(31, 'Scarlette Dorn');
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE IF EXISTS positions;
|
@ -0,0 +1,9 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS positions (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
-- the short version, which should usually one character can be 2 charaters in some rare cases.
|
||||
-- for example, in Goalball you'd have L, R, and C.
|
||||
-- In hockey, you'd have C, D, LW and RW.
|
||||
short_name VARCHAR(2) NOT NULL
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DELETE FROM positions
|
||||
WHERE id BETWEEN 1 AND 7;
|
@ -0,0 +1,11 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO positions
|
||||
(id, name, short_name)
|
||||
VALUES
|
||||
(1, 'Center', 'C'),
|
||||
(2, 'Right-Wing', 'R'),
|
||||
(3, 'Left-Wing', 'L'),
|
||||
(4, 'Defence', 'D'),
|
||||
(5, 'Goalie', 'G'),
|
||||
(6, 'Head Coach', 'HC'),
|
||||
(7, 'Assistant Coach', 'AC');
|
@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS periods;
|
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS periods (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
-- "first", "second", "third", "second overtime", "shootout"
|
||||
name VARCHAR(32) NOT NULL,
|
||||
-- "1", "2", "3", "OT", "[2-9]OT", "SO"
|
||||
-- technically 10+OT would not work, but this should be rare enough to not worry about.
|
||||
short_name VARCHAR(3) NOT NULL
|
||||
);
|
@ -0,0 +1 @@
|
||||
DELETE FROM periods;
|
@ -0,0 +1,16 @@
|
||||
INSERT INTO periods
|
||||
(id, name, short_name)
|
||||
VALUES
|
||||
(1, 'first', '1'),
|
||||
(2, 'second', '2'),
|
||||
(3, 'third', '3'),
|
||||
(4, 'overtime', 'OT'),
|
||||
(5, 'shootout', 'SO'),
|
||||
(6, 'second overtime', '2OT'),
|
||||
(7, 'third overtime', '3OT'),
|
||||
(8, 'fourth overtime', '4OT'),
|
||||
(9, 'fifth overtime', '5OT'),
|
||||
(10, 'sixth overtime', '6OT'),
|
||||
(11, 'seventh overtime', '7OT'),
|
||||
(12, 'eighth overtime', '8OT'),
|
||||
(13, 'ninth overtime', '9OT');
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE IF EXISTS team_players;
|
@ -0,0 +1,21 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS team_players (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
team INTEGER NOT NULL,
|
||||
player INTEGER NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
-- not a foreign key
|
||||
player_number INTEGER NOT NULL,
|
||||
CONSTRAINT team_fk
|
||||
FOREIGN KEY(team)
|
||||
REFERENCES teams(id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT player_fk
|
||||
FOREIGN KEY(player)
|
||||
REFERENCES players(id)
|
||||
ON DELETE RESTRICT,
|
||||
CONSTRAINT position_fk
|
||||
FOREIGN KEY(position)
|
||||
REFERENCES positions(id)
|
||||
ON DELETE RESTRICT
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DELETE FROM team_players;
|
@ -0,0 +1,178 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO team_players
|
||||
(team, player, position, player_number)
|
||||
VALUES
|
||||
(
|
||||
1,
|
||||
31,
|
||||
1,
|
||||
11
|
||||
),
|
||||
(
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
3
|
||||
),
|
||||
(
|
||||
1,
|
||||
2,
|
||||
4,
|
||||
8
|
||||
),
|
||||
(
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
1
|
||||
),
|
||||
(
|
||||
1,
|
||||
4,
|
||||
2,
|
||||
14
|
||||
),
|
||||
(
|
||||
1,
|
||||
5,
|
||||
4,
|
||||
91
|
||||
),
|
||||
(
|
||||
1,
|
||||
7,
|
||||
1,
|
||||
15
|
||||
),
|
||||
(
|
||||
1,
|
||||
8,
|
||||
4,
|
||||
10
|
||||
),
|
||||
(
|
||||
1,
|
||||
9,
|
||||
3,
|
||||
13
|
||||
),
|
||||
(
|
||||
1,
|
||||
10,
|
||||
1,
|
||||
10
|
||||
),
|
||||
(
|
||||
1,
|
||||
11,
|
||||
2,
|
||||
84
|
||||
),
|
||||
(
|
||||
2,
|
||||
12,
|
||||
5,
|
||||
35
|
||||
),
|
||||
(
|
||||
2,
|
||||
13,
|
||||
5,
|
||||
30
|
||||
),
|
||||
(
|
||||
2,
|
||||
14,
|
||||
1,
|
||||
15
|
||||
),
|
||||
(
|
||||
2,
|
||||
15,
|
||||
2,
|
||||
17
|
||||
),
|
||||
(
|
||||
2,
|
||||
16,
|
||||
1,
|
||||
3
|
||||
),
|
||||
(
|
||||
2,
|
||||
17,
|
||||
2,
|
||||
9
|
||||
),
|
||||
(
|
||||
2,
|
||||
18,
|
||||
4,
|
||||
16
|
||||
),
|
||||
(
|
||||
2,
|
||||
19,
|
||||
4,
|
||||
4
|
||||
),
|
||||
(
|
||||
2,
|
||||
21,
|
||||
4,
|
||||
14
|
||||
),
|
||||
(
|
||||
2,
|
||||
22,
|
||||
4,
|
||||
12
|
||||
),
|
||||
(
|
||||
2,
|
||||
23,
|
||||
2,
|
||||
10
|
||||
),
|
||||
(
|
||||
2,
|
||||
24,
|
||||
7,
|
||||
0
|
||||
),
|
||||
(
|
||||
2,
|
||||
25,
|
||||
7,
|
||||
0
|
||||
),
|
||||
(
|
||||
2,
|
||||
26,
|
||||
7,
|
||||
0
|
||||
),
|
||||
(
|
||||
1,
|
||||
27,
|
||||
7,
|
||||
0
|
||||
),
|
||||
(
|
||||
1,
|
||||
28,
|
||||
7,
|
||||
0
|
||||
),
|
||||
(
|
||||
1,
|
||||
29,
|
||||
7,
|
||||
0
|
||||
),
|
||||
(
|
||||
1,
|
||||
30,
|
||||
7,
|
||||
0
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
DELETE FROM games
|
||||
WHERE id BETWEEN 1 AND 4;
|
@ -0,0 +1,27 @@
|
||||
INSERT INTO games
|
||||
(id, name, team_home, team_away)
|
||||
VALUES
|
||||
(
|
||||
1,
|
||||
'Game 1',
|
||||
1, -- Bullseye
|
||||
2 -- Seecats
|
||||
),
|
||||
(
|
||||
2,
|
||||
'Game 2',
|
||||
1, -- Bullseye
|
||||
2 -- Seecats
|
||||
),
|
||||
(
|
||||
3,
|
||||
'Game 3',
|
||||
1, -- Bullseye
|
||||
2 -- Seecats
|
||||
),
|
||||
(
|
||||
4,
|
||||
'Game 4',
|
||||
1, -- Bullseye
|
||||
2 -- Seecats
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE IF EXISTS shots;
|
@ -0,0 +1,68 @@
|
||||
-- Add up migration script here
|
||||
CREATE TABLE IF NOT EXISTS shots (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
|
||||
-- video timestampt if known; seconds offset from beginning of video
|
||||
video_timestamp INTEGER,
|
||||
-- player that blocked the shot, if applicable
|
||||
blocker INTEGER,
|
||||
-- on net; did it go towards the goalie (this does not say whether it went in or not)
|
||||
on_net BOOLEAN NOT NULL,
|
||||
-- did the puck go in?
|
||||
goal BOOLEAN NOT NULL,
|
||||
-- what team was the shooter on
|
||||
shooter_team INTEGER NOT NULL,
|
||||
-- which player is the shooter
|
||||
shooter INTEGER NOT NULL,
|
||||
-- which player was the goalie
|
||||
goalie INTEGER NOT NULL,
|
||||
-- which game was this a part of
|
||||
game INTEGER NOT NULL,
|
||||
-- which period did the shot happen in
|
||||
period INTEGER NOT NULL,
|
||||
-- when did the shot happen relative to the beginning of the period
|
||||
period_time INTEGER NOT NULL,
|
||||
-- if applicable, set assistant(s)
|
||||
assistant INTEGER,
|
||||
assistant_second INTEGER,
|
||||
-- was the shooter a real player
|
||||
CONSTRAINT shooter_fk
|
||||
FOREIGN KEY(shooter)
|
||||
REFERENCES players(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- was the assistant is a real player
|
||||
CONSTRAINT assistant_fk
|
||||
FOREIGN KEY(assistant)
|
||||
REFERENCES players(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- was the second assistant a real player
|
||||
CONSTRAINT assistant_second_fk
|
||||
FOREIGN KEY(assistant_second)
|
||||
REFERENCES players(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- was the goalie a real player
|
||||
CONSTRAINT goalie_fk
|
||||
FOREIGN KEY(goalie)
|
||||
REFERENCES players(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- was the (optional) blocker a real player
|
||||
CONSTRAINT blocker_fk
|
||||
FOREIGN KEY(blocker)
|
||||
REFERENCES players(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- was the shooter's team a real team
|
||||
CONSTRAINT shooter_team_fk
|
||||
FOREIGN KEY(shooter_team)
|
||||
REFERENCES teams(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- is the game a real game
|
||||
CONSTRAINT game_fk
|
||||
FOREIGN KEY(game)
|
||||
REFERENCES games(id)
|
||||
ON DELETE RESTRICT,
|
||||
-- is the period refgerences a real period type
|
||||
CONSTRAINT period_fk
|
||||
FOREIGN KEY(period)
|
||||
REFERENCES periods(id)
|
||||
ON DELETE RESTRICT
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DELETE FROM shots;
|
@ -0,0 +1,186 @@
|
||||
-- Add up migration script here
|
||||
INSERT INTO shots
|
||||
(shooter_team, goalie, shooter, game, period, period_time, video_timestamp, assistant, assistant_second, on_net, goal)
|
||||
VALUES
|
||||
(
|
||||
2,
|
||||
3,
|
||||
14,
|
||||
1,
|
||||
3,
|
||||
503,
|
||||
null,
|
||||
16,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
12,
|
||||
7,
|
||||
1,
|
||||
3,
|
||||
26,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
13,
|
||||
31,
|
||||
2,
|
||||
2,
|
||||
1186,
|
||||
1649,
|
||||
2,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
2,
|
||||
3,
|
||||
22,
|
||||
3,
|
||||
1,
|
||||
657,
|
||||
1268,
|
||||
21,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
13,
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
565,
|
||||
1360,
|
||||
7,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
12,
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
282,
|
||||
3918,
|
||||
1,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
2,
|
||||
3,
|
||||
18,
|
||||
4,
|
||||
1,
|
||||
691,
|
||||
663,
|
||||
1,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
12,
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
106,
|
||||
1247,
|
||||
31,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
13,
|
||||
2,
|
||||
4,
|
||||
2,
|
||||
883,
|
||||
1862,
|
||||
7,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
13,
|
||||
2,
|
||||
4,
|
||||
2,
|
||||
613,
|
||||
2132,
|
||||
1,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
1,
|
||||
13,
|
||||
2,
|
||||
4,
|
||||
2,
|
||||
514,
|
||||
2231,
|
||||
31,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
2,
|
||||
3,
|
||||
14,
|
||||
4,
|
||||
3,
|
||||
1193,
|
||||
2934,
|
||||
16,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
2,
|
||||
3,
|
||||
16,
|
||||
4,
|
||||
3,
|
||||
972,
|
||||
3154,
|
||||
14,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
2,
|
||||
3,
|
||||
22,
|
||||
4,
|
||||
3,
|
||||
16,
|
||||
4232,
|
||||
16,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
);
|
@ -0,0 +1,2 @@
|
||||
-- Add down migration script here
|
||||
DROP FUNCTION IF EXISTS player_stats_overview_all;
|
@ -0,0 +1,29 @@
|
||||
-- Add up migration script here
|
||||
CREATE OR REPLACE FUNCTION player_stats_overview_all() RETURNS VOID
|
||||
LANGUAGE SQL
|
||||
AS $$
|
||||
SELECT
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM shots
|
||||
WHERE shooter=players.id
|
||||
AND goal=true
|
||||
) AS goals,
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM shots
|
||||
WHERE assistant=players.id
|
||||
AND goal=true
|
||||
) AS assists,
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM shots
|
||||
WHERE assistant=players.id
|
||||
OR shooter=players.id
|
||||
) AS points,
|
||||
players.name AS player_name
|
||||
FROM players
|
||||
ORDER BY
|
||||
points DESC,
|
||||
players.name;
|
||||
$$;
|
@ -0,0 +1,9 @@
|
||||
use sqlx::{Postgres, Pool};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
pub async fn connect() -> Pool<Postgres> {
|
||||
PgPoolOptions::new()
|
||||
.max_connections(8)
|
||||
.connect("postgres://ibihf2:ibihf@localhost/ibihf").await
|
||||
.unwrap()
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
mod db;
|
||||
mod model;
|
||||
|
||||
use crate::model::{
|
||||
TableName,
|
||||
League,
|
||||
Team,
|
||||
Division,
|
||||
TeamPlayer,
|
||||
Player,
|
||||
Shot,
|
||||
};
|
||||
|
||||
use sqlx::{
|
||||
Postgres,
|
||||
Pool,
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
http::StatusCode,
|
||||
extract::{
|
||||
Path,
|
||||
State,
|
||||
},
|
||||
response::{
|
||||
Json,
|
||||
IntoResponse,
|
||||
},
|
||||
routing::get,
|
||||
};
|
||||
use axum_macros::debug_handler;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerState {
|
||||
db_pool: Arc<Pool<Postgres>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let pool = db::connect().await;
|
||||
let state = ServerState {
|
||||
db_pool: Arc::new(pool),
|
||||
};
|
||||
let router = Router::new()
|
||||
.route("/league/", get(league_all))
|
||||
.route("/league/:id", get(league_id))
|
||||
.route("/division/", get(division_all))
|
||||
.route("/division/:id", get(division_id))
|
||||
.route("/team/", get(team_all))
|
||||
.route("/team/:id", get(team_id))
|
||||
.route("/player/", get(player_all))
|
||||
.route("/player/:id", get(player_id))
|
||||
.route("/team-player/", get(team_player_all))
|
||||
.route("/team-playerplayer/:id", get(team_player_id))
|
||||
.route("/shot/", get(shots_all))
|
||||
.route("/shot/:id", get(shots_id))
|
||||
.with_state(state);
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
|
||||
println!("Listening on {}", addr);
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
async fn get_all<T: Send + Unpin + TableName + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool) -> Result<Vec<T>, sqlx::Error> {
|
||||
sqlx::query_as::<_, T>(
|
||||
&format!("SELECT * FROM {};", <T as TableName>::TABLE_NAME)
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
async fn get_by_id<T: Send + Unpin + TableName + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool, id: i32) -> Result<Option<T>, sqlx::Error> {
|
||||
sqlx::query_as::<_, T>(
|
||||
&format!("SELECT * FROM {} WHERE id = $1;", <T as TableName>::TABLE_NAME)
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
/*
|
||||
async fn insert_into<T: Sync + Send + Unpin + TableName + for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow>>(pool: &sqlx::PgPool, new: &T) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
|
||||
let query = sql_builder::SqlBuilder::insert_into(<T as TableName>::TABLE_NAME)
|
||||
.values(())
|
||||
.sql().unwrap();
|
||||
sqlx::query(
|
||||
&query
|
||||
)
|
||||
.execute(pool)
|
||||
.await
|
||||
}
|
||||
*/
|
||||
|
||||
macro_rules! get_all {
|
||||
($crud_struct:ident, $func_name:ident) => {
|
||||
#[debug_handler]
|
||||
async fn $func_name(State(server_config): State<ServerState>) -> impl IntoResponse {
|
||||
let cruder = get_all::<$crud_struct>(&server_config.db_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
(StatusCode::OK, Json(cruder))
|
||||
}
|
||||
}
|
||||
}
|
||||
macro_rules! get_by_id {
|
||||
($crud_struct:ident, $func_name:ident) => {
|
||||
#[debug_handler]
|
||||
async fn $func_name(State(server_config): State<ServerState>, Path(id): Path<i32>) -> impl IntoResponse {
|
||||
let cruder = get_by_id::<$crud_struct>(&server_config.db_pool, id)
|
||||
.await
|
||||
.unwrap();
|
||||
(StatusCode::OK, Json(cruder))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
macro_rules! insert {
|
||||
($crud_struct:ident, $func_name:ident) => {
|
||||
#[debug_handler]
|
||||
async fn $func_name(State(server_config): State<ServerState>, Json(NewPlayer): Json<NewPlayer>) -> impl IntoResponse {
|
||||
let cruder = get_by_id::<$crud_struct>(&server_config.db_pool, id)
|
||||
.await
|
||||
.unwrap();
|
||||
(StatusCode::OK, Json(cruder))
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
macro_rules! impl_all_query_types {
|
||||
($ty:ident, $func_all:ident, $func_by_id:ident) => {
|
||||
get_all!($ty, $func_all);
|
||||
get_by_id!($ty, $func_by_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl_all_query_types!(
|
||||
TeamPlayer,
|
||||
team_player_all,
|
||||
team_player_id
|
||||
);
|
||||
impl_all_query_types!(
|
||||
Player,
|
||||
player_all,
|
||||
player_id
|
||||
);
|
||||
impl_all_query_types!(
|
||||
Team,
|
||||
team_all,
|
||||
team_id
|
||||
);
|
||||
impl_all_query_types!(
|
||||
Shot,
|
||||
shots_all,
|
||||
shots_id
|
||||
);
|
||||
impl_all_query_types!(
|
||||
Division,
|
||||
division_all,
|
||||
division_id
|
||||
);
|
||||
impl_all_query_types!(
|
||||
League,
|
||||
league_all,
|
||||
league_id
|
||||
);
|
||||
|
@ -0,0 +1,261 @@
|
||||
use sqlx::FromRow;
|
||||
use sqlx::PgPool;
|
||||
use sqlx::types::chrono::{DateTime, Utc};
|
||||
use chrono::serde::{ts_seconds, ts_seconds_option};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
pub trait TableName {
|
||||
const TABLE_NAME: &'static str;
|
||||
}
|
||||
macro_rules! impl_table_name {
|
||||
($ty:ident, $tname:expr) => {
|
||||
impl TableName for $ty {
|
||||
const TABLE_NAME: &'static str = $tname;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct League {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub start_date: DateTime<Utc>,
|
||||
#[serde(with = "ts_seconds_option")]
|
||||
pub end_date: Option<DateTime<Utc>>,
|
||||
}
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct NewLeague {
|
||||
pub name: String,
|
||||
#[serde(with = "ts_seconds")]
|
||||
pub start_date: DateTime<Utc>,
|
||||
#[serde(with = "ts_seconds_option")]
|
||||
pub end_date: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct Division {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub league: i32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct NewDivision {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub league: i32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct Team {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub division: i32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct NewTeam {
|
||||
pub name: String,
|
||||
pub division: i32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct Player {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub weight_kg: Option<i32>,
|
||||
pub height_cm: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize)]
|
||||
pub struct NewPlayer {
|
||||
pub name: String,
|
||||
pub weight_kg: Option<i32>,
|
||||
pub height_cm: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize)]
|
||||
pub struct Shot {
|
||||
pub id: i32,
|
||||
pub shooter_team: i32,
|
||||
pub goalie: i32,
|
||||
pub assistant: Option<i32>,
|
||||
pub game: i32,
|
||||
pub period: i32,
|
||||
pub period_time: i32,
|
||||
pub video_timestamp: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize)]
|
||||
pub struct TeamPlayer {
|
||||
pub id: i32,
|
||||
pub team: i32,
|
||||
pub player: i32,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize, Debug)]
|
||||
pub struct Notification {
|
||||
pub scorer_name: String,
|
||||
pub scorer_number: i32,
|
||||
pub position: String,
|
||||
pub scorer_team_name: String,
|
||||
pub period_name: String,
|
||||
pub period_time_left: i32,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Deserialize, Serialize, Debug)]
|
||||
pub struct PlayerStats {
|
||||
pub player_name: String,
|
||||
pub goals: i64,
|
||||
pub assists: i64,
|
||||
pub points: i64,
|
||||
}
|
||||
|
||||
async fn get_player_stats_overview(pool: PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
|
||||
let query = r#"
|
||||
SELECT
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM shots
|
||||
WHERE shooter=players.id
|
||||
AND goal=true
|
||||
) AS goals,
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM shots
|
||||
WHERE assistant=players.id
|
||||
AND goal=true
|
||||
) AS assists,
|
||||
(
|
||||
SELECT COUNT(id)
|
||||
FROM shots
|
||||
WHERE assistant=players.id
|
||||
OR shooter=players.id
|
||||
) AS points,
|
||||
players.name AS player_name
|
||||
FROM players
|
||||
ORDER BY
|
||||
points DESC,
|
||||
players.name;
|
||||
"#;
|
||||
let result = sqlx::query_as::<_, PlayerStats>(query)
|
||||
.fetch_all(&pool)
|
||||
.await;
|
||||
result
|
||||
}
|
||||
|
||||
impl_table_name!(TeamPlayer, "team_players");
|
||||
impl_table_name!(Player, "players");
|
||||
impl_table_name!(League, "leagues");
|
||||
impl_table_name!(Division, "divisions");
|
||||
impl_table_name!(Team, "teams");
|
||||
impl_table_name!(Shot, "shots");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use crate::model::{
|
||||
TeamPlayer,
|
||||
Player,
|
||||
League,
|
||||
Division,
|
||||
Team,
|
||||
Shot,
|
||||
TableName,
|
||||
Notification,
|
||||
get_player_stats_overview,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn check_player_overall_stats() {
|
||||
tokio_test::block_on(async move {
|
||||
let pool = db_connect().await;
|
||||
let players_stats = get_player_stats_overview(pool).await.unwrap();
|
||||
for player_stats in players_stats {
|
||||
println!("{player_stats:?}");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_notification_query() {
|
||||
tokio_test::block_on(async move {
|
||||
let pool = db_connect().await;
|
||||
let query = r#"
|
||||
SELECT
|
||||
teams.name AS scorer_team_name,
|
||||
players.name AS scorer_name,
|
||||
positions.name AS position,
|
||||
team_players.player_number AS scorer_number,
|
||||
shots.period_time AS period_time_left,
|
||||
periods.name AS period_name
|
||||
FROM
|
||||
shots
|
||||
JOIN teams ON teams.id=shots.shooter_team
|
||||
JOIN players ON players.id=shots.shooter
|
||||
JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id
|
||||
JOIN periods ON periods.id=shots.period
|
||||
JOIN positions ON positions.id=team_players.position;
|
||||
"#;
|
||||
let result = sqlx::query_as::<_, Notification>(query)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let minutes = result.period_time_left / 60;
|
||||
let seconds = result.period_time_left % 60;
|
||||
println!("{0} {1} player #{3} {2} has scored! Time of the goal: {4}:{5} in the {6}",
|
||||
result.scorer_team_name,
|
||||
result.position,
|
||||
result.scorer_name,
|
||||
result.scorer_number,
|
||||
minutes,
|
||||
seconds,
|
||||
result.period_name
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// A simple function to connect to the database.
|
||||
async fn db_connect() -> sqlx::PgPool {
|
||||
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set to run tests.");
|
||||
sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.expect("Active database connection must be made")
|
||||
}
|
||||
|
||||
/// This macro generates a test that will `SELECT` all records for a table.
|
||||
/// Then, it checks that
|
||||
/// 1. The table rows gets deserialized correctly.
|
||||
/// 2. There is at least one row.
|
||||
macro_rules! generate_select_test {
|
||||
($ret_type:ident, $func_name:ident) => {
|
||||
#[test]
|
||||
fn $func_name() {
|
||||
tokio_test::block_on(async move {
|
||||
let pool = db_connect().await;
|
||||
let results = sqlx::query_as::<_, $ret_type>(
|
||||
&format!(
|
||||
"SELECT * FROM {};",
|
||||
<$ret_type as TableName>::TABLE_NAME
|
||||
)
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
// check that there is at least one result item
|
||||
assert!(results.len() > 0, "There must be at least one result in the table.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
generate_select_test!(TeamPlayer, select_team_player);
|
||||
generate_select_test!(Player, select_player);
|
||||
generate_select_test!(League, select_league);
|
||||
generate_select_test!(Division, select_division);
|
||||
generate_select_test!(Team, select_team);
|
||||
generate_select_test!(Shot, select_shot);
|
||||
}
|
Loading…
Reference in new issue