Check pointing some exploration with graphql for the reldata service

This commit is contained in:
R Tyler Croy 2021-03-03 15:16:54 -08:00
parent 08d584974b
commit 14d78e009f
5 changed files with 116 additions and 4 deletions

2
.gitignore vendored
View File

@ -8,4 +8,4 @@ build/
*.tar.gz
tmp*
.hypothesis/
otto.db
otto.db*

10
Cargo.lock generated
View File

@ -1677,6 +1677,15 @@ dependencies = [
"waker-fn",
]
[[package]]
name = "itertools"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.7"
@ -2183,6 +2192,7 @@ dependencies = [
"pretty_env_logger 0.4.0",
"sqlx",
"tide",
"uuid",
]
[[package]]

View File

@ -5,7 +5,7 @@
CREATE TABLE IF NOT EXISTS projects
(
uuid BLOB PRIMARY KEY NOT NULL,
uuid TEXT PRIMARY KEY NOT NULL,
path TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
description TEXT,

View File

@ -5,7 +5,7 @@ authors = ["R. Tyler Croy <rtyler@brokenco.de>"]
edition = "2018"
[dependencies]
async-graphql = "2.0"
async-graphql = { version = "2.0", features = ["chrono", "dataloader", "log", "uuid"] }
async-graphql-tide = "2.0"
async-trait = "0.1"
async-std = { version = "1", features = ["attributes"]}
@ -16,3 +16,4 @@ otto-models = { path = "../../crates/models" }
pretty_env_logger = "~0.4.0"
sqlx = { version = "~0.5.1", features = ["runtime-async-std-rustls", "postgres", "tls", "json", "sqlite", "chrono", "macros", "uuid"]}
tide = "0.16"
uuid = { version = "0.8", features = ["serde", "v4"]}

View File

@ -1,8 +1,34 @@
/*
* 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("{}")
@ -10,13 +36,38 @@ async fn healthcheck(_req: Request<()>) -> tide::Result {
.build())
}
/**
* Main web service set up
*/
#[async_std::main]
async fn main() -> std::io::Result<()> {
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?;
@ -26,5 +77,55 @@ async fn main() -> std::io::Result<()> {
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 uuids = keys.iter().map(|u| u.to_string()).collect::<Vec<String>>();
Ok(
sqlx::query_as::<sqlx::Sqlite, Self::Value>("SELECT * FROM projects WHERE uuid IN (?)")
.bind(&uuids.join(","))
.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 {}