Skip to content

Commit b1db5bf

Browse files
author
Chris Connelly
committed
feat: store humans in an actual database
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).
1 parent 847575c commit b1db5bf

File tree

10 files changed

+214
-109
lines changed

10 files changed

+214
-109
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
.env
12
/target

Cargo.lock

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ edition = "2021"
88
[dependencies]
99
axum = "0.4.2"
1010
envy = "0.4.2"
11+
futures = "0.3.18"
1112
juniper = "0.15.7"
1213
juniper_hyper = "0.8.0"
13-
parking_lot = "0.11.2"
1414
serde = { version = "1.0.131", features = ["derive"] }
15-
sqlx = { version = "0.5.9", default-features = false, features = ["macros", "postgres", "runtime-tokio-rustls"] }
15+
sqlx = { version = "0.5.9", default-features = false, features = ["macros", "postgres", "runtime-tokio-rustls", "uuid"] }
1616
tokio = { version = "1.14.0", features = ["macros", "rt-multi-thread"] }
1717
tower = "0.4.11"
1818
tower-http = { version = "0.2.0", features = ["trace"] }
1919
tracing = "0.1.29"
2020
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
2121
uuid = { version = "0.8.2", features = ["v4"] }
22+
23+
[features]

Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export RUST_LOG ?= rust_graphql_sample=debug,tower_http=debug
1111
prepare-db: start-db
1212
@sqlx database create
1313
@sqlx migrate run
14+
# Write a .env file with DATABASE_URL, so that sqlx will always pick it up (e.g. from editor or language server)
15+
@echo "DATABASE_URL=$(DATABASE_URL)" > .env
1416

1517
start-db:
1618
@scripts/start-db.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
2+
3+
CREATE TYPE episode AS ENUM ('new_hope', 'empire', 'jedi');
4+
5+
CREATE TABLE humans (
6+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
7+
name TEXT NOT NULL UNIQUE,
8+
appears_in episode[] NOT NULL,
9+
home_planet TEXT NOT NULL
10+
);

src/graphql/context.rs

+39-28
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,56 @@
1-
use std::collections::HashMap;
2-
3-
use parking_lot::RwLock;
41
use uuid::Uuid;
52

6-
use super::{Human, NewHuman};
3+
use crate::model::{Human, NewHuman};
74

85
pub(crate) struct Context {
9-
_db: sqlx::PgPool,
10-
humans: RwLock<HashMap<Uuid, Human>>,
6+
db: sqlx::PgPool,
117
}
128

139
impl Context {
1410
pub(crate) fn new(db: sqlx::PgPool) -> Self {
15-
Self {
16-
_db: db,
17-
humans: Default::default(),
18-
}
11+
Self { db }
1912
}
2013

21-
pub(crate) fn humans(&self) -> Vec<Human> {
22-
self.humans.read().values().cloned().collect()
14+
pub(crate) async fn humans(&self) -> Result<Vec<Human>, sqlx::Error> {
15+
sqlx::query_as!(
16+
Human,
17+
"
18+
SELECT id, name, appears_in AS \"appears_in: _\", home_planet
19+
FROM humans
20+
",
21+
)
22+
.fetch_all(&self.db)
23+
.await
2324
}
2425

25-
pub(crate) fn find_human(&self, id: &Uuid) -> Result<Human, &'static str> {
26-
self.humans.read().get(id).cloned().ok_or("not found")
26+
pub(crate) async fn find_human(&self, id: &Uuid) -> Result<Human, sqlx::Error> {
27+
sqlx::query_as!(
28+
Human,
29+
"
30+
SELECT id, name, appears_in AS \"appears_in: _\", home_planet
31+
FROM humans
32+
WHERE id = $1
33+
",
34+
id
35+
)
36+
.fetch_one(&self.db)
37+
.await
2738
}
2839

29-
pub(crate) fn insert_human(&self, new_human: NewHuman) -> Result<Human, &'static str> {
30-
let mut humans = self.humans.write();
31-
32-
if humans
33-
.values()
34-
.any(|human| human.name() == new_human.name())
35-
{
36-
return Err("a human with that name already exists");
37-
}
38-
39-
let human = Human::new(new_human);
40-
humans.insert(human.id(), human.clone());
41-
42-
Ok(human)
40+
pub(crate) async fn insert_human(&self, new_human: NewHuman) -> Result<Human, sqlx::Error> {
41+
sqlx::query_as!(
42+
Human,
43+
"
44+
INSERT INTO humans (name, appears_in, home_planet)
45+
VALUES ($1, $2, $3)
46+
RETURNING id, name, appears_in AS \"appears_in: _\", home_planet
47+
",
48+
new_human.name(),
49+
new_human.appears_in() as _,
50+
new_human.home_planet(),
51+
)
52+
.fetch_one(&self.db)
53+
.await
4354
}
4455
}
4556

src/graphql/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
mod context;
22
mod schema;
33

4-
use self::schema::{Human, NewHuman};
54
pub(crate) use self::{
65
context::Context,
76
schema::{Mutation, Query},

src/graphql/schema.rs

+7-76
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use juniper::FieldResult;
22
use uuid::Uuid;
33

4-
use crate::Context;
4+
use crate::{Context, model::{NewHuman, Human}};
55

66
pub(crate) struct Query;
77

@@ -14,13 +14,13 @@ impl Query {
1414
}
1515

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

2121
/// A humanoid creature in the Star Wars universe.
22-
fn human(context: &Context, id: Uuid) -> FieldResult<Human> {
23-
let human = context.find_human(&id)?;
22+
async fn human(context: &Context, id: Uuid) -> FieldResult<Human> {
23+
let human = context.find_human(&id).await?;
2424
Ok(human)
2525
}
2626
}
@@ -30,77 +30,8 @@ pub(crate) struct Mutation;
3030
/// The root mutation structure.
3131
#[juniper::graphql_object(Context = Context)]
3232
impl Mutation {
33-
fn create_human(context: &Context, new_human: NewHuman) -> FieldResult<Human> {
34-
let human = context.insert_human(new_human)?;
33+
async fn create_human(context: &Context, new_human: NewHuman) -> FieldResult<Human> {
34+
let human = context.insert_human(new_human).await?;
3535
Ok(human)
3636
}
3737
}
38-
39-
/// Episodes in the original (and best) Star Wars trilogy.
40-
#[derive(Clone, Copy, juniper::GraphQLEnum)]
41-
pub(crate) enum Episode {
42-
/// Star Wars: Episode IV – A New Hope
43-
NewHope,
44-
45-
/// Star Wars: Episode V – The Empire Strikes Back
46-
Empire,
47-
48-
/// Star Wars: Episode VI – Return of the Jedi
49-
Jedi,
50-
}
51-
52-
/// A humanoid creature in the Star Wars universe.
53-
#[derive(Clone, juniper::GraphQLObject)]
54-
pub(crate) struct Human {
55-
/// Their unique identifier, assigned by us.
56-
id: Uuid,
57-
58-
/// Their name.
59-
name: String,
60-
61-
/// The episodes in which they appeared.
62-
appears_in: Vec<Episode>,
63-
64-
/// Their home planet.
65-
home_planet: String,
66-
}
67-
68-
impl Human {
69-
pub(crate) fn new(new_human: NewHuman) -> Self {
70-
Self {
71-
id: Uuid::new_v4(),
72-
name: new_human.name,
73-
appears_in: new_human.appears_in,
74-
home_planet: new_human.home_planet,
75-
}
76-
}
77-
78-
pub(crate) fn id(&self) -> Uuid {
79-
self.id
80-
}
81-
82-
pub(crate) fn name(&self) -> &str {
83-
&self.name
84-
}
85-
}
86-
87-
/// A new humanoid creature in the Star Wars universe.
88-
///
89-
/// `id` is assigned by the server upon creation.
90-
#[derive(juniper::GraphQLInputObject)]
91-
pub(crate) struct NewHuman {
92-
/// Their name.
93-
name: String,
94-
95-
/// The episodes in which they appeared.
96-
appears_in: Vec<Episode>,
97-
98-
/// Their home planet.
99-
home_planet: String,
100-
}
101-
102-
impl NewHuman {
103-
pub(crate) fn name(&self) -> &str {
104-
&self.name
105-
}
106-
}

src/main.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod graphql;
2+
pub(crate) mod model;
23

34
use std::{
45
net::{Ipv4Addr, SocketAddr, TcpListener},
@@ -17,7 +18,7 @@ use tower_http::trace::TraceLayer;
1718
use tracing::info;
1819
use tracing_subscriber::EnvFilter;
1920

20-
use self::graphql::{Context, Mutation, Query};
21+
pub(crate) use self::graphql::{Context, Mutation, Query};
2122

2223
#[derive(Debug, serde::Deserialize)]
2324
struct Config {

0 commit comments

Comments
 (0)