From a6f141e8414f021de2b3b5dd2a413c10832568c9 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 17:31:42 -0500 Subject: [PATCH 1/9] Add time feature to Diesel --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2a07e..a1c721f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -747,6 +747,7 @@ dependencies = [ "itoa", "pq-sys", "r2d2", + "time", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5952557..c15944b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [ "BsSkipEndFill" ] } dotenv = { version = "0.15.0", optional = true } -diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } +diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional = true } lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } From 0924c954b800cdf27f9a773f1d07748e555afaf8 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:37:55 -0500 Subject: [PATCH 2/9] Create artists table --- .../2024-02-06-145714_create_artists_table/down.sql | 1 + .../2024-02-06-145714_create_artists_table/up.sql | 4 ++++ src/schema.rs | 12 ++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 migrations/2024-02-06-145714_create_artists_table/down.sql create mode 100644 migrations/2024-02-06-145714_create_artists_table/up.sql diff --git a/migrations/2024-02-06-145714_create_artists_table/down.sql b/migrations/2024-02-06-145714_create_artists_table/down.sql new file mode 100644 index 0000000..943c085 --- /dev/null +++ b/migrations/2024-02-06-145714_create_artists_table/down.sql @@ -0,0 +1 @@ +DROP TABLE artists; diff --git a/migrations/2024-02-06-145714_create_artists_table/up.sql b/migrations/2024-02-06-145714_create_artists_table/up.sql new file mode 100644 index 0000000..73802e6 --- /dev/null +++ b/migrations/2024-02-06-145714_create_artists_table/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE artists ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + name VARCHAR NOT NULL +); diff --git a/src/schema.rs b/src/schema.rs index 2e9b462..58b691a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,12 @@ // @generated automatically by Diesel CLI. +diesel::table! { + artists (id) { + id -> Int4, + name -> Varchar, + } +} + diesel::table! { users (id) { id -> Int4, @@ -9,3 +16,8 @@ diesel::table! { created_at -> Timestamp, } } + +diesel::allow_tables_to_appear_in_same_query!( + artists, + users, +); From bf99dac25cb441b06162dcd20bb175f86615d2a5 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:38:18 -0500 Subject: [PATCH 3/9] Create albums table --- .../down.sql | 2 ++ .../up.sql | 13 ++++++++++++ src/schema.rs | 20 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 migrations/2024-02-06-150214_create_albums_table/down.sql create mode 100644 migrations/2024-02-06-150214_create_albums_table/up.sql diff --git a/migrations/2024-02-06-150214_create_albums_table/down.sql b/migrations/2024-02-06-150214_create_albums_table/down.sql new file mode 100644 index 0000000..31baf23 --- /dev/null +++ b/migrations/2024-02-06-150214_create_albums_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE album_artists; +DROP TABLE albums; diff --git a/migrations/2024-02-06-150214_create_albums_table/up.sql b/migrations/2024-02-06-150214_create_albums_table/up.sql new file mode 100644 index 0000000..154bab2 --- /dev/null +++ b/migrations/2024-02-06-150214_create_albums_table/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE albums ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + title VARCHAR NOT NULL, + release_date DATE +); + +-- A table to store artists for each album +-- Needed because an album can have multiple artists, but in Postgres we can't store an array of foreign keys +CREATE TABLE album_artists ( + album_id INTEGER REFERENCES albums(id) ON DELETE CASCADE NOT NULL, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE NULL, + PRIMARY KEY (album_id, artist_id) +); diff --git a/src/schema.rs b/src/schema.rs index 58b691a..1029c02 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,20 @@ // @generated automatically by Diesel CLI. +diesel::table! { + album_artists (album_id, artist_id) { + album_id -> Int4, + artist_id -> Int4, + } +} + +diesel::table! { + albums (id) { + id -> Int4, + title -> Varchar, + release_date -> Nullable, + } +} + diesel::table! { artists (id) { id -> Int4, @@ -17,7 +32,12 @@ diesel::table! { } } +diesel::joinable!(album_artists -> albums (album_id)); +diesel::joinable!(album_artists -> artists (artist_id)); + diesel::allow_tables_to_appear_in_same_query!( + album_artists, + albums, artists, users, ); From 319958f26470720fb9f49e9aedceceb343f810cd Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:38:36 -0500 Subject: [PATCH 4/9] Create songs table --- .../down.sql | 2 ++ .../up.sql | 16 ++++++++++++ src/schema.rs | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 migrations/2024-02-06-150334_create_songs_table/down.sql create mode 100644 migrations/2024-02-06-150334_create_songs_table/up.sql diff --git a/migrations/2024-02-06-150334_create_songs_table/down.sql b/migrations/2024-02-06-150334_create_songs_table/down.sql new file mode 100644 index 0000000..b5ef474 --- /dev/null +++ b/migrations/2024-02-06-150334_create_songs_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE song_artists; +DROP TABLE songs; diff --git a/migrations/2024-02-06-150334_create_songs_table/up.sql b/migrations/2024-02-06-150334_create_songs_table/up.sql new file mode 100644 index 0000000..91249a1 --- /dev/null +++ b/migrations/2024-02-06-150334_create_songs_table/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE songs ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + title VARCHAR NOT NULL, + album_id INTEGER REFERENCES albums(id), + track INTEGER, + duration INTEGER NOT NULL, + release_date DATE, + storage_path VARCHAR NOT NULL, + image_path VARCHAR +); + +CREATE TABLE song_artists ( + song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE NOT NULL, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE NOT NULL, + PRIMARY KEY (song_id, artist_id) +); diff --git a/src/schema.rs b/src/schema.rs index 1029c02..b98d736 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -22,6 +22,26 @@ diesel::table! { } } +diesel::table! { + song_artists (song_id, artist_id) { + song_id -> Int4, + artist_id -> Int4, + } +} + +diesel::table! { + songs (id) { + id -> Int4, + title -> Varchar, + album_id -> Nullable, + track -> Nullable, + duration -> Int4, + release_date -> Nullable, + storage_path -> Varchar, + image_path -> Nullable, + } +} + diesel::table! { users (id) { id -> Int4, @@ -34,10 +54,15 @@ diesel::table! { diesel::joinable!(album_artists -> albums (album_id)); diesel::joinable!(album_artists -> artists (artist_id)); +diesel::joinable!(song_artists -> artists (artist_id)); +diesel::joinable!(song_artists -> songs (song_id)); +diesel::joinable!(songs -> albums (album_id)); diesel::allow_tables_to_appear_in_same_query!( album_artists, albums, artists, + song_artists, + songs, users, ); From 79add82c2da3171e8167f2c58ae9ff47e62126d6 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:40:02 -0500 Subject: [PATCH 5/9] Create artist model --- src/models.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/models.rs b/src/models.rs index 2daf3ae..f336c79 100644 --- a/src/models.rs +++ b/src/models.rs @@ -43,3 +43,16 @@ pub struct NewUser { /// The user's password, stored as a hash pub password: String, } + +/// Model for an artist +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Artist { + /// A unique id for the artist + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The artist's name + pub name: String, +} + From 577090aa1ae292914c3e99affa0e8ea9da80a6bf Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:40:38 -0500 Subject: [PATCH 6/9] Add time package --- Cargo.lock | 17 +++++++++++++---- Cargo.toml | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1c721f..7122c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "leptos_router", "openssl", "serde", + "time", "wasm-bindgen", ] @@ -1666,6 +1667,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "object" version = "0.32.2" @@ -2458,12 +2465,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2478,10 +2486,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] diff --git a/Cargo.toml b/Cargo.toml index c15944b..25e3b1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } +time = "0.3.34" [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] From 8638265fa3cecadf03684ca6647c9e0c5ad52b8f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:41:28 -0500 Subject: [PATCH 7/9] Create album model --- src/models.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/models.rs b/src/models.rs index f336c79..74c4ae7 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,5 @@ use std::time::SystemTime; +use time::Date; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] @@ -56,3 +57,17 @@ pub struct Artist { pub name: String, } +/// Model for an album +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Album { + /// A unique id for the album + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The album's title + pub title: String, + /// The album's release date + pub release_date: Option, +} + From a6556b7a98b0a812cdddc8a23a81d54990e237e5 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:41:37 -0500 Subject: [PATCH 8/9] Create song model --- src/models.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/models.rs b/src/models.rs index 74c4ae7..d71fc6c 100644 --- a/src/models.rs +++ b/src/models.rs @@ -71,3 +71,26 @@ pub struct Album { pub release_date: Option, } +/// Model for a song +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Song { + /// A unique id for the song + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The song's title + pub title: String, + /// The album the song is from + pub album_id: Option, + /// The track number of the song on the album + pub track: Option, + /// The duration of the song in seconds + pub duration: i32, + /// The song's release date + pub release_date: Option, + /// The path to the song's audio file + pub storage_path: String, + /// The path to the song's image file + pub image_path: Option, +} From 7e24038b228826a3d14dc0cd73d441009fc27257 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 22:47:14 -0500 Subject: [PATCH 9/9] Implement data manipulation methods for artists, albums, songs --- src/models.rs | 206 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 2 deletions(-) diff --git a/src/models.rs b/src/models.rs index d71fc6c..671ec15 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,9 +1,16 @@ use std::time::SystemTime; +use std::error::Error; use time::Date; use serde::{Deserialize, Serialize}; -#[cfg(feature = "ssr")] -use diesel::prelude::*; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use diesel::prelude::*; + use crate::database::PgPooledConn; + } +} // These "models" are used to represent the data in the database // Diesel uses these models to generate the SQL queries that are used to interact with the database. @@ -57,6 +64,114 @@ pub struct Artist { pub name: String, } +impl Artist { + /// Add an album to this artist in the database + /// + /// # Arguments + /// + /// * `new_album_id` - The id of the album to add to this artist + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_album(self: &Self, new_album_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; + + diesel::insert_into(album_artists) + .values((album_id.eq(new_album_id), artist_id.eq(my_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get albums by artist from the database + /// + /// The `id` field of this artist must be present (Some) to get albums + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of albums, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_albums(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::albums::dsl::*; + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to get albums")?; + + let my_albums = albums + .inner_join(album_artists) + .filter(artist_id.eq(my_id)) + .select(albums::all_columns()) + .load(conn)?; + + Ok(my_albums) + } + + /// Add a song to this artist in the database + /// + /// The `id` field of this artist must be present (Some) to add a song + /// + /// # Arguments + /// + /// * `new_song_id` - The id of the song to add to this artist + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_song(self: &Self, new_song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; + + diesel::insert_into(song_artists) + .values((song_id.eq(new_song_id), artist_id.eq(my_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get songs by this artist from the database + /// + /// The `id` field of this artist must be present (Some) to get songs + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of songs, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::songs::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to get songs")?; + + let my_songs = songs + .inner_join(song_artists) + .filter(artist_id.eq(my_id)) + .select(songs::all_columns()) + .load(conn)?; + + Ok(my_songs) + } +} + /// Model for an album #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] @@ -71,6 +186,62 @@ pub struct Album { pub release_date: Option, } +impl Album { + /// Add an artist to this album in the database + /// + /// The `id` field of this album must be present (Some) to add an artist + /// + /// # Arguments + /// + /// * `new_artist_id` - The id of the artist to add to this album + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_artist(self: &Self, new_artist_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Album id must be present (Some) to add an artist")?; + + diesel::insert_into(album_artists) + .values((album_id.eq(my_id), artist_id.eq(new_artist_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get songs by this artist from the database + /// + /// The `id` field of this album must be present (Some) to get songs + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of songs, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::songs::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Album id must be present (Some) to get songs")?; + + let my_songs = songs + .inner_join(song_artists) + .filter(album_id.eq(my_id)) + .select(songs::all_columns()) + .load(conn)?; + + Ok(my_songs) + } +} + /// Model for a song #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] @@ -94,3 +265,34 @@ pub struct Song { /// The path to the song's image file pub image_path: Option, } + +impl Song { + /// Add an artist to this song in the database + /// + /// The `id` field of this song must be present (Some) to add an artist + /// + /// # Arguments + /// + /// * `new_artist_id` - The id of the artist to add to this song + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::artists::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Song id must be present (Some) to get artists")?; + + let my_artists = artists + .inner_join(song_artists) + .filter(song_id.eq(my_id)) + .select(artists::all_columns()) + .load(conn)?; + + Ok(my_artists) + } +}