Skip to content

Commit

Permalink
sample implementation of dataloader for canteen
Browse files Browse the repository at this point in the history
  • Loading branch information
worldofjoni committed Aug 29, 2023
1 parent abd23fc commit 47e1b83
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 24 deletions.
38 changes: 35 additions & 3 deletions backend/Cargo.lock

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

2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ chrono = "0.4.26"
thiserror = "1.0.40"
uuid = "1.4.0"
axum = {version = "0.6.18", features = ["http2", "macros"]}
async-graphql = {version = "5.0.10", features = ["chrono", "uuid", "tracing", "apollo_tracing"]}
async-graphql = {version = "5.0.10", features = ["chrono", "uuid", "tracing", "apollo_tracing", "dataloader"]}
async-graphql-axum = "5.0.10"
tokio = {version = "1.29.0", features = ["full"]}
tracing = "0.1"
Expand Down
21 changes: 16 additions & 5 deletions backend/src/interface/persistent_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use async_trait::async_trait;
use sqlx::migrate::MigrateError;
use std::collections::HashMap;
use std::num::TryFromIntError;
use std::sync::Arc;
use thiserror::Error;

pub type Result<T> = std::result::Result<T, DataError>;
Expand All @@ -19,7 +20,7 @@ pub enum DataError {
NoSuchItem,
/// Error occurred during data request or an internal connection fault.
#[error("internal error ocurred: {0}")]
InternalError(#[from] sqlx::Error),
InternalError(Arc<sqlx::Error>),
/// Failed to convert integers.
#[error("error converting type: {0}")]
TypeConversionError(#[from] TryFromIntError),
Expand All @@ -28,7 +29,19 @@ pub enum DataError {
UnexpectedNullError(String),
/// Database migration could not be run.
#[error("error while running database migration: {0}")]
MigrateError(#[from] MigrateError),
MigrateError(Arc<MigrateError>),
}

impl From<sqlx::Error> for DataError {
fn from(value: sqlx::Error) -> Self {
Self::InternalError(Arc::new(value))
}
}

impl From<MigrateError> for DataError {
fn from(value: MigrateError) -> Self {
Self::MigrateError(Arc::new(value))
}
}

/// Extracts a value from an option by returning an [`DataError::UnexpectedNullError`] using [`std::ops::Try`] (`?`).
Expand Down Expand Up @@ -200,7 +213,7 @@ pub trait CommandDataAccess: Sync + Send {

#[async_trait]
/// An interface for graphql query data. The GraphQL component uses this interface for database access.
pub trait RequestDataAccess {
pub trait RequestDataAccess: Send + Sync {
/// Returns the canteen from the database.
async fn get_canteen(&self, id: Uuid) -> Result<Option<Canteen>>;
/// Returns all canteens from the database.
Expand Down Expand Up @@ -238,8 +251,6 @@ pub trait RequestDataAccess {
async fn get_canteen_multi(&self, id: &[Uuid]) -> Result<HashMap<Uuid, Option<Canteen>>> {
todo!()
}
/// Returns all canteens from the database.
async fn get_canteens_multi(&self) -> Result<HashMap<Uuid, Vec<Canteen>>> { todo!() }
/// Returns the line from the database.
async fn get_line_multi(&self, id: &[Uuid]) -> Result<HashMap<Uuid, Option<Line>>> { todo!() }
/// Returns all lines of a canteen from the database.
Expand Down
2 changes: 1 addition & 1 deletion backend/src/interface/persistent_data/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct ApiKey {
}

/// Struct for database-operations. Related to the database entity 'canteen'.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Canteen {
/// Identification of the canteen
pub id: Uuid,
Expand Down
20 changes: 20 additions & 0 deletions backend/src/layer/data/database/request.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use async_trait::async_trait;
use chrono::{Duration, Local};
use sqlx::{Pool, Postgres};
Expand Down Expand Up @@ -303,6 +305,24 @@ impl RequestDataAccess for PersistentRequestData {
.await?;
Ok(res)
}

// --- multi ---

async fn get_canteen_multi(&self, id: &[Uuid]) -> Result<HashMap<Uuid, Option<Canteen>>> {
let x: HashMap<_, _> = sqlx::query_as!(
Canteen,
"SELECT canteen_id as id, name FROM canteen WHERE canteen_id = ANY ($1)",
id
)
.fetch_all(&self.pool)
.await?
.into_iter()
.map(|c| (c.id, Some(c)))
.collect();

// todo when id has no canteen, a None entry should be in the HashMap
Ok(x)
}
}

#[cfg(test)]
Expand Down
47 changes: 47 additions & 0 deletions backend/src/layer/trigger/graphql/dataloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::collections::HashMap;

use async_trait::async_trait;

use crate::{util::Uuid, interface::persistent_data::{DataError, RequestDataAccess, model::Canteen}};


use async_graphql::dataloader::Loader;



pub struct CanteenDataLoader {
data: Box<dyn RequestDataAccess>
}

impl CanteenDataLoader {
pub fn new(data: impl RequestDataAccess + 'static) -> Self {
Self {
data: Box::new(data),
}
}
}

// little bit useless here... maybe useful fore cashing
#[async_trait]
impl Loader<()> for CanteenDataLoader {
type Value = Vec<Canteen>;
type Error = DataError;

async fn load(&self, _keys: &[()]) -> Result<HashMap<(), Self::Value>, Self::Error> {
self.data.get_canteens().await.map(|c| HashMap::from([((), c)]))
}


}

#[async_trait]
impl Loader<Uuid> for CanteenDataLoader {
type Value = Option<Canteen>;
type Error = DataError;

async fn load(&self, keys: &[Uuid]) -> Result<HashMap<Uuid, Self::Value>, Self::Error> {
self.data.get_canteen_multi(keys).await
}


}
1 change: 1 addition & 0 deletions backend/src/layer/trigger/graphql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ pub mod query;
pub mod server;
mod types;
pub mod util;
pub mod dataloader;

mod tests;
27 changes: 21 additions & 6 deletions backend/src/layer/trigger/graphql/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ impl QueryRoot {
#[instrument(skip(self, ctx))]
async fn get_canteens(&self, ctx: &Context<'_>) -> Result<Vec<Canteen>> {
trace!("Queried `getCanteens`");
let data = ctx.get_data_access();
let canteens = data
.get_canteens()
// let data = ctx.get_data_access();
// let canteens = data
// .get_canteens()
// .await?
// .into_iter()
// .map(Into::into)
// .collect();
let canteens = ctx
.get_canteen_loader()
.load_one(())
.await?
.ok_or("data could not be loaded")? // todo better error
.into_iter()
.map(Into::into)
.collect();
Expand All @@ -38,9 +46,16 @@ impl QueryRoot {
#[graphql(desc = "Id of the canteen to get.")] canteen_id: Uuid,
) -> Result<Option<Canteen>> {
trace!("Queried `getCanteen`");
let data = ctx.get_data_access();
let canteen = data.get_canteen(canteen_id).await?.map(Into::into);
Ok(canteen)
// let data = ctx.get_data_access();
// let canteen = data.get_canteen(canteen_id).await?.map(Into::into);
// Ok(canteen)
let x = ctx
.get_canteen_loader()
.load_one(canteen_id)
.await?
.unwrap_or(None) // todo handle this case, when id is not valid at only _one_ place!
.map(Into::into);
Ok(x)
}

/// This query returns the main dish (including its price and sides) identified by the specified ID, the line and the date.
Expand Down
9 changes: 5 additions & 4 deletions backend/src/layer/trigger/graphql/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{
use async_graphql::{
extensions::{Tracing, ApolloTracing},
http::{playground_source, GraphQLPlaygroundConfig},
EmptySubscription, Schema,
EmptySubscription, Schema, dataloader::DataLoader,
};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
Expand All @@ -33,7 +33,7 @@ use crate::interface::{
use super::{
mutation::MutationRoot,
query::QueryRoot,
util::{read_auth_from_header, CommandBox, DataBox},
util::{read_auth_from_header, CommandBox, DataBox}, dataloader::CanteenDataLoader,
};

type GraphQLSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
Expand Down Expand Up @@ -148,11 +148,12 @@ pub(super) fn construct_schema(
data_access: impl RequestDataAccess + Sync + Send + 'static,
command: impl Command + Sync + Send + 'static,
) -> GraphQLSchema {
let data_access_box: DataBox = Box::new(data_access);
// let data_access_box: DataBox = Box::new(data_access);
let command_box: CommandBox = Box::new(command);

Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(data_access_box)
// .data(data_access_box)
.data(DataLoader::new(CanteenDataLoader::new(data_access), tokio::spawn))
.data(command_box)
.extension(Tracing)
.extension(ApolloTracing)
Expand Down
2 changes: 1 addition & 1 deletion backend/src/layer/trigger/graphql/types/canteen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tracing::instrument;

use super::line::Line;

#[derive(SimpleObject, Debug)]
#[derive(SimpleObject, Debug, Clone)]
#[graphql(complex)]
pub struct Canteen {
/// The id of the canteen.
Expand Down
13 changes: 10 additions & 3 deletions backend/src/layer/trigger/graphql/util.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use async_graphql::Context;
use async_graphql::{Context, dataloader::DataLoader};

use crate::{
interface::{
Expand All @@ -9,23 +9,30 @@ use crate::{
};
use base64::{engine::general_purpose, Engine};

use super::dataloader::CanteenDataLoader;

pub type DataBox = Box<dyn RequestDataAccess + Sync + Send + 'static>;
pub type CommandBox = Box<dyn Command + Sync + Send + 'static>;

pub trait ApiUtil {
fn get_command(&self) -> &(dyn Command + Sync + Send);
fn get_data_access(&self) -> &(dyn RequestDataAccess + Sync + Send);
fn get_canteen_loader(&self) -> &DataLoader<CanteenDataLoader>;
fn get_auth_info(&self) -> AuthInfo;
}

impl<'a> ApiUtil for Context<'a> {
fn get_command(&self) -> &'a (dyn Command + Sync + Send) {
fn get_command(&self) -> &(dyn Command + Sync + Send) {
self.data_unchecked::<CommandBox>().as_ref()
}

fn get_data_access(&self) -> &'a (dyn RequestDataAccess + Sync + Send) {
fn get_data_access(&self) -> &(dyn RequestDataAccess + Sync + Send) {
self.data_unchecked::<DataBox>().as_ref()
}

fn get_canteen_loader(&self) -> &DataLoader<CanteenDataLoader> {
self.data_unchecked::<DataLoader<CanteenDataLoader>>()
}

fn get_auth_info(&self) -> AuthInfo {
self.data::<AuthInfo>().iter().find_map(|i| (*i).clone())
Expand Down

0 comments on commit 47e1b83

Please sign in to comment.