Skip to content

Commit

Permalink
added schema and crl apis for organisation
Browse files Browse the repository at this point in the history
  • Loading branch information
sauraww committed Dec 20, 2024
1 parent c44bd0c commit 778257c
Show file tree
Hide file tree
Showing 16 changed files with 434 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ HOSTNAME="<application_name>-<deployment_id>-<replicaset>-<pod>"
ACTIX_KEEP_ALIVE=120
MAX_DB_CONNECTION_POOL_SIZE=3
ENABLE_TENANT_AND_SCOPE=true
TENANTS=dev,test
TENANTS=dev,test,superposition
TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/"
SERVICE_PREFIX=""
SERVICE_NAME="CAC"
31 changes: 31 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/frontend",
"crates/cac_toml",
"crates/superposition",
"crates/superposition/organisation",
"crates/superposition_types",
"examples/experimentation_client_integration_example",
"examples/cac_client_integration_example",
Expand Down
1 change: 1 addition & 0 deletions crates/service_utils/src/service/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl FromStr for AppEnv {
pub enum AppScope {
CAC,
EXPERIMENTATION,
SUPERPOSITION,
}
impl FromRequest for AppScope {
type Error = Error;
Expand Down
2 changes: 2 additions & 0 deletions crates/superposition/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ context_aware_config = { path = "../context_aware_config" }
dotenv = "0.15.0"
env_logger = "0.8"
experimentation_platform = { path = "../experimentation_platform" }
organisation = { path = "./organisation" }
fred = { workspace = true, optional = true }
frontend = { path = "../frontend" }
leptos = { workspace = true }
Expand All @@ -24,6 +25,7 @@ serde_json = { workspace = true }
service_utils = { path = "../service_utils" }
superposition_types = { path = "../superposition_types" }
toml = { workspace = true }
idgenerator = "2.0.0"

[features]
high-performance-mode = [
Expand Down
21 changes: 21 additions & 0 deletions crates/superposition/organisation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "organisation"
version = "0.1.0"
edition = "2021"


[dependencies]
service_utils = { path = "../../service_utils" }
superposition_types = { path = "../../superposition_types", features = [
"result",
"diesel_derives",
]}
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
actix-web = { workspace = true }
anyhow = { workspace = true }
diesel = { workspace = true , features = ["numeric"]}
idgenerator = "2.0.0"
log = { workspace = true }
superposition_macros = { path = "../../superposition_macros" }
154 changes: 154 additions & 0 deletions crates/superposition/organisation/src/api/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use super::types::{
CreateOrganisationRequest, CreateOrganisationResponse, OrganisationResponse,
};
use actix_web::{
get, post,
web::{self, Json, Query},
HttpResponse, Scope,
};
use chrono::Utc;
use diesel::prelude::*;
use idgenerator::IdInstance;
use service_utils::service::types::DbConnection;
use superposition_types::{
custom_query::PaginationParams, result as superposition, PaginatedResponse,
};

use superposition_types::database::{
models::organisation::Organisation, schema::organisation::dsl::organisation,
};

pub fn endpoints() -> Scope {
Scope::new("")
.service(create_organisation)
.service(list_organisations)
.service(get_organisation)
}

#[post("")]
pub async fn create_organisation(
req: web::Json<CreateOrganisationRequest>,
mut db_conn: DbConnection,
) -> superposition::Result<HttpResponse> {
let org_id =
db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| {
// Generating a numeric ID from IdInstance and prefixing it with `orgid`
let numeric_id = IdInstance::next_id();
let org_id = format!("orgid{}", numeric_id);

let now = Utc::now().naive_utc();

let new_org = Organisation {
id: org_id.clone(),
country_code: req.country_code.clone(),
contact_email: req.contact_email.clone(),
contact_phone: req.contact_phone.clone(),
created_by: req.created_by.clone(),
admin_email: req.admin_email.clone(),
status: req.status,
contact_details: req.contact_details.clone(),
sector: req.sector.clone(),
industry: req.industry.clone(),
created_at: now,
updated_at: now,
updated_by: req.created_by.clone(),
};

diesel::insert_into(organisation)
.values(&new_org)
.execute(transaction_conn)
.map_err(|e| {
log::error!("Failed to insert new organisation: {:?}", e);
superposition::AppError::UnexpectedError(anyhow::anyhow!(
"Failed to create organisation"
))
})?;
Ok(org_id)
})?;

let mut http_resp = HttpResponse::Created();
Ok(http_resp.json(CreateOrganisationResponse { org_id }))
}

#[get("/{org_id}")]
pub async fn get_organisation(
org_id: web::Path<String>,
mut db_conn: DbConnection,
) -> superposition::Result<HttpResponse> {
let org =
db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| {
organisation
.find(org_id.as_str())
.first::<Organisation>(transaction_conn)
.map_err(|e| {
log::error!("Failed to fetch organisation {}: {:?}", org_id, e);
match e {
diesel::result::Error::NotFound => {
superposition::AppError::NotFound(format!(
"Organisation {} not found",
org_id
))
}
_ => superposition::AppError::UnexpectedError(anyhow::anyhow!(
"Failed to fetch organisation"
)),
}
})
})?;

Ok(HttpResponse::Ok().json(OrganisationResponse::from(org)))
}

#[get("/list")]
pub async fn list_organisations(
db_conn: DbConnection,
filters: Query<PaginationParams>,
) -> superposition::Result<Json<PaginatedResponse<Organisation>>> {
use superposition_types::database::schema::organisation::dsl::*;
let DbConnection(mut conn) = db_conn;
log::info!("list_organisations");
let result =
conn.transaction::<_, superposition::AppError, _>(|transaction_conn| {
// If all parameter is true, return all organisations
if let Some(true) = filters.all {
let organisations: Vec<Organisation> = organisation
.order(created_at.desc())
.get_results(transaction_conn)?;
log::info!("organisations: {organisations:?}");
return Ok(PaginatedResponse {
total_pages: 1,
total_items: organisations.len() as i64,
data: organisations,
});
}

// Get total count of organisations
let total_items: i64 = organisation.count().get_result(transaction_conn)?;

// Set up pagination
let limit = filters.count.unwrap_or(10);
let mut builder = organisation
.into_boxed()
.order(created_at.desc())
.limit(limit);

// Apply offset if page is specified
if let Some(page) = filters.page {
let offset = (page - 1) * limit;
builder = builder.offset(offset);
}

// Get paginated results
let organisations: Vec<Organisation> = builder.load(transaction_conn)?;

let total_pages = (total_items as f64 / limit as f64).ceil() as i64;

Ok(PaginatedResponse {
total_pages,
total_items,
data: organisations,
})
})?;

Ok(Json(result))
}
3 changes: 3 additions & 0 deletions crates/superposition/organisation/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod handlers;
pub mod types;
pub use handlers::endpoints;
82 changes: 82 additions & 0 deletions crates/superposition/organisation/src/api/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use superposition_types::database::models::organisation::OrgStatus;
use superposition_types::database::models::organisation::Organisation;

// Request payload for creating an organisation
#[derive(Deserialize)]
pub struct CreateOrganisationRequest {
pub country_code: Option<String>,
pub contact_email: Option<String>,
pub contact_phone: Option<String>,
pub created_by: String,
pub admin_email: String,
pub status: OrgStatus,
pub contact_details: Option<JsonValue>,
pub sector: Option<String>,
pub industry: Option<String>,
}

// Response type to include `org_id`
#[derive(Serialize)]
pub struct CreateOrganisationResponse {
pub org_id: String,
}

// Response struct for single organisation
#[derive(Serialize)]
pub struct OrganisationResponse {
id: String,
country_code: Option<String>,
contact_email: Option<String>,
contact_phone: Option<String>,
admin_email: String,
status: OrgStatus,
contact_details: Option<JsonValue>,
sector: Option<String>,
industry: Option<String>,
created_at: chrono::NaiveDateTime,
updated_at: chrono::NaiveDateTime,
created_by: String,
updated_by: String,
}

impl From<Organisation> for OrganisationResponse {
fn from(org: Organisation) -> Self {
OrganisationResponse {
id: org.id,
country_code: org.country_code,
contact_email: org.contact_email,
contact_phone: org.contact_phone,
admin_email: org.admin_email,
status: org.status,
contact_details: org.contact_details,
sector: org.sector,
industry: org.industry,
created_at: org.created_at,
updated_at: org.updated_at,
created_by: org.created_by,
updated_by: org.updated_by,
}
}
}

#[derive(Deserialize, Default)]
pub struct OrganisationFilters {
pub page: Option<i64>,
pub size: Option<i64>,
pub sort_by: Option<SortBy>,
pub sort_on: Option<OrganisationSortOn>,
}

#[derive(Deserialize)]
pub enum SortBy {
Asc,
Desc,
}

#[derive(Deserialize)]
pub enum OrganisationSortOn {
CreatedAt,
Status,
}
1 change: 1 addition & 0 deletions crates/superposition/organisation/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod api;
13 changes: 13 additions & 0 deletions crates/superposition/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![deny(unused_crate_dependencies)]
mod app_state;

use idgenerator::{IdGeneratorOptions, IdInstance};
use std::{collections::HashSet, io::Result, time::Duration};

use actix_files::Files;
Expand Down Expand Up @@ -44,6 +45,13 @@ async fn main() -> Result<()> {
let service_prefix: String =
get_from_env_unsafe("SERVICE_PREFIX").expect("SERVICE_PREFIX is not set");

let options = IdGeneratorOptions::new()
.worker_id(1)
.worker_id_bit_len(8)
.seq_bit_len(12);

IdInstance::init(options).expect("Failed to initialize ID generator");

/*
Reading from a env returns a String at best we cannot obtain a &'static str from it,
which seems logical as it not known at compiletime, and there is no straightforward way to do this.
Expand Down Expand Up @@ -179,6 +187,11 @@ async fn main() -> Result<()> {
AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION),
),
)
.service(
scope("/organisation")
.wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::SUPERPOSITION))
.service(organisation::api::endpoints()),
)
/***************************** UI Routes ******************************/
.route("/fxn/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
Expand Down
Loading

0 comments on commit 778257c

Please sign in to comment.