Skip to content

Commit

Permalink
update data model to handle feature environments + start server refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
viktormarinho committed Oct 4, 2023
1 parent d2fe4de commit 5182ce3
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 79 deletions.
1 change: 0 additions & 1 deletion migrations/2_projects.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ create table if not exists environment (

create table if not exists feature (
id text primary key not null, -- not and uuid yet.
active boolean not null,

project_id text not null,
foreign key(project_id) references project(id)
Expand Down
13 changes: 10 additions & 3 deletions src/external/features.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
use axum::{
extract::Path,
extract::{Path, Query},
headers::{authorization::Bearer, Authorization},
http::StatusCode,
routing::get,
Extension, Json, Router, TypedHeader,
};
use serde::Serialize;
use serde::{Serialize, Deserialize};
use sqlx::SqlitePool;

#[derive(Serialize)]
struct FeatureState {
active: bool,
}

#[derive(Deserialize)]
pub struct EnvQuery {
env_id: String
}

async fn get_feature_state(
Extension(pool): Extension<SqlitePool>,
TypedHeader(authorization): TypedHeader<Authorization<Bearer>>,
Path(id): Path<String>,
Query(EnvQuery { env_id }): Query<EnvQuery>
) -> Result<Json<FeatureState>, StatusCode> {
let token = authorization.token();
let credential = sqlx::query!(r#"SELECT * FROM credentials WHERE token = ?;"#, token)
Expand All @@ -28,7 +34,8 @@ async fn get_feature_state(
return Err(StatusCode::UNAUTHORIZED);
}

let feature = sqlx::query!(r#"SELECT active FROM feature WHERE id = ?;"#, id)
// this is going to change for sure
let feature = sqlx::query!(r#"SELECT active FROM environment_feature WHERE feature_id = ? AND environment_id = ?;"#, id, env_id)
.fetch_optional(&pool)
.await
.unwrap();
Expand Down
44 changes: 18 additions & 26 deletions src/features/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,11 @@ use serde::Serialize;
use serde_json::json;
use sqlx::SqlitePool;

use crate::projects::create::Project;

#[derive(Serialize)]
pub struct Feature {
pub id: String,
pub active: bool,
pub project_id: String,
}
use crate::models::feature::EnvironmentFeature;

#[derive(Serialize)]
pub struct AllFeaturesResponse {
features: Vec<Feature>,
project: Project,
features: Vec<EnvironmentFeature>,
}

pub enum AllFeaturesErr {
Expand All @@ -37,34 +29,34 @@ impl IntoResponse for AllFeaturesErr {

pub async fn fetch_all_features(
pool: &SqlitePool,
project_id: &String,
) -> Result<Vec<Feature>, AllFeaturesErr> {
let result = sqlx::query_as!(
Feature,
"SELECT * FROM feature WHERE project_id = ?",
project_id
env_id: &String,
) -> Result<Vec<EnvironmentFeature>, AllFeaturesErr> {
let features = sqlx::query_as!(
EnvironmentFeature,
r#"
SELECT *
FROM environment_feature
WHERE environment_id = ?;
"#,
env_id,
)
.fetch_all(pool)
.await;

if result.is_err() {
if features.is_err() {
return Err(AllFeaturesErr::CouldNotFetchFromDatabase);
}

Ok(result.unwrap())
Ok(features.unwrap())
}

pub async fn all(
Extension(pool): Extension<SqlitePool>,
Path(project_id): Path<String>,
Path(env_id): Path<String>,
) -> Result<(StatusCode, Json<AllFeaturesResponse>), AllFeaturesErr> {
let features = fetch_all_features(&pool, &project_id).await?;
let project = sqlx::query_as!(Project, "SELECT * FROM project WHERE id = ?", project_id)
.fetch_one(&pool)
.await
.unwrap();
let features = fetch_all_features(&pool, &env_id).await?;

Ok((StatusCode::OK, Json(AllFeaturesResponse { features, project })))
Ok((StatusCode::OK, Json(AllFeaturesResponse { features })))
}

mod tests {
Expand All @@ -73,7 +65,7 @@ mod tests {
async fn fetch_all_features_work() {
use crate::{db::get_pool, features::all::fetch_all_features};
let pool = get_pool().await;
let features = fetch_all_features(&pool, &String::from("kekeke")).await;
let features = fetch_all_features(&pool, &String::from("dev")).await;
assert!(features.is_ok());
}
}
85 changes: 55 additions & 30 deletions src/features/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::SqlitePool;

use crate::models::Project;

#[derive(Serialize)]
pub struct NewFeatureResponse {
feature_id: String
Expand All @@ -25,20 +27,58 @@ pub struct CreateFeatureDto {
pub name: String,
}

impl CreateFeatureDto {
async fn validate(&self, pool: &SqlitePool) -> Result<(String, String), CreateFeatureErr> {
if self.name.len() < 3 {
return Err(CreateFeatureErr::FeatureNameTooShort)
}

let project = sqlx::query!("SELECT * FROM project WHERE id = ?", self.project_id)
.fetch_optional(pool)
.await
.unwrap();

if project.is_none() {
return Err(CreateFeatureErr::ProjectDoesNotExist);
}

let project = project.unwrap();

let feature_id = format!(
"{}_{}",
&project.name,
&self.name
).replace(" ", "_").to_lowercase();

let existing_feature = sqlx::query!("SELECT * FROM feature WHERE id = ?", feature_id)
.fetch_optional(pool)
.await
.unwrap();

if existing_feature.is_some() {
return Err(CreateFeatureErr::FeatureAlreadyExists);
}

Ok((feature_id, project.id))
}
}

pub struct CreateFeatureOk {
pub feature_id: String
}

pub enum CreateFeatureErr {
ProjectDoesNotExist,
FeatureAlreadyExists
FeatureAlreadyExists,
FeatureNameTooShort
}

impl IntoResponse for CreateFeatureErr {
fn into_response(self) -> axum::response::Response {
let (status, error_message, fields) = match self {
CreateFeatureErr::FeatureAlreadyExists => (StatusCode::BAD_REQUEST, "Feature already exists", vec!["name"]),
CreateFeatureErr::ProjectDoesNotExist => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error: The project does not exist", vec![])
CreateFeatureErr::ProjectDoesNotExist => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error: The project does not exist", vec![]),
CreateFeatureErr::FeatureNameTooShort => (StatusCode::BAD_REQUEST, "Feature name is too short", vec!["name"])
};
let body = Json(json!({
"error": error_message, "fields": fields
Expand All @@ -48,42 +88,27 @@ impl IntoResponse for CreateFeatureErr {
}

pub async fn create(pool: &SqlitePool, data: CreateFeatureDto) -> Result<CreateFeatureOk, CreateFeatureErr> {
let project = sqlx::query!("SELECT * FROM project WHERE id = ?", data.project_id)
.fetch_optional(pool)
.await
.unwrap();

if project.is_none() {
return Err(CreateFeatureErr::ProjectDoesNotExist);
}

let project = project.unwrap();

let feature_id = format!(
"{}_{}",
&project.name,
&data.name
).replace(" ", "_").to_lowercase();

let existing_feature = sqlx::query!("SELECT * FROM feature WHERE id = ?", feature_id)
.fetch_optional(pool)
.await
.unwrap();

if existing_feature.is_some() {
return Err(CreateFeatureErr::FeatureAlreadyExists);
}
let (feature_id, project_id) = data.validate(pool).await?;

let feature = sqlx::query!(
"INSERT INTO feature (id, active, project_id) VALUES (?, ?, ?) RETURNING id",
"INSERT INTO feature (id, project_id) VALUES (?, ?) RETURNING id",
feature_id,
false,
data.project_id
project_id
)
.fetch_one(pool)
.await
.unwrap();

let envs = Project::load_envs(project_id, pool)
.await
.unwrap();

for env in envs {
env.connect_feature(pool, feature_id.clone())
.await
.unwrap();
}

return Ok(CreateFeatureOk {
feature_id: feature.id
});
Expand Down
2 changes: 1 addition & 1 deletion src/features/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub mod toggle;

pub fn features() -> Router {
Router::new()
.route("/:project_id", get(all::all))
.route("/:env_id", get(all::all))
.route("/", post(create::new))
.route("/toggle", post(toggle::toggle))
.layer(middleware::from_fn(require_auth))
Expand Down
6 changes: 3 additions & 3 deletions src/features/toggle.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::all::Feature;
use crate::models::{Feature, feature::EnvironmentFeature};
use axum::{Extension, Json, response::IntoResponse, http::StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
Expand Down Expand Up @@ -34,7 +34,7 @@ pub async fn toggle(
Extension(pool): Extension<SqlitePool>,
axum::extract::Json(data): Json<ToggleFeatureDto>,
) -> Result<Json<ToggleFeatureResponse>, ToggleFeatureErr> {
let feature = match sqlx::query_as!(Feature, "SELECT * FROM feature WHERE id = ?;", data.id)
let feature = match sqlx::query_as!(EnvironmentFeature, "SELECT * FROM environment_feature WHERE id = ?;", data.id)
.fetch_one(&pool)
.await {
Ok(feat) => feat,
Expand All @@ -45,7 +45,7 @@ pub async fn toggle(

let new_state = !feature.active;

let feature = sqlx::query_as!(Feature, "UPDATE feature SET active = ? WHERE id = ? RETURNING *;", new_state, data.id)
let feature = sqlx::query_as!(EnvironmentFeature, "UPDATE environment_feature SET active = ? WHERE id = ? RETURNING *;", new_state, data.id)
.fetch_one(&pool)
.await
.unwrap();
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub mod features;
pub mod projects;
pub mod external;

pub mod models;

pub mod gen {
pub fn id() -> String {
uuid::Uuid::new_v4().to_string()
Expand Down
43 changes: 43 additions & 0 deletions src/models/environment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
pub use super::*;

#[derive(Serialize, Deserialize)]
pub struct Environment {
pub id: String,
pub name: String,
pub project_id: String
}

impl Environment {
pub async fn new(pool: &SqlitePool, name: impl Into<String>, project_id: &String) -> Result<Environment, sqlx::Error> {
let id = gen::id();
let name: String = name.into();
sqlx::query_as!(
Environment,
r#"
INSERT INTO environment
(id, name, project_id)
VALUES
(?, ?, ?)
RETURNING *;
"#,
id,
name,
project_id,
)
.fetch_one(pool)
.await
}

pub async fn connect_feature(self, pool: &SqlitePool, feature_id: String) -> Result<String, sqlx::Error> {
let id = gen::id();
sqlx::query!(
"INSERT INTO environment_feature (id, environment_id, feature_id, active) VALUES (?, ?, ?, ?) RETURNING id",
id,
self.id,
feature_id,
false
)
.fetch_one(pool)
.await.map(|rec| rec.id)
}
}
15 changes: 15 additions & 0 deletions src/models/feature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pub use super::*;

#[derive(Serialize, Deserialize)]
pub struct Feature {
pub id: String,
pub project_id: String,
}

#[derive(Serialize, Deserialize)]
pub struct EnvironmentFeature {
pub id: String,
pub active: bool,
pub feature_id: String,
pub environment_id: String
}
13 changes: 13 additions & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pub mod environment;
pub use environment::Environment;

pub mod feature;
pub use feature::Feature;

pub mod project;
pub use project::Project;

pub use serde::{Deserialize, Serialize};
pub use sqlx::SqlitePool;

pub use crate::gen;
20 changes: 20 additions & 0 deletions src/models/project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use super::*;

#[derive(Serialize, Deserialize)]
pub struct Project {
pub id: Option<String>,
pub name: String,
pub user_id: String,
}

impl Project {
pub async fn load_envs(project_id: String, pool: &SqlitePool) -> Result<Vec<Environment>, sqlx::Error> {
sqlx::query_as!(
Environment,
"SELECT * FROM environment WHERE project_id = ?;",
project_id
)
.fetch_all(pool)
.await
}
}
Loading

0 comments on commit 5182ce3

Please sign in to comment.