141 lines
3.7 KiB
Rust
141 lines
3.7 KiB
Rust
/*
|
|
* The relational data service largely is meant to expose information from an underlying database
|
|
*/
|
|
use dotenv::dotenv;
|
|
use log::*;
|
|
use tide::Request;
|
|
|
|
use async_graphql::dataloader::{DataLoader, Loader};
|
|
use async_graphql::futures_util::TryStreamExt;
|
|
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
|
use async_graphql::{
|
|
Context, EmptyMutation, EmptySubscription, FieldError, Object, Result, Schema, SimpleObject,
|
|
};
|
|
use async_trait::async_trait;
|
|
use sqlx::{Pool, SqlitePool};
|
|
use std::collections::HashMap;
|
|
use tide::{http::mime, Body, Response, StatusCode};
|
|
use uuid::Uuid;
|
|
|
|
/**
|
|
* QueryState is a simple struct to pass data through to async-graphql implementations
|
|
*/
|
|
struct QueryState {
|
|
pool: SqlitePool,
|
|
data_loader: DataLoader<ProjectLoader>,
|
|
}
|
|
|
|
/**
|
|
* Simple/empty healthcheck endpoint which can be used to determine whether the webservice is at
|
|
* least minimally functional
|
|
*/
|
|
async fn healthcheck(_req: Request<()>) -> tide::Result {
|
|
Ok(tide::Response::builder(200)
|
|
.body("{}")
|
|
.content_type("application/json")
|
|
.build())
|
|
}
|
|
|
|
/**
|
|
* Main web service set up
|
|
*/
|
|
#[async_std::main]
|
|
async fn main() -> async_graphql::Result<()> {
|
|
use std::{env, net::TcpListener, os::unix::io::FromRawFd};
|
|
pretty_env_logger::init();
|
|
dotenv().ok();
|
|
let pool: SqlitePool = Pool::connect(&env::var("DATABASE_URL")?).await?;
|
|
debug!("Connecting to: {}", env::var("DATABASE_URL")?);
|
|
|
|
let qs = QueryState {
|
|
pool: pool.clone(),
|
|
data_loader: DataLoader::new(ProjectLoader::new(pool.clone())),
|
|
};
|
|
|
|
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
|
|
.data(qs)
|
|
.finish();
|
|
|
|
let mut app = tide::new();
|
|
app.at("/health").get(healthcheck);
|
|
app.at("/graphql")
|
|
.post(async_graphql_tide::endpoint(schema));
|
|
app.at("/").get(|_| async move {
|
|
let mut resp = Response::new(StatusCode::Ok);
|
|
resp.set_body(Body::from_string(playground_source(
|
|
GraphQLPlaygroundConfig::new("/graphql"),
|
|
)));
|
|
resp.set_content_type(mime::HTML);
|
|
Ok(resp)
|
|
});
|
|
|
|
if let Some(fd) = env::var("LISTEN_FD").ok().and_then(|fd| fd.parse().ok()) {
|
|
app.listen(unsafe { TcpListener::from_raw_fd(fd) }).await?;
|
|
} else {
|
|
app.listen("http://localhost:7674").await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(sqlx::FromRow, Clone, Debug, SimpleObject)]
|
|
pub struct Project {
|
|
uuid: Uuid,
|
|
path: String,
|
|
title: String,
|
|
}
|
|
|
|
pub struct ProjectLoader(SqlitePool);
|
|
impl ProjectLoader {
|
|
fn new(pool: SqlitePool) -> Self {
|
|
Self(pool)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Loader<Uuid> for ProjectLoader {
|
|
type Value = Project;
|
|
type Error = FieldError;
|
|
|
|
async fn load(&self, keys: &[Uuid]) -> Result<HashMap<Uuid, Self::Value>, Self::Error> {
|
|
let query = format!(
|
|
"SELECT * FROM projects WHERE uuid IN ({})",
|
|
(0..keys.len())
|
|
.map(|_| "?")
|
|
.collect::<Vec<&str>>()
|
|
.join(",")
|
|
);
|
|
|
|
let mut q = sqlx::query_as::<sqlx::Sqlite, Project>(&query);
|
|
for x in (0..keys.len()) {
|
|
q = q.bind(keys[x]);
|
|
}
|
|
debug!("query: {}", query);
|
|
|
|
Ok(q.fetch(&self.0)
|
|
.map_ok(|p: Project| (p.uuid, p))
|
|
.try_collect()
|
|
.await?)
|
|
}
|
|
}
|
|
|
|
struct QueryRoot;
|
|
|
|
#[Object]
|
|
impl QueryRoot {
|
|
async fn project(&self, ctx: &Context<'_>, id: Uuid) -> Result<Option<Project>> {
|
|
Ok(ctx
|
|
.data_unchecked::<QueryState>()
|
|
.data_loader
|
|
.load_one(id)
|
|
.await?)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
schema: Schema<QueryRoot, EmptyMutation, EmptySubscription>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {}
|