From 14d78e009f2aa47ae4d3aedcd11ce5c1548a9e31 Mon Sep 17 00:00:00 2001 From: "R. Tyler Croy" Date: Wed, 3 Mar 2021 15:16:54 -0800 Subject: [PATCH] Check pointing some exploration with graphql for the reldata service --- .gitignore | 2 +- Cargo.lock | 10 ++ migrations/sqlite/20210303191923_projects.sql | 2 +- services/reldata/Cargo.toml | 3 +- services/reldata/src/main.rs | 103 +++++++++++++++++- 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3390433..44a8a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ build/ *.tar.gz tmp* .hypothesis/ -otto.db +otto.db* diff --git a/Cargo.lock b/Cargo.lock index 46a4526..954b5b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/migrations/sqlite/20210303191923_projects.sql b/migrations/sqlite/20210303191923_projects.sql index ef92bcd..6a5d6f6 100644 --- a/migrations/sqlite/20210303191923_projects.sql +++ b/migrations/sqlite/20210303191923_projects.sql @@ -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, diff --git a/services/reldata/Cargo.toml b/services/reldata/Cargo.toml index 55dcbc3..5b49c03 100644 --- a/services/reldata/Cargo.toml +++ b/services/reldata/Cargo.toml @@ -5,7 +5,7 @@ authors = ["R. Tyler Croy "] 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"]} diff --git a/services/reldata/src/main.rs b/services/reldata/src/main.rs index cc4fdec..0e0c64a 100644 --- a/services/reldata/src/main.rs +++ b/services/reldata/src/main.rs @@ -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, +} + +/** + * 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 for ProjectLoader { + type Value = Project; + type Error = FieldError; + + async fn load(&self, keys: &[Uuid]) -> Result, Self::Error> { + let uuids = keys.iter().map(|u| u.to_string()).collect::>(); + Ok( + sqlx::query_as::("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> { + Ok(ctx + .data_unchecked::() + .data_loader + .load_one(id) + .await?) + } +} + +#[derive(Clone)] +struct AppState { + schema: Schema, +} + #[cfg(test)] mod tests {}