Skip to content

Commit

Permalink
feat: store humans in an actual database
Browse files Browse the repository at this point in the history
This uses `sqlx` to read/write humans from/to the sample-db.

The schema consists of a custom enum type for episodes, and a table for
humans. For now, the same structs as were used for the GraphQL schema
are being used to (de)serialize database values. This would have been
quite smooth, but sadly, `sqlx`'s derivation can't handle `Vec`s of
custom enums (launchbadge/sqlx#298), so we
have to jump through some hoops by introducing `EpisodeSlice` and
`EpisodeVec` newtypes, which can then implement the required traits
(plus all the required juniper traits, which is the bigger pain).

Since we're using `sqlx`'s macros to check queries at compile time, we
need to connect to the database during compilation. The macro will use
the `DATABASE_URL` environment variable, which it can read from a `.env`
file, so we now write one of these files as part of `make prepare-db`
(note that this is for the benefit of editors, language servers, etc.,
`make run` would already inherit the `DATABASE_URL` when compiling).
  • Loading branch information
Chris Connelly committed Dec 11, 2021
1 parent 847575c commit b1db5bf
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 109 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.env
/target
3 changes: 2 additions & 1 deletion Cargo.lock

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

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ edition = "2021"
[dependencies]
axum = "0.4.2"
envy = "0.4.2"
futures = "0.3.18"
juniper = "0.15.7"
juniper_hyper = "0.8.0"
parking_lot = "0.11.2"
serde = { version = "1.0.131", features = ["derive"] }
sqlx = { version = "0.5.9", default-features = false, features = ["macros", "postgres", "runtime-tokio-rustls"] }
sqlx = { version = "0.5.9", default-features = false, features = ["macros", "postgres", "runtime-tokio-rustls", "uuid"] }
tokio = { version = "1.14.0", features = ["macros", "rt-multi-thread"] }
tower = "0.4.11"
tower-http = { version = "0.2.0", features = ["trace"] }
tracing = "0.1.29"
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
uuid = { version = "0.8.2", features = ["v4"] }

[features]
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export RUST_LOG ?= rust_graphql_sample=debug,tower_http=debug
prepare-db: start-db
@sqlx database create
@sqlx migrate run
# Write a .env file with DATABASE_URL, so that sqlx will always pick it up (e.g. from editor or language server)
@echo "DATABASE_URL=$(DATABASE_URL)" > .env

start-db:
@scripts/start-db.sh
Expand Down
10 changes: 10 additions & 0 deletions migrations/20211210224745_create_initial_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TYPE episode AS ENUM ('new_hope', 'empire', 'jedi');

CREATE TABLE humans (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL UNIQUE,
appears_in episode[] NOT NULL,
home_planet TEXT NOT NULL
);
67 changes: 39 additions & 28 deletions src/graphql/context.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,56 @@
use std::collections::HashMap;

use parking_lot::RwLock;
use uuid::Uuid;

use super::{Human, NewHuman};
use crate::model::{Human, NewHuman};

pub(crate) struct Context {
_db: sqlx::PgPool,
humans: RwLock<HashMap<Uuid, Human>>,
db: sqlx::PgPool,
}

impl Context {
pub(crate) fn new(db: sqlx::PgPool) -> Self {
Self {
_db: db,
humans: Default::default(),
}
Self { db }
}

pub(crate) fn humans(&self) -> Vec<Human> {
self.humans.read().values().cloned().collect()
pub(crate) async fn humans(&self) -> Result<Vec<Human>, sqlx::Error> {
sqlx::query_as!(
Human,
"
SELECT id, name, appears_in AS \"appears_in: _\", home_planet
FROM humans
",
)
.fetch_all(&self.db)
.await
}

pub(crate) fn find_human(&self, id: &Uuid) -> Result<Human, &'static str> {
self.humans.read().get(id).cloned().ok_or("not found")
pub(crate) async fn find_human(&self, id: &Uuid) -> Result<Human, sqlx::Error> {
sqlx::query_as!(
Human,
"
SELECT id, name, appears_in AS \"appears_in: _\", home_planet
FROM humans
WHERE id = $1
",
id
)
.fetch_one(&self.db)
.await
}

pub(crate) fn insert_human(&self, new_human: NewHuman) -> Result<Human, &'static str> {
let mut humans = self.humans.write();

if humans
.values()
.any(|human| human.name() == new_human.name())
{
return Err("a human with that name already exists");
}

let human = Human::new(new_human);
humans.insert(human.id(), human.clone());

Ok(human)
pub(crate) async fn insert_human(&self, new_human: NewHuman) -> Result<Human, sqlx::Error> {
sqlx::query_as!(
Human,
"
INSERT INTO humans (name, appears_in, home_planet)
VALUES ($1, $2, $3)
RETURNING id, name, appears_in AS \"appears_in: _\", home_planet
",
new_human.name(),
new_human.appears_in() as _,
new_human.home_planet(),
)
.fetch_one(&self.db)
.await
}
}

Expand Down
1 change: 0 additions & 1 deletion src/graphql/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod context;
mod schema;

use self::schema::{Human, NewHuman};
pub(crate) use self::{
context::Context,
schema::{Mutation, Query},
Expand Down
83 changes: 7 additions & 76 deletions src/graphql/schema.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use juniper::FieldResult;
use uuid::Uuid;

use crate::Context;
use crate::{Context, model::{NewHuman, Human}};

pub(crate) struct Query;

Expand All @@ -14,13 +14,13 @@ impl Query {
}

/// All the humanoid creatures in the Star Wars universe that we know about.
fn humans(context: &Context) -> FieldResult<Vec<Human>> {
Ok(context.humans())
async fn humans(context: &Context) -> FieldResult<Vec<Human>> {
Ok(context.humans().await?)
}

/// A humanoid creature in the Star Wars universe.
fn human(context: &Context, id: Uuid) -> FieldResult<Human> {
let human = context.find_human(&id)?;
async fn human(context: &Context, id: Uuid) -> FieldResult<Human> {
let human = context.find_human(&id).await?;
Ok(human)
}
}
Expand All @@ -30,77 +30,8 @@ pub(crate) struct Mutation;
/// The root mutation structure.
#[juniper::graphql_object(Context = Context)]
impl Mutation {
fn create_human(context: &Context, new_human: NewHuman) -> FieldResult<Human> {
let human = context.insert_human(new_human)?;
async fn create_human(context: &Context, new_human: NewHuman) -> FieldResult<Human> {
let human = context.insert_human(new_human).await?;
Ok(human)
}
}

/// Episodes in the original (and best) Star Wars trilogy.
#[derive(Clone, Copy, juniper::GraphQLEnum)]
pub(crate) enum Episode {
/// Star Wars: Episode IV – A New Hope
NewHope,

/// Star Wars: Episode V – The Empire Strikes Back
Empire,

/// Star Wars: Episode VI – Return of the Jedi
Jedi,
}

/// A humanoid creature in the Star Wars universe.
#[derive(Clone, juniper::GraphQLObject)]
pub(crate) struct Human {
/// Their unique identifier, assigned by us.
id: Uuid,

/// Their name.
name: String,

/// The episodes in which they appeared.
appears_in: Vec<Episode>,

/// Their home planet.
home_planet: String,
}

impl Human {
pub(crate) fn new(new_human: NewHuman) -> Self {
Self {
id: Uuid::new_v4(),
name: new_human.name,
appears_in: new_human.appears_in,
home_planet: new_human.home_planet,
}
}

pub(crate) fn id(&self) -> Uuid {
self.id
}

pub(crate) fn name(&self) -> &str {
&self.name
}
}

/// A new humanoid creature in the Star Wars universe.
///
/// `id` is assigned by the server upon creation.
#[derive(juniper::GraphQLInputObject)]
pub(crate) struct NewHuman {
/// Their name.
name: String,

/// The episodes in which they appeared.
appears_in: Vec<Episode>,

/// Their home planet.
home_planet: String,
}

impl NewHuman {
pub(crate) fn name(&self) -> &str {
&self.name
}
}
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod graphql;
pub(crate) mod model;

use std::{
net::{Ipv4Addr, SocketAddr, TcpListener},
Expand All @@ -17,7 +18,7 @@ use tower_http::trace::TraceLayer;
use tracing::info;
use tracing_subscriber::EnvFilter;

use self::graphql::{Context, Mutation, Query};
pub(crate) use self::graphql::{Context, Mutation, Query};

#[derive(Debug, serde::Deserialize)]
struct Config {
Expand Down
Loading

0 comments on commit b1db5bf

Please sign in to comment.