Skip to content

Commit

Permalink
Create a user
Browse files Browse the repository at this point in the history
  • Loading branch information
markholmes committed Jun 6, 2024
1 parent f3ff0f8 commit 9da3546
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 10 deletions.
1 change: 1 addition & 0 deletions db/migrations/20240525044318_create_users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Expand Down
11 changes: 6 additions & 5 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ version = "1.0.0"
# https://gleam.run/writing-gleam/gleam-toml/.

[dependencies]
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
wisp = ">= 0.14.0 and < 1.0.0"
mist = { path = "../../mist" }
gleam_http = ">= 3.6.0 and < 4.0.0"
antigone = ">= 1.1.0 and < 2.0.0"
envoy = ">= 1.0.1 and < 2.0.0"
gleam_erlang = ">= 0.25.0 and < 1.0.0"
gleam_http = ">= 3.6.0 and < 4.0.0"
gleam_json = ">= 1.0.1 and < 2.0.0"
envoy = ">= 1.0.1 and < 2.0.0"
gleam_pgo = ">= 0.9.0 and < 1.0.0"
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
mist = ">= 1.2.0 and < 2.0.0"
wisp = ">= 0.14.0 and < 1.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
15 changes: 11 additions & 4 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
# You typically do not need to edit this file

packages = [
{ name = "antigone", version = "1.1.0", build_tools = ["gleam"], requirements = ["argon2_elixir"], otp_app = "antigone", source = "hex", outer_checksum = "2AF72F94924FD2C1C732DF714F756BAB1B04E7128569F5E2785179A17691FC0D" },
{ name = "argon2_elixir", version = "4.0.0", build_tools = ["mix", "make"], requirements = ["comeonin", "elixir_make"], otp_app = "argon2_elixir", source = "hex", outer_checksum = "F9DA27CF060C9EA61B1BD47837A28D7E48A8F6FA13A745E252556C14F9132C7F" },
{ name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" },
{ name = "birl", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "B1FA529E7BE3FF12CADF32814AB8EC7294E74CEDEE8CC734505707B929A98985" },
{ name = "castore", version = "1.0.7", build_tools = ["mix"], requirements = [], otp_app = "castore", source = "hex", outer_checksum = "DA7785A4B0D2A021CD1292A60875A784B6CAEF71E76BF4917BDEE1F390455CF5" },
{ name = "certifi", version = "2.13.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "8F3D9533A0F06070AFDFD5D596B32E21C6580667A492891851B0E2737BC507A1" },
{ name = "comeonin", version = "5.4.0", build_tools = ["mix"], requirements = [], otp_app = "comeonin", source = "hex", outer_checksum = "796393A9E50D01999D56B7B8420AB0481A7538D0CAF80919DA493B4A6E51FAF1" },
{ name = "elixir_make", version = "0.8.4", build_tools = ["mix"], requirements = ["castore", "certifi"], otp_app = "elixir_make", source = "hex", outer_checksum = "6E7F1D619B5F61DFABD0A20AA268E575572B542AC31723293A4C1A567D5EF040" },
{ name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" },
{ name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
Expand All @@ -16,11 +22,11 @@ packages = [
{ name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
{ name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
{ name = "gramps", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "99A1BC8462E502282097D92CD7053043B714B91DC78E8AEB7D86AB8FF71E016F" },
{ name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
{ name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" },
{ name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" },
{ name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], source = "local", path = "../../mist" },
{ name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
{ name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" },
{ name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" },
{ name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" },
Expand All @@ -32,12 +38,13 @@ packages = [
]

[requirements]
antigone = { version = ">= 1.1.0 and < 2.0.0"}
envoy = { version = ">= 1.0.1 and < 2.0.0" }
gleam_erlang = { version = ">= 0.25.0 and < 1.0.0" }
gleam_http = { version = ">= 3.6.0 and < 4.0.0" }
gleam_json = { version = ">= 1.0.1 and < 2.0.0" }
gleam_pgo = { version = ">= 0.9.0 and < 1.0.0"}
gleam_pgo = { version = ">= 0.9.0 and < 1.0.0" }
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
mist = { path = "../../mist" }
mist = { version = ">= 1.2.0 and < 2.0.0" }
wisp = { version = ">= 0.14.0 and < 1.0.0" }
39 changes: 39 additions & 0 deletions src/app/contexts/user_contexts.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import gleam/dynamic.{type DecodeError, type Dynamic}
import gleam/result

pub type NewUser {
NewUser(name: String, email: String, password: String)
}

pub type User {
User(id: Int, name: String, email: String, password_hash: String)
}

// Decodes the JSON blob from the POST to /users
pub fn new_user_decoder(json: Dynamic) -> Result(NewUser, Nil) {
let decoder =
dynamic.decode3(
NewUser,
dynamic.field("name", dynamic.string),
dynamic.field("email", dynamic.string),
dynamic.field("password", dynamic.string),
)

json
|> decoder()
|> result.nil_error()
}

// Decodes the return from the db insert
pub fn user_decoder(tuple: Dynamic) -> Result(User, List(DecodeError)) {
let decoder =
dynamic.decode4(
User,
dynamic.element(0, dynamic.int),
dynamic.element(1, dynamic.string),
dynamic.element(2, dynamic.string),
dynamic.element(3, dynamic.string),
)

decoder(tuple)
}
28 changes: 28 additions & 0 deletions src/app/queries/user_queries.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import antigone
import app/contexts/user_contexts.{type NewUser, type User, user_decoder}
import gleam/bit_array
import gleam/pgo.{type Connection, Returned}

pub fn create_user(connection: Connection, user: NewUser) -> Result(User, Nil) {
let sql =
"
INSERT INTO users (name, email, password_hash)
VALUES ($1, $2, $3)
RETURNING id, name, email, password_hash;
"
let password = bit_array.from_string(user.password)
let hashed_password = antigone.hash(antigone.hasher(), password)

let returned =
pgo.execute(
sql,
connection,
[pgo.text(user.name), pgo.text(user.email), pgo.text(hashed_password)],
user_decoder,
)

case returned {
Ok(Returned(_, [user, ..])) -> Ok(user)
_ -> Error(Nil)
}
}
11 changes: 11 additions & 0 deletions src/app/rest/user_endpoints.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import app/contexts/web_contexts.{type Context}
import app/services/user_services.{create_user}
import gleam/http.{Post}
import wisp.{type Request, type Response}

pub fn all(req: Request, ctx: Context) -> Response {
case req.method {
Post -> create_user(req, ctx)
_ -> wisp.method_not_allowed([Post])
}
}
4 changes: 3 additions & 1 deletion src/app/router.gleam
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import app/contexts/web_contexts.{type Context}
import app/rest/organization_endpoints as organizations
import app/types/web_types.{type Context}
import app/rest/user_endpoints as users
import app/web.{middleware}
import wisp.{type Request, type Response}

Expand All @@ -8,6 +9,7 @@ pub fn handle_request(req: Request, ctx: Context) -> Response {

case wisp.path_segments(req) {
["organizations"] -> organizations.all(req, ctx)
["users"] -> users.all(req, ctx)
_ -> wisp.not_found()
}
}
36 changes: 36 additions & 0 deletions src/app/services/user_services.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import app/contexts/user_contexts as contexts
import app/contexts/web_contexts.{type Context}
import app/queries/user_queries as queries
import gleam/json
import gleam/result.{try}
import wisp.{type Request, type Response}

pub fn create_user(req: Request, ctx: Context) -> Response {
use json <- wisp.require_json(req)

let result = {
// Decode the JSON into a NewUser record.
use new_user <- try(contexts.new_user_decoder(json))

// Save the user to the database.
use user <- try(queries.create_user(ctx.db, new_user))

// Construct a JSON payload with the id and name of the newly created user.
// TODO: case on the user result, return Ok or Error
Ok(
json.to_string_builder(
json.object([
#("id", json.int(user.id)),
#("name", json.string(user.name)),
#("email", json.string(user.email)),
#("password_hash", json.string(user.password_hash)),
]),
),
)
}

case result {
Ok(json) -> wisp.json_response(json, 201)
Error(Nil) -> wisp.unprocessable_entity()
}
}

0 comments on commit 9da3546

Please sign in to comment.