Skip to content

Commit 597acac

Browse files
authored
Merge pull request #6 from outof-coffee/development
feat(repo): Implement field tracking
2 parents b20729c + 22e4a6c commit 597acac

File tree

7 files changed

+419
-65
lines changed

7 files changed

+419
-65
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dross-manager"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition = "2021"
55

66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -20,3 +20,4 @@ http = "1.0.0"
2020
bytes = "1.5.0"
2121
tower = "0.4.13"
2222
log = "0.4.20"
23+
semver = "1.0.21"

src/endpoints.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ use axum::extract::rejection::JsonRejection;
44
use axum::http::StatusCode;
55
use axum::Json;
66
use axum::response::{IntoResponse, Response};
7-
use serde::Deserialize;
87
use crate::DrossManagerState;
9-
use crate::faery::Faery;
8+
use crate::faery::{CreateFaeryRequest, Faery};
109
use crate::repository::{Repository, RepositoryError};
1110

1211
pub async fn list_faeries(State(state): State<Arc<DrossManagerState>>) -> Response {
@@ -24,7 +23,7 @@ pub async fn list_faeries(State(state): State<Arc<DrossManagerState>>) -> Respon
2423
}
2524
}
2625

27-
pub async fn get_faery(State(state): State<Arc<DrossManagerState>>, Path(faery_id): Path<u32>) -> Response {
26+
pub async fn get_faery(State(state): State<Arc<DrossManagerState>>, Path(faery_id): Path<i64>) -> Response {
2827
log::info!("Getting faery {}", faery_id);
2928
let res = state.clone().faery_repository.get(faery_id).await;
3029
match res {
@@ -48,7 +47,7 @@ pub async fn get_faery(State(state): State<Arc<DrossManagerState>>, Path(faery_i
4847

4948
pub async fn update_faery(
5049
State(state): State<Arc<DrossManagerState>>,
51-
Path(faery_id): Path<u32>,
50+
Path(faery_id): Path<i64>,
5251
payload: Result<Json<Faery>, JsonRejection>
5352
) -> Response {
5453
match payload {
@@ -70,24 +69,20 @@ pub async fn update_faery(
7069
},
7170
Err(err) => {
7271
log::error!("Error updating faery {}: {:?}", faery_id, err);
73-
return (StatusCode::BAD_REQUEST, Json(RepositoryError::from(err))).into_response();
72+
let repo_error: RepositoryError = err.into();
73+
return (StatusCode::BAD_REQUEST, Json(repo_error)).into_response();
7474
}
7575
}
7676
}
7777

78-
#[derive(Deserialize, Debug)]
79-
pub struct CreateFaeryRequest {
80-
pub name: String,
81-
pub email: String,
82-
}
8378
pub async fn create_faery(
8479
State(state): State<Arc<DrossManagerState>>,
8580
payload: Result<Json<CreateFaeryRequest>, JsonRejection>
8681
) -> Response {
8782
match payload {
8883
Ok(Json(payload)) => {
8984
log::info!("Creating faery: {:?}", payload);
90-
let faery = Faery::new(payload.name, payload.email, false, 0, None);
85+
let faery: Faery = payload.into();
9186
match state.clone().faery_repository.create(Some(faery.clone())).await {
9287
Ok(_) => {
9388
(StatusCode::CREATED, Json(faery)).into_response()
@@ -100,13 +95,14 @@ pub async fn create_faery(
10095
},
10196
Err(err) => {
10297
log::error!("Error creating faery: {:?}", err);
103-
return (StatusCode::BAD_REQUEST, Json(RepositoryError::from(err))).into_response();
98+
let repo_error: RepositoryError = err.into();
99+
return (StatusCode::BAD_REQUEST, Json(repo_error)).into_response();
104100
}
105101
}
106102

107103
}
108104

109-
pub async fn delete_faery(State(state): State<Arc<DrossManagerState>>, Path(faery_id): Path<u32>) -> Response {
105+
pub async fn delete_faery(State(state): State<Arc<DrossManagerState>>, Path(faery_id): Path<i64>) -> Response {
110106
log::info!("Deleting faery {}", faery_id);
111107
match state.clone().faery_repository.delete(faery_id).await {
112108
Ok(_) => {

src/faery.rs

Lines changed: 91 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use libsql::{Row, params, Database};
33
use serde::{Deserialize, Serialize};
44
use tokio::sync::Mutex;
55
use crate::dross::{DrossError, DrossHolder, DrossResult};
6-
use crate::repository::{Repository, RepositoryError, RepositoryResult};
6+
use crate::repository::{Repository, RepositoryError, RepositoryItem, RepositoryResult};
77

88
#[derive(Clone)]
99
pub struct FaeryRepository {
@@ -16,14 +16,59 @@ impl FaeryRepository {
1616
db,
1717
}
1818
}
19+
20+
// TODO: Secure this method
21+
pub async fn update_auth_token(&self, id: i64, auth_token: String) -> RepositoryResult<()> {
22+
let db = self.db.lock().await.connect().unwrap();
23+
let result = db.execute("UPDATE faeries SET auth_token = ?1 WHERE id = ?2", params![auth_token, id]).await;
24+
match result {
25+
Ok(_) => Ok(()),
26+
Err(_) => Err(RepositoryError::Other),
27+
}
28+
}
29+
}
30+
31+
impl RepositoryItem for Faery {
32+
fn masked_columns(is_admin: bool) -> Vec<String> {
33+
let mut columns = vec!["auth_token".to_string()];
34+
if !is_admin {
35+
columns.push("email".to_string());
36+
}
37+
columns
38+
}
39+
40+
fn saved_columns() -> Vec<String> {
41+
// get all columns
42+
let columns = Faery::all_columns();
43+
// filter out masked columns, assuming is_admin is true
44+
let masked_columns = Faery::masked_columns(true);
45+
columns.into_iter().filter(|c| !masked_columns.contains(c)).collect()
46+
}
47+
48+
fn all_columns() -> Vec<String> {
49+
vec![
50+
"id".to_string(),
51+
"name".to_string(),
52+
"is_admin".to_string(),
53+
"email".to_string(),
54+
"auth_token".to_string(),
55+
"dross".to_string(),
56+
]
57+
}
58+
59+
fn table_name() -> String {
60+
"faeries".to_string()
61+
}
62+
1963
}
2064

2165
#[shuttle_runtime::async_trait]
2266
// Mark: Repository
2367
impl Repository for FaeryRepository {
2468
type Item = Faery;
69+
type RowIdentifier = i64;
2570

26-
async fn create(&self, faery: Option<Faery>) -> RepositoryResult<()> {
71+
async fn create(&self, faery: Option<Faery>) -> RepositoryResult<i64> {
2772
match faery {
2873
Some(faery) => {
2974
self.save(faery).await
@@ -32,7 +77,7 @@ impl Repository for FaeryRepository {
3277
}
3378
}
3479

35-
async fn save(&self, faery: Faery) -> RepositoryResult<()> {
80+
async fn save(&self, faery: Faery) -> RepositoryResult<i64> {
3681
let db = self.db.lock().await.connect().unwrap();
3782
let result = match faery.id {
3883
Some(id) => {
@@ -45,38 +90,13 @@ impl Repository for FaeryRepository {
4590
},
4691
};
4792
match result {
48-
Ok(_) => Ok(()),
49-
Err(_) => Err(RepositoryError::Other),
50-
}
51-
}
52-
53-
async fn create_table(&self) -> RepositoryResult<()> {
54-
let db = self.db.lock().await.connect().unwrap();
55-
let result = db.execute(
56-
r#"CREATE TABLE IF NOT EXISTS faeries (
57-
id INTEGER PRIMARY KEY,
58-
name VARCHAR(255) NOT NULL,
59-
is_admin BOOLEAN NOT NULL,
60-
email VARCHAR(255) NOT NULL,
61-
dross INTEGER
62-
)"#, ()).await;
63-
match result {
64-
Ok(_) => Ok(()),
65-
Err(_) => Err(RepositoryError::Other),
66-
}
67-
}
68-
69-
async fn drop_table(&self) -> RepositoryResult<()> {
70-
let db = self.db.lock().await.connect().unwrap();
71-
let result = db.execute("DROP TABLE IF EXISTS faeries", ()).await;
72-
match result {
73-
Ok(_) => Ok(()),
93+
Ok(_) => Ok(db.last_insert_rowid()),
7494
Err(_) => Err(RepositoryError::Other),
7595
}
7696
}
7797

7898
// Mark: Faery
79-
async fn get(&self, id: u32) -> RepositoryResult<Faery> {
99+
async fn get(&self, id: i64) -> RepositoryResult<Faery> {
80100
let db = self.db.lock().await.connect().unwrap();
81101
let mut stmt = db
82102
.prepare("SELECT * FROM faeries WHERE id = ?1")
@@ -102,6 +122,7 @@ impl Repository for FaeryRepository {
102122
return Err(RepositoryError::Other)
103123
},
104124
};
125+
105126
let mut faeries: Vec<Faery> = Vec::new();
106127
while let Ok(result_row) = res.next().await {
107128
match result_row {
@@ -114,7 +135,7 @@ impl Repository for FaeryRepository {
114135
Ok(faeries)
115136
}
116137

117-
async fn delete(&self, id: u32) -> RepositoryResult<()> {
138+
async fn delete(&self, id: i64) -> RepositoryResult<()> {
118139
let db = self.db.lock().await.connect().unwrap();
119140
let result = db.execute("DELETE FROM faeries WHERE id = ?1", [id]).await;
120141
match result {
@@ -123,20 +144,40 @@ impl Repository for FaeryRepository {
123144
}
124145
}
125146

126-
fn table_name() -> String {
127-
"faeries".to_string()
147+
async fn create_table(&self) -> RepositoryResult<()> {
148+
let db = self.db.lock().await.connect().unwrap();
149+
let result = db.execute(
150+
r#"CREATE TABLE IF NOT EXISTS faeries (
151+
id INTEGER PRIMARY KEY,
152+
name VARCHAR(255) NOT NULL,
153+
is_admin BOOLEAN NOT NULL,
154+
email VARCHAR(255) NOT NULL,
155+
dross INTEGER
156+
)"#, ()).await;
157+
match result {
158+
Ok(_) => Ok(()),
159+
Err(_) => Err(RepositoryError::Other),
160+
}
161+
}
162+
163+
async fn drop_table(&self) -> RepositoryResult<()> {
164+
let db = self.db.lock().await.connect().unwrap();
165+
let result = db.execute("DROP TABLE IF EXISTS faeries", ()).await;
166+
match result {
167+
Ok(_) => Ok(()),
168+
Err(_) => Err(RepositoryError::Other),
169+
}
128170
}
129171
}
130172

131173
// Faery represents the user of the application.
132174
// It has the name of the user, their email, an authentication token, and a count of their dross.
133175
#[derive(Debug, Deserialize, Serialize)]
134176
pub struct Faery {
135-
pub(crate) id: Option<u32>,
177+
pub(crate) id: Option<i64>,
136178
pub name: String,
137179
pub email: String,
138180
pub is_admin: bool,
139-
#[serde(skip_serializing)]
140181
pub auth_token: Option<String>,
141182
pub dross: u32,
142183
}
@@ -145,12 +186,13 @@ pub struct Faery {
145186
impl Faery {
146187
// This is a method that creates a new Faery.
147188
// It takes a name and an email and returns a Faery.
148-
pub fn new(name: String, email: String, is_admin: bool, dross: u32, id: Option<u32>) -> Faery {
189+
pub fn new(name: String, email: String, is_admin: bool, dross: u32, id: Option<i64>) -> Faery {
149190
Faery {
150191
id,
151192
name,
152193
email,
153194
is_admin,
195+
// TODO: Implement static method for inflating from auth_token claims
154196
auth_token: None,
155197
dross,
156198
}
@@ -246,3 +288,16 @@ impl DrossHolder for Faery {
246288
Ok(self.dross)
247289
}
248290
}
291+
292+
293+
#[derive(Deserialize, Debug)]
294+
pub struct CreateFaeryRequest {
295+
pub name: String,
296+
pub email: String,
297+
}
298+
299+
impl From<CreateFaeryRequest> for Faery {
300+
fn from(req: CreateFaeryRequest) -> Self {
301+
Faery::new(req.name, req.email, false, 0, None)
302+
}
303+
}

src/main.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@ mod faery;
22
mod dross;
33
mod endpoints;
44
mod repository;
5+
mod migrations;
6+
mod version;
57

68
use std::net::SocketAddr;
79
use axum::{routing::get, Router};
810
use tower_http::services::ServeDir;
911
use libsql::Builder;
1012
use std::sync::Arc;
1113
use tokio::sync::Mutex;
12-
use repository::Repository;
1314
use http::{Method};
1415

1516
use tower::{ServiceBuilder};
16-
use tower_http::cors::{Any, CorsLayer};
17+
use tower_http::cors::{CorsLayer};
1718

1819

1920
pub struct DrossManagerService {
@@ -47,15 +48,16 @@ async fn axum(
4748
});
4849

4950
// TODO: Handle errors
50-
log::info!("Creating table");
51-
state.faery_repository.create_table().await.unwrap();
51+
let manager = migrations::Manager::new(db.clone(), state.faery_repository.clone());
52+
let needs_migration = manager.needs_migration().await;
53+
if needs_migration {
54+
log::info!("Running migrations");
55+
manager.migrate().await.unwrap();
56+
}
5257

5358
log::info!("Creating CORS middleware");
5459
let cors = CorsLayer::new()
55-
// allow `GET` and `POST` when accessing the resource
56-
.allow_methods([Method::GET, Method::POST])
57-
// allow requests from any origin
58-
.allow_origin(Any);
60+
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]);
5961

6062
log::info!("Creating router");
6163
let router = Router::new()

0 commit comments

Comments
 (0)