From 778257c1b183b7bbccda1b8e1ff82acc69a3d929 Mon Sep 17 00:00:00 2001 From: Saurav Suman Date: Fri, 20 Dec 2024 14:04:26 +0530 Subject: [PATCH] added schema and crl apis for organisation --- .env.example | 2 +- Cargo.lock | 31 ++++ Cargo.toml | 1 + crates/service_utils/src/service/types.rs | 1 + crates/superposition/Cargo.toml | 2 + crates/superposition/organisation/Cargo.toml | 21 +++ .../organisation/src/api/handlers.rs | 154 ++++++++++++++++++ .../superposition/organisation/src/api/mod.rs | 3 + .../organisation/src/api/types.rs | 82 ++++++++++ crates/superposition/organisation/src/lib.rs | 1 + crates/superposition/src/main.rs | 13 ++ .../down.sql | 8 + .../up.sql | 27 +++ .../src/database/models.rs | 2 + .../src/database/models/organisation.rs | 53 ++++++ .../src/database/schema.rs | 34 ++++ 16 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 crates/superposition/organisation/Cargo.toml create mode 100644 crates/superposition/organisation/src/api/handlers.rs create mode 100644 crates/superposition/organisation/src/api/mod.rs create mode 100644 crates/superposition/organisation/src/api/types.rs create mode 100644 crates/superposition/organisation/src/lib.rs create mode 100644 crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/down.sql create mode 100644 crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/up.sql create mode 100644 crates/superposition_types/src/database/models/organisation.rs diff --git a/.env.example b/.env.example index 8e81a063..1cfb6ceb 100644 --- a/.env.example +++ b/.env.example @@ -19,7 +19,7 @@ HOSTNAME="---" 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" diff --git a/Cargo.lock b/Cargo.lock index aa8474fe..5273dd3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2298,6 +2298,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idgenerator" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab32f68e287887b5f783055dac63971ae26c76be1ebde166c64d5bf5bdd5b6a" +dependencies = [ + "chrono", + "once_cell", + "parking_lot", + "thiserror", +] + [[package]] name = "idna" version = "0.5.0" @@ -3092,6 +3104,23 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "organisation" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "chrono", + "diesel", + "idgenerator", + "log", + "serde", + "serde_json", + "service_utils", + "superposition_macros", + "superposition_types", +] + [[package]] name = "os_str_bytes" version = "6.6.1" @@ -4061,8 +4090,10 @@ dependencies = [ "experimentation_platform", "fred", "frontend", + "idgenerator", "leptos", "leptos_actix", + "organisation", "reqwest", "rs-snowflake", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 3e216cf5..89b514ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/crates/service_utils/src/service/types.rs b/crates/service_utils/src/service/types.rs index 87c2a7ca..403ebc2b 100644 --- a/crates/service_utils/src/service/types.rs +++ b/crates/service_utils/src/service/types.rs @@ -74,6 +74,7 @@ impl FromStr for AppEnv { pub enum AppScope { CAC, EXPERIMENTATION, + SUPERPOSITION, } impl FromRequest for AppScope { type Error = Error; diff --git a/crates/superposition/Cargo.toml b/crates/superposition/Cargo.toml index 2481c6d2..89e80c72 100644 --- a/crates/superposition/Cargo.toml +++ b/crates/superposition/Cargo.toml @@ -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 } @@ -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 = [ diff --git a/crates/superposition/organisation/Cargo.toml b/crates/superposition/organisation/Cargo.toml new file mode 100644 index 00000000..c3ffd482 --- /dev/null +++ b/crates/superposition/organisation/Cargo.toml @@ -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" } \ No newline at end of file diff --git a/crates/superposition/organisation/src/api/handlers.rs b/crates/superposition/organisation/src/api/handlers.rs new file mode 100644 index 00000000..9c9bafd1 --- /dev/null +++ b/crates/superposition/organisation/src/api/handlers.rs @@ -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, + mut db_conn: DbConnection, +) -> superposition::Result { + 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, + mut db_conn: DbConnection, +) -> superposition::Result { + let org = + db_conn.transaction::<_, superposition::AppError, _>(|transaction_conn| { + organisation + .find(org_id.as_str()) + .first::(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, +) -> superposition::Result>> { + 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 + .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 = 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)) +} diff --git a/crates/superposition/organisation/src/api/mod.rs b/crates/superposition/organisation/src/api/mod.rs new file mode 100644 index 00000000..e155400f --- /dev/null +++ b/crates/superposition/organisation/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod handlers; +pub mod types; +pub use handlers::endpoints; diff --git a/crates/superposition/organisation/src/api/types.rs b/crates/superposition/organisation/src/api/types.rs new file mode 100644 index 00000000..88e3880f --- /dev/null +++ b/crates/superposition/organisation/src/api/types.rs @@ -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, + pub contact_email: Option, + pub contact_phone: Option, + pub created_by: String, + pub admin_email: String, + pub status: OrgStatus, + pub contact_details: Option, + pub sector: Option, + pub industry: Option, +} + +// 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, + contact_email: Option, + contact_phone: Option, + admin_email: String, + status: OrgStatus, + contact_details: Option, + sector: Option, + industry: Option, + created_at: chrono::NaiveDateTime, + updated_at: chrono::NaiveDateTime, + created_by: String, + updated_by: String, +} + +impl From 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, + pub size: Option, + pub sort_by: Option, + pub sort_on: Option, +} + +#[derive(Deserialize)] +pub enum SortBy { + Asc, + Desc, +} + +#[derive(Deserialize)] +pub enum OrganisationSortOn { + CreatedAt, + Status, +} diff --git a/crates/superposition/organisation/src/lib.rs b/crates/superposition/organisation/src/lib.rs new file mode 100644 index 00000000..e5fdf85e --- /dev/null +++ b/crates/superposition/organisation/src/lib.rs @@ -0,0 +1 @@ +pub mod api; diff --git a/crates/superposition/src/main.rs b/crates/superposition/src/main.rs index 649fe4e3..eddf4a99 100644 --- a/crates/superposition/src/main.rs +++ b/crates/superposition/src/main.rs @@ -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; @@ -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. @@ -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` diff --git a/crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/down.sql b/crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/down.sql new file mode 100644 index 00000000..30638532 --- /dev/null +++ b/crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/down.sql @@ -0,0 +1,8 @@ +-- down.sql +DROP INDEX IF EXISTS superposition.idx_organisation_admin_email; +DROP INDEX IF EXISTS superposition.idx_organisation_created_at; +DROP INDEX IF EXISTS superposition.idx_organisation_status; +DROP INDEX IF EXISTS superposition.idx_organisation_contact_email; +DROP TABLE IF EXISTS superposition.organisation; +DROP TYPE IF EXISTS superposition.org_status; +DROP SCHEMA IF EXISTS superposition; \ No newline at end of file diff --git a/crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/up.sql b/crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/up.sql new file mode 100644 index 00000000..e1a82f61 --- /dev/null +++ b/crates/superposition_types/migrations/2024-12-19-095525_create_organisation_table/up.sql @@ -0,0 +1,27 @@ +-- up.sql +CREATE SCHEMA IF NOT EXISTS superposition; + +CREATE TYPE superposition.org_status AS ENUM ('ACTIVE', 'INACTIVE', 'PENDINGKYB'); + +CREATE TABLE IF NOT EXISTS superposition.organisation ( + id VARCHAR(30) PRIMARY KEY NOT NULL, + country_code VARCHAR(10), + contact_email VARCHAR(255), + contact_phone VARCHAR(15), + created_by VARCHAR(255) NOT NULL, + admin_email VARCHAR(255) NOT NULL, + status superposition.org_status NOT NULL DEFAULT 'ACTIVE', + contact_details JSONB, + sector VARCHAR(100), + industry VARCHAR(100), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_by VARCHAR(255) NOT NULL +); + +-- Indexes for optimizing queries +CREATE INDEX IF NOT EXISTS idx_organisation_contact_email ON superposition.organisation (contact_email); +CREATE INDEX IF NOT EXISTS idx_organisation_status ON superposition.organisation (status); +CREATE INDEX IF NOT EXISTS idx_organisation_created_at ON superposition.organisation (created_at); +CREATE INDEX IF NOT EXISTS idx_organisation_admin_email ON superposition.organisation (admin_email); + diff --git a/crates/superposition_types/src/database/models.rs b/crates/superposition_types/src/database/models.rs index 448b838b..0bc052e0 100644 --- a/crates/superposition_types/src/database/models.rs +++ b/crates/superposition_types/src/database/models.rs @@ -2,3 +2,5 @@ pub mod cac; #[cfg(feature = "experimentation")] pub mod experimentation; + +pub mod organisation; diff --git a/crates/superposition_types/src/database/models/organisation.rs b/crates/superposition_types/src/database/models/organisation.rs new file mode 100644 index 00000000..b8a5e5a7 --- /dev/null +++ b/crates/superposition_types/src/database/models/organisation.rs @@ -0,0 +1,53 @@ +#[cfg(feature = "diesel_derives")] +use super::super::schema::*; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +#[cfg(feature = "diesel_derives")] +use diesel::{AsChangeset, Insertable, QueryId, Queryable, Selectable}; + +#[derive( + Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, +)] +#[serde(rename_all = "UPPERCASE")] +#[strum(serialize_all = "UPPERCASE")] +#[cfg_attr( + feature = "diesel_derives", + derive(diesel_derive_enum::DbEnum, QueryId) +)] +#[cfg_attr(feature = "diesel_derives", DbValueStyle = "UPPERCASE")] +#[cfg_attr( + feature = "diesel_derives", + ExistingTypePath = "crate::database::schema::sql_types::OrgStatus" +)] +pub enum OrgStatus { + ACTIVE, + INACTIVE, + PENDINGKYB, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[cfg_attr( + feature = "diesel_derives", + derive(Queryable, Selectable, Insertable, AsChangeset) +)] +#[cfg_attr(feature = "diesel_derives", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "diesel_derives", diesel(primary_key(id)))] +#[cfg_attr(feature = "diesel_derives", diesel(treat_none_as_null = true))] +#[cfg_attr(feature = "diesel_derives", diesel(table_name = organisation))] +pub struct Organisation { + pub id: String, + pub country_code: Option, + pub contact_email: Option, + pub contact_phone: Option, + pub created_by: String, + pub admin_email: String, + pub status: OrgStatus, + pub contact_details: Option, + pub sector: Option, + pub industry: Option, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub updated_by: String, +} diff --git a/crates/superposition_types/src/database/schema.rs b/crates/superposition_types/src/database/schema.rs index 5c19a230..12597f02 100644 --- a/crates/superposition_types/src/database/schema.rs +++ b/crates/superposition_types/src/database/schema.rs @@ -4,6 +4,9 @@ pub mod sql_types { #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "experiment_status_type"))] pub struct ExperimentStatusType; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "org_status"))] + pub struct OrgStatus; } diesel::table! { @@ -646,6 +649,36 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::OrgStatus; + + organisation (id) { + #[max_length = 30] + id -> Varchar, + #[max_length = 10] + country_code -> Nullable, + #[max_length = 255] + contact_email -> Nullable, + #[max_length = 15] + contact_phone -> Nullable, + #[max_length = 255] + created_by -> Varchar, + #[max_length = 255] + admin_email -> Varchar, + status -> OrgStatus, + contact_details -> Nullable, + #[max_length = 100] + sector -> Nullable, + #[max_length = 100] + industry -> Nullable, + created_at -> Timestamp, + updated_at -> Timestamp, + #[max_length = 255] + updated_by -> Varchar, + } +} + diesel::table! { type_templates (type_name) { type_name -> Text, @@ -710,5 +743,6 @@ diesel::allow_tables_to_appear_in_same_query!( event_log_y2026m12, experiments, functions, + organisation, type_templates, );