diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index de177af6..7e7999a7 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -14,6 +14,7 @@ jobs: - chimp_chomp - chimp_controller - compound_library + - crystal_library - pin_packing - soakdb_sync - targeting diff --git a/backend/Cargo.lock b/backend/Cargo.lock index bc22012a..8e6026db 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" dependencies = [ "cfg-if", "once_cell", @@ -323,7 +323,7 @@ dependencies = [ "proc-macro2", "quote", "strum", - "syn 2.0.49", + "syn 2.0.50", "thiserror", ] @@ -441,7 +441,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -458,7 +458,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -865,7 +865,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1196,7 +1196,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1353,6 +1353,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "crystal_library" +version = "0.1.0" +dependencies = [ + "async-graphql", + "axum", + "chrono", + "clap 4.5.1", + "dotenvy", + "graphql_endpoints", + "opa_client", + "sea-orm", + "sea-orm-migration", + "the_paginator", + "tokio", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + [[package]] name = "cynic" version = "3.4.3" @@ -1382,7 +1403,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.49", + "syn 2.0.50", "thiserror", ] @@ -1395,7 +1416,7 @@ dependencies = [ "cynic-codegen", "darling", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1419,7 +1440,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1430,7 +1451,7 @@ checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be" dependencies = [ "darling_core", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1796,7 +1817,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -1963,7 +1984,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.8", + "ahash 0.8.9", "allocator-api2", ] @@ -2681,9 +2702,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -2702,7 +2723,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -2713,9 +2734,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "ae94056a791d0e1217d18b6cbdccb02c61e3054fc69893607f4067e3bb0b1fd1" dependencies = [ "cc", "libc", @@ -2793,7 +2814,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -2931,7 +2952,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -3000,7 +3021,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -3159,7 +3180,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", "version_check", "yansi", ] @@ -3281,7 +3302,7 @@ checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -3758,29 +3779,29 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -4091,7 +4112,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -4113,9 +4134,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.49" +version = "2.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" +checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" dependencies = [ "proc-macro2", "quote", @@ -4245,7 +4266,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -4328,7 +4349,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -4466,7 +4487,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] @@ -4773,7 +4794,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", "wasm-bindgen-shared", ] @@ -4807,7 +4828,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5094,7 +5115,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.49", + "syn 2.0.50", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index fc971ea5..0905685b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -2,7 +2,8 @@ members = [ "chimp_chomp", "chimp_controller", - "chimp_protocol", + "chimp_protocol", + "crystal_library", "compound_library", "graphql_endpoints", "graphql_event_broker", diff --git a/backend/Dockerfile b/backend/Dockerfile index db28e4eb..f63e9f2f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -12,6 +12,7 @@ COPY chimp_chomp/Cargo.toml chimp_chomp/Cargo.toml COPY chimp_controller/Cargo.toml chimp_controller/Cargo.toml COPY chimp_protocol/Cargo.toml chimp_protocol/Cargo.toml COPY compound_library/Cargo.toml compound_library/Cargo.toml +COPY crystal_library/Cargo.toml crystal_library/Cargo.toml COPY graphql_endpoints/Cargo.toml graphql_endpoints/Cargo.toml COPY graphql_event_broker/Cargo.toml graphql_event_broker/Cargo.toml COPY opa_client/Cargo.toml opa_client/Cargo.toml @@ -28,6 +29,8 @@ RUN mkdir chimp_chomp/src \ && echo "fn main() {}" > chimp_controller/src/main.rs \ && mkdir compound_library/src \ && echo "fn main() {}" > compound_library/src/main.rs \ + && mkdir crystal_library/src \ + && echo "fn main() {}" > crystal_library/src/main.rs \ && mkdir graphql_endpoints/src \ && touch graphql_endpoints/src/lib.rs \ && mkdir graphql_event_broker/src \ @@ -53,6 +56,7 @@ RUN touch chimp_chomp/src/main.rs \ && touch chimp_protocol/src/lib.rs \ && touch chimp_controller/src/main.rs \ && touch compound_library/src/main.rs \ + && touch crystal_library/src/main.rs \ && touch graphql_endpoints/src/lib.rs \ && touch graphql_event_broker/src/lib.rs \ && touch opa_client/src/lib.rs \ @@ -97,6 +101,12 @@ COPY --from=build /app/target/release/compound_library /compound_library ENTRYPOINT ["/compound_library"] +FROM gcr.io/distroless/cc as crystal_library + +COPY --from=build /app/target/release/crystal_library /crystal_library + +ENTRYPOINT ["/crystal_library"] + FROM gcr.io/distroless/cc as soakdb_sync COPY --from=build /app/target/release/soakdb_sync /soakdb_sync diff --git a/backend/crystal_library/Cargo.toml b/backend/crystal_library/Cargo.toml new file mode 100644 index 00000000..332fdd44 --- /dev/null +++ b/backend/crystal_library/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "crystal_library" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-graphql = { workspace = true } +axum = { workspace = true } +clap = { workspace = true } +chrono ={ workspace = true } +dotenvy = { workspace = true } +graphql_endpoints = { path = "../graphql_endpoints" } +opa_client = { path = "../opa_client", features = ["graphql"] } +sea-orm = { workspace = true, features = ["sqlx-postgres"] } +sea-orm-migration = { workspace = true } +the_paginator = { version = "0.1.0", path = "../the_paginator", features = [ + "async-graphql", +] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tokio = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } diff --git a/backend/crystal_library/README.md b/backend/crystal_library/README.md new file mode 100644 index 00000000..aa00147d --- /dev/null +++ b/backend/crystal_library/README.md @@ -0,0 +1,3 @@ +# Crystal Library Service + +This service keeps track of all the crystals used for expeiments at the Diamond Light Source. diff --git a/backend/crystal_library/src/graphql/crystal_plates_res.rs b/backend/crystal_library/src/graphql/crystal_plates_res.rs new file mode 100644 index 00000000..0a62ac34 --- /dev/null +++ b/backend/crystal_library/src/graphql/crystal_plates_res.rs @@ -0,0 +1,78 @@ +use crate::tables::{crystal_plates, crystal_wells}; +use async_graphql::{ComplexObject, Context, Object}; +use chrono::Utc; +use opa_client::subject_authorization; +use sea_orm::{ActiveValue, DatabaseConnection, EntityTrait, ModelTrait}; +use the_paginator::graphql::{CursorInput, ModelConnection}; +use uuid::Uuid; + +/// CrystalQuery is a type that represents all the queries for the crystals. +#[derive(Debug, Clone, Default)] +pub struct CrystalPlatesQuery; + +/// CrystalMutation is a type that represents all the mutations for the crystals. +#[derive(Debug, Clone, Default)] +pub struct CrystalPlatesMutation; + +#[ComplexObject] +impl crystal_plates::Model { + /// This function fetches all crystal well on the crytal plate + async fn wells(&self, ctx: &Context<'_>) -> async_graphql::Result> { + subject_authorization!("xchemlab.crystal_library.read_crystal_plates", ctx).await?; + let db = ctx.data::()?; + Ok(self.find_related(crystal_wells::Entity).all(db).await?) + } +} + +#[Object] +impl CrystalPlatesQuery { + /// Fetches all crystal plates from the database. + async fn crystal_plates( + &self, + ctx: &Context<'_>, + cursor: CursorInput, + ) -> async_graphql::Result> { + subject_authorization!("xchemlab.crystal_library.read_crystal_plates", ctx).await?; + let db = ctx.data::()?; + Ok(cursor + .try_into_query_cursor::()? + .all(db) + .await? + .try_into_connection()?) + } + + /// Fetches a single crystal plate using the plate_id. + async fn crystal_plate( + &self, + ctx: &Context<'_>, + plate_id: Uuid, + ) -> async_graphql::Result> { + subject_authorization!("xchemlab.crystal_library.read_crystal_plates", ctx).await?; + let db = ctx.data::()?; + Ok(crystal_plates::Entity::find_by_id(plate_id).one(db).await?) + } +} + +#[Object] +impl CrystalPlatesMutation { + /// Adds a crystal plates to the database + async fn add_crystal_plate( + &self, + ctx: &Context<'_>, + plate_id: Uuid, + proposal_number: i32, + ) -> async_graphql::Result { + let operator_id = + subject_authorization!("xchemlab.crystal_library.write_crystal_plates", ctx).await?; + let db = ctx.data::()?; + let crystal = crystal_plates::ActiveModel { + plate_id: ActiveValue::Set(plate_id), + proposal_number: ActiveValue::Set(proposal_number), + operator_id: ActiveValue::Set(operator_id), + timestamp: ActiveValue::Set(Utc::now()), + }; + Ok(crystal_plates::Entity::insert(crystal) + .exec_with_returning(db) + .await?) + } +} diff --git a/backend/crystal_library/src/graphql/crystal_wells_res.rs b/backend/crystal_library/src/graphql/crystal_wells_res.rs new file mode 100644 index 00000000..16e6cd08 --- /dev/null +++ b/backend/crystal_library/src/graphql/crystal_wells_res.rs @@ -0,0 +1,86 @@ +use crate::tables::{crystal_plates, crystal_wells}; +use async_graphql::{ComplexObject, Context, Object}; +use chrono::Utc; +use opa_client::subject_authorization; +use sea_orm::{ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter}; +use the_paginator::graphql::{CursorInput, ModelConnection}; +use uuid::Uuid; + +/// CrystalQuery is a type that represents all the queries for the crystals. +#[derive(Debug, Clone, Default)] +pub struct CrystalWellsQuery; + +/// CrystalMutation is a type that represents all the mutations for the crystals. +#[derive(Debug, Clone, Default)] +pub struct CrystalWellsMutation; + +#[ComplexObject] +impl crystal_wells::Model { + /// Fetches all crystal well on the crytal plate + async fn plate( + &self, + ctx: &Context<'_>, + ) -> async_graphql::Result> { + subject_authorization!("xchemlab.crystal_library.read_crystal_wells", ctx).await?; + let db = ctx.data::()?; + Ok(self.find_related(crystal_plates::Entity).one(db).await?) + } +} + +#[Object] +impl CrystalWellsQuery { + /// Fetches all crystals related from the database. + async fn crystal_wells( + &self, + ctx: &Context<'_>, + cursor: CursorInput, + ) -> async_graphql::Result> { + subject_authorization!("xchemlab.crystal_library.read_crystal_wells", ctx).await?; + let db = ctx.data::()?; + Ok(cursor + .try_into_query_cursor::()? + .all(db) + .await? + .try_into_connection()?) + } + + /// Fetches a single crystal well using the plate_id and well number. + async fn crystal_well( + &self, + ctx: &Context<'_>, + plate_id: Uuid, + well_number: i16, + ) -> async_graphql::Result> { + subject_authorization!("xchemlab.crystal_library.read_crystal_wells", ctx).await?; + let db = ctx.data::()?; + Ok(crystal_wells::Entity::find() + .filter(crystal_wells::Column::PlateId.eq(plate_id)) + .filter(crystal_wells::Column::WellNumber.eq(well_number)) + .one(db) + .await?) + } +} + +#[Object] +impl CrystalWellsMutation { + /// Adds a crystal well to the database + async fn add_crystal_well( + &self, + ctx: &Context<'_>, + plate_id: Uuid, + #[graphql(validator(minimum = 1, maximum = 288))] well_number: i16, + ) -> async_graphql::Result { + let operator_id = + subject_authorization!("xchemlab.crystal_library.write_crystal_wells", ctx).await?; + let db = ctx.data::()?; + let crystal = crystal_wells::ActiveModel { + plate_id: ActiveValue::Set(plate_id), + well_number: ActiveValue::Set(well_number), + operator_id: ActiveValue::Set(operator_id), + timestamp: ActiveValue::Set(Utc::now()), + }; + Ok(crystal_wells::Entity::insert(crystal) + .exec_with_returning(db) + .await?) + } +} diff --git a/backend/crystal_library/src/graphql/mod.rs b/backend/crystal_library/src/graphql/mod.rs new file mode 100644 index 00000000..77b81771 --- /dev/null +++ b/backend/crystal_library/src/graphql/mod.rs @@ -0,0 +1,25 @@ +/// A collection of resolvers relating to crystal plates +pub mod crystal_plates_res; +/// A collection of resolvers relating to crystal wells +pub mod crystal_wells_res; + +use async_graphql::{EmptySubscription, MergedObject, Schema, SchemaBuilder}; +use crystal_plates_res::{CrystalPlatesMutation, CrystalPlatesQuery}; +use crystal_wells_res::{CrystalWellsMutation, CrystalWellsQuery}; + +/// Combines all query resolvers into a single GraphQL `Query` type. +#[derive(Debug, Clone, MergedObject, Default)] +pub struct Query(CrystalWellsQuery, CrystalPlatesQuery); + +/// Combines all mutation resolvers into a single GraphQL `Query` type. +#[derive(Debug, Clone, MergedObject, Default)] +pub struct Mutation(CrystalWellsMutation, CrystalPlatesMutation); + +/// Type alias for the complete GraphQL schema. +pub type RootSchema = Schema; + +/// This function initializes the schema with default instances of `Query`, +/// `Mutation`, and `EmptySubscription`. +pub fn root_schema_builder() -> SchemaBuilder { + Schema::build(Query::default(), Mutation::default(), EmptySubscription).enable_federation() +} diff --git a/backend/crystal_library/src/main.rs b/backend/crystal_library/src/main.rs new file mode 100644 index 00000000..c656482c --- /dev/null +++ b/backend/crystal_library/src/main.rs @@ -0,0 +1,143 @@ +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] +#![doc=include_str!("../README.md")] + +/// This module sets up the GraphQL schema, including queries, mutations, +/// and subscriptions. It defines how data is queried and mutated through the API. +mod graphql; +/// This module is responsible for defining and applying database migrations. +mod migrator; +/// This module defines the structure and schema of the database tables +/// through various entity structs. +mod tables; + +use async_graphql::extensions::Tracing; +use axum::{routing::get, Router, Server}; +use clap::Parser; +use graphql::{root_schema_builder, RootSchema}; +use graphql_endpoints::{GraphQLHandler, GraphQLSubscription, GraphiQLHandler}; +use opa_client::OPAClient; +use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr, TransactionError}; +use sea_orm_migration::MigratorTrait; +use std::{ + fs::File, + io::Write, + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + path::PathBuf, +}; +use url::Url; + +/// A service for tracking crystals available in the XChem lab +#[derive(Debug, Parser)] +#[command(author, version, about, long_about = None)] +enum Cli { + /// Starts a webserver serving the GraphQL API + Serve(ServeArgs), + /// Prints the GraphQL API to stdout + Schema(SchemaArgs), +} + +#[derive(Debug, Parser)] +#[allow(clippy::missing_docs_in_private_items)] +struct ServeArgs { + /// The port number to serve on + #[arg(short, long, default_value_t = 80)] + port: u16, + /// URL for the database + #[arg(long, env)] + database_url: Url, + /// URL for the OPA server + #[arg(long, env)] + opa_url: Url, +} + +/// Arguments for the `schema` command +#[derive(Debug, Parser)] +struct SchemaArgs { + /// Specifies an optional path to the file to save the schema + #[arg(short, long)] + path: Option, +} + +/// Sets up the database connection and performs the migrations +/// The database name is set of compound_library if not provided +/// +/// Returns a `Result` with a `DatabaseConnection` on success, +/// or a `TransactionError` if connecting to the database or running +/// migrations fails +async fn setup_database( + mut database_url: Url, +) -> Result> { + if database_url.path().is_empty() { + database_url.set_path("crystal_library"); + } + let connection_options = ConnectOptions::new(database_url.to_string()); + let connection = Database::connect(connection_options).await?; + migrator::Migrator::up(&connection, None).await?; + Ok(connection) +} + +/// Sets up the router for handling GraphQL queries and subscriptions +/// Returns a `Router` configured with routes +fn setup_router(schema: RootSchema) -> Router { + /// The endpoint for handling GraphQL queries and mutations + const GRAPHQL_ENDPOINT: &str = "/"; + /// The endpoint for establishing WebSocket connections for GraphQL subscriptions + const SUBSCRIPTION_ENDPOINT: &str = "/ws"; + + Router::new() + .route( + GRAPHQL_ENDPOINT, + get(GraphiQLHandler::new( + GRAPHQL_ENDPOINT, + SUBSCRIPTION_ENDPOINT, + )) + .post(GraphQLHandler::new(schema.clone())), + ) + .route_service(SUBSCRIPTION_ENDPOINT, GraphQLSubscription::new(schema)) +} + +/// Starts a web server to handle HTTP requests as defined in the provided `router` +async fn serve(router: Router, port: u16) { + let socket_addr: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)); + println!("GraphiQL IDE: {}", socket_addr); + Server::bind(&socket_addr) + .serve(router.into_make_service()) + .await + .unwrap(); +} + +#[tokio::main] +async fn main() { + dotenvy::dotenv().ok(); + let args = Cli::parse(); + let tracing_subscriber = tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .finish(); + tracing::subscriber::set_global_default(tracing_subscriber).unwrap(); + + match args { + Cli::Serve(args) => { + let db = setup_database(args.database_url).await.unwrap(); + let opa_client = OPAClient::new(args.opa_url); + let schema = root_schema_builder() + .data(db) + .data(opa_client) + .extension(Tracing) + .finish(); + let router = setup_router(schema); + serve(router, args.port).await; + } + Cli::Schema(args) => { + let schema = root_schema_builder().finish(); + let schema_string = schema.sdl(); + if let Some(path) = args.path { + let mut file = File::create(path).unwrap(); + file.write_all(schema_string.as_bytes()).unwrap(); + } else { + println!("{}", schema_string); + } + } + } +} diff --git a/backend/crystal_library/src/migrator.rs b/backend/crystal_library/src/migrator.rs new file mode 100644 index 00000000..91714411 --- /dev/null +++ b/backend/crystal_library/src/migrator.rs @@ -0,0 +1,37 @@ +use crate::tables::{crystal_plates, crystal_wells}; +use axum::async_trait; +use sea_orm::{DbErr, DeriveMigrationName, Schema}; +use sea_orm_migration::{MigrationTrait, MigratorTrait, SchemaManager}; + +/// Migrator for managing and applying database migrations. +pub struct Migrator; + +/// This struct is used to define the very first migration that sets up +/// the initial database schema. +#[derive(DeriveMigrationName)] +struct Initial; + +#[async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(Initial)] + } +} + +#[async_trait] +impl MigrationTrait for Initial { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let backend = manager.get_database_backend(); + let schema = Schema::new(backend); + + manager + .create_table(schema.create_table_from_entity(crystal_plates::Entity)) + .await?; + + manager + .create_table(schema.create_table_from_entity(crystal_wells::Entity)) + .await?; + + Ok(()) + } +} diff --git a/backend/crystal_library/src/tables/crystal_plates.rs b/backend/crystal_library/src/tables/crystal_plates.rs new file mode 100644 index 00000000..cd595a70 --- /dev/null +++ b/backend/crystal_library/src/tables/crystal_plates.rs @@ -0,0 +1,40 @@ +use super::crystal_wells; +use async_graphql::SimpleObject; +use chrono::{DateTime, Utc}; +use sea_orm::{ + ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, + EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, +}; +use uuid::Uuid; + +/// Represents a plate on which crystals are located. +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SimpleObject)] +#[sea_orm(table_name = "crystal_plates")] +#[graphql(name = "crystal_plates", complex)] +pub struct Model { + /// ID of the plate on which the crystal is located. + #[sea_orm(primary_key, auto_increment = false)] + pub plate_id: Uuid, + /// Project proposal number + pub proposal_number: i32, + /// The identifier of the operator which added this entry. + pub operator_id: String, + /// The date and time when the compound instance was added. + pub timestamp: DateTime, +} + +/// Defines the relationships between entities in the database schema +#[derive(Clone, Copy, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + /// Defines the relations between the crystal wells and crystal plates + #[sea_orm(has_many = "crystal_wells::Entity")] + CrystalWells, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CrystalWells.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/crystal_library/src/tables/crystal_wells.rs b/backend/crystal_library/src/tables/crystal_wells.rs new file mode 100644 index 00000000..2b087db2 --- /dev/null +++ b/backend/crystal_library/src/tables/crystal_wells.rs @@ -0,0 +1,45 @@ +use super::crystal_plates; +use async_graphql::SimpleObject; +use chrono::{DateTime, Utc}; +use sea_orm::{ + ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, DeriveRelation, EntityTrait, + EnumIter, PrimaryKeyTrait, Related, RelationDef, RelationTrait, +}; +use uuid::Uuid; + +/// Represents a crystal within the database. +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SimpleObject)] +#[sea_orm(table_name = "crystal_wells")] +#[graphql(name = "crystal_wells", complex)] +pub struct Model { + /// ID of the plate on which the crystal is located. + #[sea_orm(primary_key, auto_increment = false)] + pub plate_id: Uuid, + /// The well on the plate which the crystal is located. + #[sea_orm(primary_key, auto_increment = false)] + pub well_number: i16, + /// The identifier of the operator which added this entry. + pub operator_id: String, + /// The date and time when the compound instance was added. + pub timestamp: DateTime, +} + +/// Defines the relationships between entities in the database schema +#[derive(Clone, Copy, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "crystal_plates::Entity", + from = "Column::PlateId", + to = "crystal_plates::Column::PlateId" + )] + /// Defines the relations between the crystal plates and crystal wells + CrystalPlates, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CrystalPlates.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/backend/crystal_library/src/tables/mod.rs b/backend/crystal_library/src/tables/mod.rs new file mode 100644 index 00000000..74d7efd7 --- /dev/null +++ b/backend/crystal_library/src/tables/mod.rs @@ -0,0 +1,4 @@ +/// The table storing crystal plate information +pub mod crystal_plates; +/// The table storing crystal well information +pub mod crystal_wells; diff --git a/policies/crystal_library.rego b/policies/crystal_library.rego new file mode 100644 index 00000000..c76e5b63 --- /dev/null +++ b/policies/crystal_library.rego @@ -0,0 +1,24 @@ +package xchemlab.crystal_library + +import data.xchemlab + +default read_crystal_wells = {"allowed" : false} +default write_crystal_wells = {"allowed" : false} +default read_crystal_plates = {"allowed" : false} +default write_crystal_plates = {"allowed" : false} + +read_crystal_wells = {"allowed": true, "subject": xchemlab.subject} { + xchemlab.valid_token +} + +write_crystal_wells = {"allowed" : true, "subject" : xchemlab.subject} { + xchemlab.valid_token +} + +read_crystal_plates = {"allowed": true, "subject": xchemlab.subject} { + xchemlab.valid_token +} + +write_crystal_plates = {"allowed" : true, "subject" : xchemlab.subject} { + xchemlab.valid_token +}