diff --git a/.sqlx/query-22237747eeb6181b9e7818733abe10e8936995349517ac2f31f2e3fe215943b5.json b/.sqlx/query-22237747eeb6181b9e7818733abe10e8936995349517ac2f31f2e3fe215943b5.json new file mode 100644 index 0000000..00f9e4d --- /dev/null +++ b/.sqlx/query-22237747eeb6181b9e7818733abe10e8936995349517ac2f31f2e3fe215943b5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO instrument (name, scan_number, directory, scan, detector, tracker_file_extension) VALUES (?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "22237747eeb6181b9e7818733abe10e8936995349517ac2f31f2e3fe215943b5" +} diff --git a/.sqlx/query-6a62dc83c7ad9074b5ebf42ff730949236027cd2523b54c47a23382312cab6d8.json b/.sqlx/query-6a62dc83c7ad9074b5ebf42ff730949236027cd2523b54c47a23382312cab6d8.json new file mode 100644 index 0000000..c938239 --- /dev/null +++ b/.sqlx/query-6a62dc83c7ad9074b5ebf42ff730949236027cd2523b54c47a23382312cab6d8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM instrument", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "6a62dc83c7ad9074b5ebf42ff730949236027cd2523b54c47a23382312cab6d8" +} diff --git a/src/db_service.rs b/src/db_service.rs index 0445014..522ca29 100644 --- a/src/db_service.rs +++ b/src/db_service.rs @@ -16,8 +16,9 @@ use std::fmt; use std::marker::PhantomData; use std::path::Path; -pub use error::ConfigurationError; use error::NewConfigurationError; +pub use error::{ConfigurationError, InsertConfigurationsError}; +use serde::{Deserialize, Serialize}; use sqlx::sqlite::{SqliteConnectOptions, SqliteRow}; use sqlx::{query_as, FromRow, QueryBuilder, Row, Sqlite, SqlitePool}; use tracing::{info, instrument, trace}; @@ -220,7 +221,7 @@ impl InstrumentConfigurationUpdate { } } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] struct DbInstrumentConfig { #[allow(unused)] // unused but allows use of 'SELECT * ...' queries id: Option, @@ -323,6 +324,47 @@ impl SqliteScanPathService { .collect()) } + pub async fn insert_configurations( + &self, + configs: &[InstrumentConfiguration], + force_clear: bool, + ) -> Result<(), InsertConfigurationsError> { + let mut tx = self.pool.begin().await?; + + if force_clear { + //User has chosen to overwrite existing data so delete the table before inserting new rows + sqlx::query!("DELETE FROM instrument") + .execute(&mut *tx) + .await?; + } else if !configs.is_empty() { + // Not forcing clear, check if table is empty + let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM instrument") + .fetch_one(&mut *tx) + .await?; + if count > 0 { + //Table not empty, do not clear + return Err(InsertConfigurationsError::NotEmpty); + } + } + + for config in configs { + sqlx::query!( + "INSERT INTO instrument (name, scan_number, directory, scan, detector, tracker_file_extension) VALUES (?, ?, ?, ?, ?, ?)", + config.name, + config.scan_number, + config.directory.0, + config.scan.0, + config.detector.0, + config.tracker_file_extension + ) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(()) + } + pub async fn next_scan_configuration( &self, instrument: &str, @@ -392,6 +434,15 @@ mod error { Self::MissingField(value.into()) } } + + #[derive(Debug, Display, Error, From)] + pub enum InsertConfigurationsError { + #[display("Instrument table not empty and force_clear not set to true")] + NotEmpty, + #[from] + #[display("Error inserting configurations: {_0}")] + Db(sqlx::Error), + } } #[cfg(test)] diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs index 707d42b..2a49e08 100644 --- a/src/graphql/mod.rs +++ b/src/graphql/mod.rs @@ -26,6 +26,7 @@ use async_graphql::{ }; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use auth::{AuthError, PolicyCheck}; +use axum::extract::State; use axum::http::StatusCode; use axum::response::{Html, IntoResponse}; use axum::routing::{get, post}; @@ -35,6 +36,8 @@ use axum_extra::headers::Authorization; use axum_extra::TypedHeader; use chrono::{Datelike, Local}; use derive_more::{Display, Error}; +use serde::Deserialize; +use serde::de::value::MapDeserializer; use tokio::net::TcpListener; use tokio::select; use tokio::signal::unix::{signal, SignalKind}; @@ -42,8 +45,7 @@ use tracing::{debug, info, instrument, trace, warn}; use crate::build_info::ServerStatus; use crate::cli::ServeOptions; -use crate::db_service::{ - InstrumentConfiguration, InstrumentConfigurationUpdate, SqliteScanPathService, +use crate::db_service::{InsertConfigurationsError,InstrumentConfiguration, InstrumentConfigurationUpdate, SqliteScanPathService, }; use crate::numtracker::NumTracker; use crate::paths::{ @@ -67,7 +69,7 @@ pub async fn serve_graphql(opts: ServeOptions) { let schema = Schema::build(Query, Mutation, EmptySubscription) .extension(Tracing) .limit_directives(32) - .data(db) + .data(db.clone()) .data(directory_numtracker) .data(opts.policy.map(PolicyCheck::new)) .finish(); @@ -75,6 +77,8 @@ pub async fn serve_graphql(opts: ServeOptions) { // status check endpoint allows external processes to monitor status of server without // making graphql queries .route("/status", get(server_status)) + .route("/admin/export", get(export_handler)) + .route("/admin/restore", post(restore_handler)) .route("/graphql", post(graphql_handler)) // make it obvious that /graphql isn't expected to work when visiting from a browser .route( @@ -88,6 +92,7 @@ pub async fn serve_graphql(opts: ServeOptions) { // Interactive graphiql playground .route("/graphiql", get(graphiql)) // Make it look less like something is broken when going to any other page + .with_state(db) .fallback(( StatusCode::NOT_FOUND, Html(include_str!("../../static/404.html")), @@ -102,6 +107,28 @@ pub async fn serve_graphql(opts: ServeOptions) { .expect("Can't serve graphql endpoint"); } +<<<<<<< HEAD +async fn export_handler(State(db): State,) -> Result>, (StatusCode)> { + let configs = db.all_configurations().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(configs)) +} + +async fn restore_handler( + State(db): State, + Query(params): Query, + Json(configs): Json>, +) -> Result { + db.insert_configurations(&configs, params.force_clear) + .await + .map_err(|e|match e{InsertConfigurationsError::NotEmpty => StatusCode::CONFLICT, InstrumentConfigurationError::Db(_) =>StatusCode::INTERNAL_SERVER_ERROR})?; + Ok("Configurations restored".into()) +======= +async fn export_handler(State(db): State) -> String { + let configs = db.all_configurations().await; + return format!("{configs:?}"); +>>>>>>> 6f2341aa6c32228eb280afc426ca7d77aaca598f +} + async fn create_signal_handler() { let mut term = signal(SignalKind::terminate()).expect("Failed to create SIGTERM listener"); let mut int = signal(SignalKind::interrupt()).expect("Failed to create SIGINT listener"); @@ -135,6 +162,12 @@ async fn graphql_handler( .into() } +#[derive(Debug, Deserialize)] +struct ImportParams{ + #[serde(default)] + force_clear: bool, +} + /// Read-only API for GraphQL struct Query;