Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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

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

2 changes: 2 additions & 0 deletions migrations/0005_template_table.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP VIEW instrument_template;
DROP TABLE template;
19 changes: 19 additions & 0 deletions migrations/0005_template_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Add new table for additional templates
CREATE TABLE template (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
template TEXT NOT NULL,
instrument INTEGER NOT NULL REFERENCES instrument(id) ON DELETE CASCADE ON UPDATE CASCADE,

CONSTRAINT duplicate_names UNIQUE (name, instrument) ON CONFLICT REPLACE,

CONSTRAINT empty_template CHECK (length(template) > 0),
CONSTRAINT empty_name CHECK (length(name) > 0)
);

CREATE VIEW instrument_template (instrument, name, template) AS
SELECT
instrument.name, template.name, template.template
FROM instrument
JOIN template
ON instrument.id = template.instrument;
107 changes: 106 additions & 1 deletion src/db_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use std::fmt;
use std::marker::PhantomData;
use std::path::Path;

pub use error::ConfigurationError;
use error::NewConfigurationError;
pub use error::{ConfigurationError, NamedTemplateError};
use sqlx::sqlite::{SqliteConnectOptions, SqliteRow};
use sqlx::{query_as, FromRow, QueryBuilder, Row, Sqlite, SqlitePool};
use tracing::{info, instrument, trace};
Expand All @@ -35,6 +35,21 @@ pub struct SqliteScanPathService {
pool: SqlitePool,
}

#[derive(Debug)]
pub struct NamedTemplate {
pub name: String,
pub template: String,
}

impl<'r> FromRow<'r, SqliteRow> for NamedTemplate {
fn from_row(row: &'r SqliteRow) -> Result<Self, sqlx::Error> {
Ok(Self {
name: row.try_get("name")?,
template: row.try_get("template")?,
})
}
}

#[derive(Debug, PartialEq, Eq)]
struct RawPathTemplate<F>(String, PhantomData<F>);

Expand Down Expand Up @@ -341,6 +356,54 @@ impl SqliteScanPathService {
.ok_or(ConfigurationError::MissingInstrument(instrument.into()))
}

pub async fn all_additional_templates(
&self,
instrument: &str,
) -> Result<Vec<NamedTemplate>, ConfigurationError> {
Ok(query_as!(
NamedTemplate,
"SELECT name, template FROM instrument_template WHERE instrument = ?",
instrument
)
.fetch_all(&self.pool)
.await?)
}
pub async fn additional_templates(
&self,
instrument: &str,
names: Vec<String>,
) -> Result<Vec<NamedTemplate>, ConfigurationError> {
let mut q =
QueryBuilder::new("SELECT name, template FROM instrument_template WHERE instrument = ");
q.push_bind(instrument);
q.push(" AND name IN (");
let mut name_query = q.separated(", ");
for name in names {
name_query.push_bind(name);
}
q.push(")");
let query = q.build_query_as();
Ok(query.fetch_all(&self.pool).await?)
}

pub async fn register_template(
&self,
instrument: &str,
name: String,
template: String,
) -> Result<NamedTemplate, NamedTemplateError> {
Ok(query_as!(
NamedTemplate,
"INSERT INTO template (name, template, instrument)
VALUES (?, ?, (SELECT id FROM instrument WHERE name = ?)) RETURNING name, template;",
name,
template,
instrument
)
.fetch_one(&self.pool)
.await?)
}

/// Create a db service from a new empty/schema-less DB
#[cfg(test)]
pub(crate) async fn uninitialised() -> Self {
Expand Down Expand Up @@ -368,7 +431,9 @@ impl fmt::Debug for SqliteScanPathService {
}

mod error {

use derive_more::{Display, Error, From};
use sqlx::error::ErrorKind;

#[derive(Debug, Display, Error, From)]
pub enum ConfigurationError {
Expand All @@ -392,6 +457,46 @@ mod error {
Self::MissingField(value.into())
}
}

#[derive(Debug, Display, Error)]
pub enum NamedTemplateError {
#[display("No configuration for instrument")]
MissingInstrument,
#[display("Template name was empty")]
EmptyName,
#[display("Template was empty")]
EmptyTemplate,
#[display("Error accessing named template: {_0}")]
DbError(sqlx::Error),
}

impl From<sqlx::Error> for NamedTemplateError {
fn from(value: sqlx::Error) -> Self {
match value {
sqlx::Error::Database(err) => match (err.kind(), err.message().split_once(": ")) {
(ErrorKind::NotNullViolation, Some((_, "template.instrument"))) => {
NamedTemplateError::MissingInstrument
}
// pretty sure these two are not possible as strings can't be null
(ErrorKind::NotNullViolation, Some((_, "template.name"))) => {
NamedTemplateError::EmptyName
}
(ErrorKind::NotNullViolation, Some((_, "template.template"))) => {
NamedTemplateError::EmptyTemplate
}
// Values are empty - these rely on the named checks in the schema
(ErrorKind::CheckViolation, Some((_, "empty_name"))) => {
NamedTemplateError::EmptyName
}
(ErrorKind::CheckViolation, Some((_, "empty_template"))) => {
NamedTemplateError::EmptyTemplate
}
(_, _) => NamedTemplateError::DbError(sqlx::Error::Database(err)),
},
err => err.into(),
}
}
}
}

#[cfg(test)]
Expand Down
93 changes: 92 additions & 1 deletion src/graphql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use std::any;
use std::borrow::Cow;
use std::collections::HashMap;
use std::future::Future;
use std::io::Write;
use std::path::{Component, PathBuf};
Expand Down Expand Up @@ -41,7 +42,7 @@ use tracing::{debug, info, instrument, trace, warn};
use crate::build_info::ServerStatus;
use crate::cli::ServeOptions;
use crate::db_service::{
InstrumentConfiguration, InstrumentConfigurationUpdate, SqliteScanPathService,
InstrumentConfiguration, InstrumentConfigurationUpdate, NamedTemplate, SqliteScanPathService,
};
use crate::numtracker::NumTracker;
use crate::paths::{
Expand Down Expand Up @@ -144,6 +145,7 @@ struct DirectoryPath {
/// GraphQL type to provide path data for the next scan for a given instrument session
struct ScanPaths {
directory: DirectoryPath,
extra_templates: HashMap<String, PathTemplate<ScanField>>,
subdirectory: Subdirectory,
}

Expand Down Expand Up @@ -224,6 +226,15 @@ impl ScanPaths {
self.directory.info.scan_number()
}

async fn template(&self, name: String) -> async_graphql::Result<String> {
Ok(path_to_string(
self.extra_templates
.get(&name)
.ok_or(NoSuchTemplate(name))?
.render(self),
)?)
}

/// The paths where the given detectors should write their files.
///
/// Detector names are normalised before being used in file names by replacing any
Expand Down Expand Up @@ -302,6 +313,27 @@ impl CurrentConfiguration {
}
}

#[derive(Debug, InputObject)]
struct NamedTemplateInput {
name: String,
template: InputTemplate<ScanTemplate>,
}

#[derive(Debug, Display, Error)]
#[display("Template {_0:?} not found")]
struct NoSuchTemplate(#[error(ignore)] String);

#[Object]
impl NamedTemplate {
async fn name(&self) -> &str {
&self.name
}

async fn template(&self) -> &str {
&self.template
}
}

impl FieldSource<ScanField> for ScanPaths {
fn resolve(&self, field: &ScanField) -> Cow<'_, str> {
match field {
Expand Down Expand Up @@ -384,6 +416,24 @@ impl Query {
.into_iter()
.collect()
}

#[instrument(skip(self, ctx))]
async fn named_templates<'ctx>(
&self,
ctx: &Context<'ctx>,
instrument: String,
names: Option<Vec<String>>,
) -> async_graphql::Result<Vec<NamedTemplate>> {
check_auth(ctx, |policy, token| {
policy.check_instrument_admin(token, &instrument)
})
.await?;
let db = ctx.data::<SqliteScanPathService>()?;
match names {
Some(names) => Ok(db.additional_templates(&instrument, names).await?),
None => Ok(db.all_additional_templates(&instrument).await?),
}
}
}

#[Object]
Expand Down Expand Up @@ -420,11 +470,35 @@ impl Mutation {
warn!("Failed to increment tracker file: {e}");
}

let required_templates = ctx
.field()
.selection_set()
.filter(|slct| slct.name() == "template")
.flat_map(|slct| slct.arguments())
.filter_map(|args| {
args.first().map(|arg| {
let Value::String(name) = &arg.1 else {
panic!("name isn't a string")
};
name.into()
})
})
.collect::<Vec<_>>();
let extra_templates = db
.additional_templates(&instrument, required_templates)
.await?
.into_iter()
.map(|template| {
ScanTemplate::new_checked(&template.template).map(|tmpl| (template.name, tmpl))
})
.collect::<Result<_, _>>()?;

Ok(ScanPaths {
directory: DirectoryPath {
instrument_session,
info: next_scan,
},
extra_templates,
subdirectory: sub.unwrap_or_default(),
})
}
Expand All @@ -451,6 +525,23 @@ impl Mutation {
};
CurrentConfiguration::for_config(db_config, nt).await
}

#[instrument(skip(self, ctx))]
async fn register_template<'ctx>(
&self,
ctx: &Context<'ctx>,
instrument: String,
template: NamedTemplateInput,
) -> async_graphql::Result<NamedTemplate> {
check_auth(ctx, |pc, token| {
pc.check_instrument_admin(token, &instrument)
})
.await?;
let db = ctx.data::<SqliteScanPathService>()?;
Ok(db
.register_template(&instrument, template.name, template.template.to_string())
.await?)
}
}

async fn check_auth<'ctx, Check, R>(ctx: &Context<'ctx>, check: Check) -> async_graphql::Result<()>
Expand Down
Loading
Loading