feat: one-to-many relation helper
Allow one to specify that a field of a model is a foreign key. It will generate a bunch of helper methods to query related entities from one entity.
This commit is contained in:
parent
32ef1f7b33
commit
5f45671b74
25 changed files with 764 additions and 140 deletions
1
lib/sandbox/.gitignore
vendored
Normal file
1
lib/sandbox/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
tmp/
|
||||
|
|
@ -8,8 +8,12 @@ name = "sandbox"
|
|||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
chrono = "0.4.39"
|
||||
fully_pub = "0.1.4"
|
||||
serde = "1.0.216"
|
||||
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "chrono", "uuid", "migrate"] }
|
||||
sqlxgentools_attrs = { path = "../sqlxgentools_attrs" }
|
||||
sqlxgentools_misc = { path = "../sqlxgentools_misc" }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
# Children justfile
|
||||
|
||||
reset-db *args:
|
||||
rm sandbox.db
|
||||
sqlite3 {{args}} sandbox.db < src/migrations/all.sql
|
||||
touch tmp/db.db
|
||||
rm tmp/db.db
|
||||
sqlite3 {{args}} tmp/db.db < src/migrations/all.sql
|
||||
|
||||
seed-db:
|
||||
sqlite3 tmp/db.db < src/migrations/all.sql
|
||||
|
||||
gen-sqlx:
|
||||
../../target/release/sqlx-generator \
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
use anyhow::Context;
|
||||
use std::str::FromStr;
|
||||
use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
|
||||
use fully_pub::fully_pub;
|
||||
use sqlx::{
|
||||
Pool, Sqlite,
|
||||
Pool, Sqlite, sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||
};
|
||||
|
||||
/// database storage interface
|
||||
|
|
@ -8,3 +13,29 @@ use sqlx::{
|
|||
#[derive(Clone, Debug)]
|
||||
struct Database(Pool<Sqlite>);
|
||||
|
||||
|
||||
/// Initialize database
|
||||
pub async fn provide_database(sqlite_db_path: &str) -> Result<Database> {
|
||||
let path = PathBuf::from(sqlite_db_path);
|
||||
let is_db_initialization = !path.exists();
|
||||
// // database does not exists, trying to create it
|
||||
// if path
|
||||
// .parent()
|
||||
// .filter(|pp| pp.exists())
|
||||
// Err(anyhow!("Could not find parent directory of the db location.")));
|
||||
|
||||
let conn_str = format!("sqlite://{sqlite_db_path}");
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(50)
|
||||
.connect_with(SqliteConnectOptions::from_str(&conn_str)?.create_if_missing(true))
|
||||
.await
|
||||
.context("could not connect to database_url")?;
|
||||
// if is_db_initialization {
|
||||
// initialize_db(Database(pool.clone())).await?;
|
||||
// }
|
||||
|
||||
Ok(Database(pool))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,95 @@
|
|||
use anyhow::{Context, Result};
|
||||
|
||||
use chrono::Utc;
|
||||
use sqlx::types::Json;
|
||||
use sqlxgentools_misc::ForeignRef;
|
||||
|
||||
use crate::{db::provide_database, models::user::{User, UserToken}, repositories::user_token_repository::UserTokenRepository};
|
||||
|
||||
pub mod models;
|
||||
pub mod repositories;
|
||||
pub mod db;
|
||||
|
||||
fn main() {
|
||||
println!("Sandbox")
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
println!("Sandbox");
|
||||
|
||||
let users = vec![
|
||||
User {
|
||||
id: "idu1".into(),
|
||||
handle: "john.doe".into(),
|
||||
full_name: None,
|
||||
prefered_color: None,
|
||||
last_login_at: None,
|
||||
status: models::user::UserStatus::Invited,
|
||||
groups: Json(vec![]),
|
||||
avatar_bytes: None
|
||||
},
|
||||
User {
|
||||
id: "idu2".into(),
|
||||
handle: "richard".into(),
|
||||
full_name: None,
|
||||
prefered_color: None,
|
||||
last_login_at: None,
|
||||
status: models::user::UserStatus::Invited,
|
||||
groups: Json(vec![]),
|
||||
avatar_bytes: None
|
||||
},
|
||||
User {
|
||||
id: "idu3".into(),
|
||||
handle: "manned".into(),
|
||||
full_name: None,
|
||||
prefered_color: None,
|
||||
last_login_at: None,
|
||||
status: models::user::UserStatus::Invited,
|
||||
groups: Json(vec![]),
|
||||
avatar_bytes: None
|
||||
}
|
||||
];
|
||||
let user_token = UserToken {
|
||||
id: "idtoken1".into(),
|
||||
secret: "4LP5A3F3XBV5NM8VXRGZG3QDXO9PNAC0".into(),
|
||||
last_use_time: None,
|
||||
creation_time: Utc::now(),
|
||||
expiration_time: Utc::now(),
|
||||
user_id: ForeignRef::new(&users.get(0).unwrap())
|
||||
};
|
||||
|
||||
let db = provide_database("tmp/db.db").await?;
|
||||
|
||||
let user_token_repo = UserTokenRepository::new(db);
|
||||
user_token_repo.insert_many(&vec![
|
||||
UserToken {
|
||||
id: "idtoken2".into(),
|
||||
secret: "4LP5A3F3XBV5NM8VXRGZG3QDXO9PNAC0".into(),
|
||||
last_use_time: None,
|
||||
creation_time: Utc::now(),
|
||||
expiration_time: Utc::now(),
|
||||
user_id: ForeignRef::new(&users.get(0).unwrap())
|
||||
},
|
||||
UserToken {
|
||||
id: "idtoken3".into(),
|
||||
secret: "CBHR6G41KSEMR1AI".into(),
|
||||
last_use_time: None,
|
||||
creation_time: Utc::now(),
|
||||
expiration_time: Utc::now(),
|
||||
user_id: ForeignRef::new(&users.get(1).unwrap())
|
||||
},
|
||||
UserToken {
|
||||
id: "idtoken4".into(),
|
||||
secret: "CBHR6G41KSEMR1AI".into(),
|
||||
last_use_time: None,
|
||||
creation_time: Utc::now(),
|
||||
expiration_time: Utc::now(),
|
||||
user_id: ForeignRef::new(&users.get(1).unwrap())
|
||||
}
|
||||
]).await?;
|
||||
let user_tokens = user_token_repo.get_many_user_tokens_by_usersss(
|
||||
vec!["idu2".into()]
|
||||
).await?;
|
||||
dbg!(&user_tokens);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
-- DO NOT EDIT THIS FILE.
|
||||
-- Generated by sqlxgentools from models files.
|
||||
CREATE TABLE usersss (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
handle TEXT NOT NULL UNIQUE,
|
||||
|
|
@ -6,13 +8,13 @@ CREATE TABLE usersss (
|
|||
last_login_at DATETIME,
|
||||
status TEXT NOT NULL,
|
||||
groups TEXT NOT NULL,
|
||||
avatar_bytes BLOB NOT NULL
|
||||
avatar_bytes TEXT
|
||||
);
|
||||
CREATE TABLE user_tokens (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
last_use_time DATETIME,
|
||||
creation_time DATETIME NOT NULL,
|
||||
expiration_time DATETIME NOT NULL
|
||||
expiration_time DATETIME NOT NULL,
|
||||
user_id TEXT NOT NULL
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use chrono::{DateTime, Utc};
|
|||
use sqlx::types::Json;
|
||||
use fully_pub::fully_pub;
|
||||
|
||||
use sqlxgentools_attrs::{sql_generator_model, SqlGeneratorDerive};
|
||||
use sqlxgentools_attrs::{SqlGeneratorDerive, SqlGeneratorModelWithId, sql_generator_model};
|
||||
use sqlxgentools_misc::{DatabaseLine, ForeignRef};
|
||||
|
||||
#[derive(sqlx::Type, Clone, Debug, PartialEq)]
|
||||
#[fully_pub]
|
||||
|
|
@ -13,7 +14,7 @@ enum UserStatus {
|
|||
Archived
|
||||
}
|
||||
|
||||
#[derive(SqlGeneratorDerive, sqlx::FromRow, Debug, Clone)]
|
||||
#[derive(SqlGeneratorDerive, SqlGeneratorModelWithId, sqlx::FromRow, Debug, Clone)]
|
||||
#[sql_generator_model(table_name="usersss")]
|
||||
#[fully_pub]
|
||||
struct User {
|
||||
|
|
@ -26,20 +27,21 @@ struct User {
|
|||
last_login_at: Option<DateTime<Utc>>,
|
||||
status: UserStatus,
|
||||
groups: Json<Vec<String>>,
|
||||
avatar_bytes: Vec<u8>
|
||||
avatar_bytes: Option<Vec<u8>>
|
||||
}
|
||||
|
||||
#[derive(SqlGeneratorDerive, sqlx::FromRow, Debug, Clone)]
|
||||
|
||||
#[derive(SqlGeneratorDerive, SqlGeneratorModelWithId, sqlx::FromRow, Debug, Clone)]
|
||||
#[sql_generator_model(table_name="user_tokens")]
|
||||
#[fully_pub]
|
||||
struct UserToken {
|
||||
#[sql_generator_field(is_primary=true)]
|
||||
id: String,
|
||||
// #[sql_generator_field(foreign_key=Relation::BelongsTo(User))]
|
||||
user_id: String,
|
||||
secret: String,
|
||||
last_use_time: Option<DateTime<Utc>>,
|
||||
creation_time: DateTime<Utc>,
|
||||
expiration_time: DateTime<Utc>
|
||||
expiration_time: DateTime<Utc>,
|
||||
#[sql_generator_field(reverse_relation_name="user_tokens")] // to generate get_user_tokens_of_user(&user_id)
|
||||
user_id: ForeignRef<User>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,14 +40,14 @@ impl UserTokenRepository {
|
|||
}
|
||||
pub async fn insert(&self, entity: &UserToken) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"INSERT INTO user_tokens (id, user_id, secret, last_use_time, creation_time, expiration_time) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
"INSERT INTO user_tokens (id, secret, last_use_time, creation_time, expiration_time, user_id) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
)
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.user_id)
|
||||
.bind(&entity.secret)
|
||||
.bind(&entity.last_use_time)
|
||||
.bind(&entity.creation_time)
|
||||
.bind(&entity.expiration_time)
|
||||
.bind(&entity.user_id.target_id)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
|
@ -69,18 +69,18 @@ impl UserTokenRepository {
|
|||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let query_sql = format!(
|
||||
"INSERT INTO user_tokens (id, user_id, secret, last_use_time, creation_time, expiration_time) VALUES {} ON CONFLICT DO NOTHING",
|
||||
"INSERT INTO user_tokens (id, secret, last_use_time, creation_time, expiration_time, user_id) VALUES {} ON CONFLICT DO NOTHING",
|
||||
values_templates
|
||||
);
|
||||
let mut query = sqlx::query(&query_sql);
|
||||
for entity in entities {
|
||||
query = query
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.user_id)
|
||||
.bind(&entity.secret)
|
||||
.bind(&entity.last_use_time)
|
||||
.bind(&entity.creation_time)
|
||||
.bind(&entity.expiration_time);
|
||||
.bind(&entity.expiration_time)
|
||||
.bind(&entity.user_id.target_id);
|
||||
}
|
||||
query.execute(&self.db.0).await?;
|
||||
Ok(())
|
||||
|
|
@ -91,15 +91,15 @@ impl UserTokenRepository {
|
|||
entity: &UserToken,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"UPDATE user_tokens SET id = $2, user_id = $3, secret = $4, last_use_time = $5, creation_time = $6, expiration_time = $7 WHERE id = $1",
|
||||
"UPDATE user_tokens SET id = $2, secret = $3, last_use_time = $4, creation_time = $5, expiration_time = $6, user_id = $7 WHERE id = $1",
|
||||
)
|
||||
.bind(item_id)
|
||||
.bind(&entity.id)
|
||||
.bind(&entity.user_id)
|
||||
.bind(&entity.secret)
|
||||
.bind(&entity.last_use_time)
|
||||
.bind(&entity.creation_time)
|
||||
.bind(&entity.expiration_time)
|
||||
.bind(&entity.user_id.target_id)
|
||||
.execute(&self.db.0)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
|
@ -111,4 +111,33 @@ impl UserTokenRepository {
|
|||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn get_many_user_tokens_by_user(
|
||||
&self,
|
||||
item_id: &str,
|
||||
) -> Result<Vec<UserToken>, sqlx::Error> {
|
||||
sqlx::query_as::<_, UserToken>("SELECT * FROM user_tokens WHERE user_id = $1")
|
||||
.bind(item_id)
|
||||
.fetch_all(&self.db.0)
|
||||
.await
|
||||
}
|
||||
pub async fn get_many_user_tokens_by_usersss(
|
||||
&self,
|
||||
items_ids: Vec<String>,
|
||||
) -> Result<Vec<UserToken>, sqlx::Error> {
|
||||
if items_ids.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let placeholder_params: String = (1..=(items_ids.len()))
|
||||
.map(|i| format!("${i}"))
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
let query_tmpl = format!(
|
||||
"SELECT * FROM user_tokens WHERE user_id IN ({})", placeholder_params
|
||||
);
|
||||
let mut query = sqlx::query_as::<_, UserToken>(&query_tmpl);
|
||||
for id in items_ids {
|
||||
query = query.bind(id);
|
||||
}
|
||||
query.fetch_all(&self.db.0).await
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue