diff --git a/server/src/server/crew.gleam b/server/src/server/crew.gleam index 4429a3c..dc5d3fb 100644 --- a/server/src/server/crew.gleam +++ b/server/src/server/crew.gleam @@ -26,29 +26,38 @@ pub fn find(db db: pog.Connection, id id: uuid.Uuid) -> Result(Crew, CrewError) |> result.map_error(DataBase), ) - use row <- result.try( + use row <- result.map( list.first(returned.rows) |> result.replace_error(NotFound), ) - use leader <- result.map( - user.find(db, row.leader_id) - |> result.map_error(UserError), + Crew( + id: row.id, + leader_id: row.leader_id, + name: row.crew_name, + is_active: row.is_active, ) +} - Crew(id: row.id, leader:, name: row.crew_name, is_active: row.is_active) +///  Find the leader assigned to the given crew. +pub fn find_leader( + db db: pog.Connection, + crew crew: Crew, +) -> Result(User, CrewError) { + user.find(db:, id: crew.leader_id) + |> result.map_error(UserError) } /// Register an new empty crew. /// Use the `assign` function to associate users to it. pub fn register( db db: pog.Connection, - leader leader: User, + leader_id leader_id: uuid.Uuid, crew_name crew_name: String, is_active is_active: Bool, ) -> Result(Crew, CrewError) { use returned <- result.try( - case sql.register(db, leader.id, crew_name, is_active) { + case sql.register(db, leader_id, crew_name, is_active) { // Crew name needs to be unique Error(pog.ConstraintViolated(constraint: "crew_name_key", ..)) -> Error(NameConflict) @@ -59,19 +68,14 @@ pub fn register( }, ) - use row <- result.try( + use row <- result.map( list.first(returned.rows) |> result.replace_error(NotFound), ) - use crew_leader <- result.map( - user.find(db, row.crew_leader) - |> result.map_error(UserError), - ) - Crew( id: row.id, - leader: crew_leader, + leader_id: row.leader_id, name: row.crew_name, is_active: row.is_active, ) @@ -81,10 +85,10 @@ pub fn register( /// Nothing happens is a member is already assigned. /// /// Returns all successfully assigned members. -pub fn assign( - db: pog.Connection, - crew: Crew, - members: List(User), +pub fn assign_members( + db db: pog.Connection, + crew crew: Crew, + members members: List(User), ) -> Result(List(User), CrewError) { let id_list = { use member <- list.map(members) @@ -104,15 +108,15 @@ pub fn assign( pub fn handle_error(err: CrewError) { case err { - NotFound -> wisp.not_found() - - UserError(err) -> user.handle_error(err) - NameConflict -> "Crew name is already taken" |> wisp.Text |> wisp.set_body(wisp.response(409), _) + NotFound -> wisp.not_found() + + UserError(err) -> user.handle_error(err) + DataBase(err) -> database.handle_error(err) } } diff --git a/server/src/server/crew/sql.gleam b/server/src/server/crew/sql.gleam index f6290d8..038f609 100644 --- a/server/src/server/crew/sql.gleam +++ b/server/src/server/crew/sql.gleam @@ -62,7 +62,7 @@ pub type FindRow { FindRow(id: Uuid, leader_id: Uuid, crew_name: String, is_active: Bool) } -/// Find a crew in the database +///  Find a crew /// /// > 🐿️ This function was generated automatically using v4.6.0 of /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). @@ -79,7 +79,7 @@ pub fn find( decode.success(FindRow(id:, leader_id:, crew_name:, is_active:)) } - "-- Find a crew in the database + "--  Find a crew select c.id, c.crew_leader as leader_id, @@ -157,7 +157,7 @@ where u.id = any(select m.id from member as m); /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). /// pub type RegisterRow { - RegisterRow(id: Uuid, crew_leader: Uuid, crew_name: String, is_active: Bool) + RegisterRow(id: Uuid, leader_id: Uuid, crew_name: String, is_active: Bool) } ///  Register an new empty crew @@ -173,10 +173,10 @@ pub fn register( ) -> Result(pog.Returned(RegisterRow), pog.QueryError) { let decoder = { use id <- decode.field(0, uuid_decoder()) - use crew_leader <- decode.field(1, uuid_decoder()) + use leader_id <- decode.field(1, uuid_decoder()) use crew_name <- decode.field(2, decode.string) use is_active <- decode.field(3, decode.bool) - decode.success(RegisterRow(id:, crew_leader:, crew_name:, is_active:)) + decode.success(RegisterRow(id:, leader_id:, crew_name:, is_active:)) } "--  Register an new empty crew @@ -186,7 +186,7 @@ insert into crew as c ( is_active ) values ($1::uuid, $2::text, $3::boolean) -returning c.id, c.crew_leader, c.crew_name, c.is_active; +returning c.id, c.crew_leader as leader_id, c.crew_name, c.is_active; " |> pog.query |> pog.parameter(pog.text(uuid.to_string(arg_1))) diff --git a/server/src/server/crew/sql/find.sql b/server/src/server/crew/sql/find.sql index 650ee7e..f1488d0 100644 --- a/server/src/server/crew/sql/find.sql +++ b/server/src/server/crew/sql/find.sql @@ -1,4 +1,4 @@ --- Find a crew in the database +--  Find a crew select c.id, c.crew_leader as leader_id, diff --git a/server/src/server/crew/sql/register.sql b/server/src/server/crew/sql/register.sql index b8312e3..743ed03 100644 --- a/server/src/server/crew/sql/register.sql +++ b/server/src/server/crew/sql/register.sql @@ -5,4 +5,4 @@ insert into crew as c ( is_active ) values ($1::uuid, $2::text, $3::boolean) -returning c.id, c.crew_leader, c.crew_name, c.is_active; +returning c.id, c.crew_leader as leader_id, c.crew_name, c.is_active; diff --git a/server/src/server/router/register_crew.gleam b/server/src/server/router/register_crew.gleam index c855ee6..8d33731 100644 --- a/server/src/server/router/register_crew.gleam +++ b/server/src/server/router/register_crew.gleam @@ -28,12 +28,12 @@ pub fn handle_request(req: Request, ctx: Context) -> Response { fn handle_body(ctx: Context, body: contract.RequestBody) -> Response { let query_result = { use crew <- result.try( - crew.register(ctx.db, body.crew_leader, body.crew_name, is_active: True) + crew.register(ctx.db, body.leader_id, body.crew_name, is_active: True) |> result.map_error(CrewError), ) use assigned <- result.map( - crew.assign(ctx.db, crew, body.members) + crew.assign_members(ctx.db, crew, body.members) |> result.map_error(CrewError), ) diff --git a/server/src/server/user.gleam b/server/src/server/user.gleam index 89862be..b0d7702 100644 --- a/server/src/server/user.gleam +++ b/server/src/server/user.gleam @@ -43,15 +43,14 @@ pub fn find(db db: pog.Connection, id id: uuid.Uuid) -> Result(User, UserError) ) } -/// 󰇮 Find all users which email starts with the given prefix. -/// You can limit the number of results. +/// 󰇮 Find users by their email pub fn find_by_email( db db: pog.Connection, prefix prefix: String, limit max: Int, ) -> Result(List(User), UserError) { use returned <- result.map( - sql.find_by_email_prefix(db, prefix, max) + sql.find_by_email(db, prefix, max) |> result.map_error(DataBase), ) @@ -67,8 +66,7 @@ pub fn find_by_email( }) } -/// 󰀖 Find all users which name is similar to the given input. -/// Text search supports partial matches. +/// 󰇮 Find users by their name pub fn find_by_name( db db: pog.Connection, name name: String, @@ -149,8 +147,6 @@ pub fn register( pub fn handle_error(err: UserError) -> wisp.Response { case err { - DataBase(err) -> database.handle_error(err) - NotFound -> "User not found" |> wisp.Text @@ -165,6 +161,8 @@ pub fn handle_error(err: UserError) -> wisp.Response { "Email is already taken" |> wisp.Text |> wisp.set_body(wisp.response(409), _) + + DataBase(err) -> database.handle_error(err) } } diff --git a/server/src/server/user/sql.gleam b/server/src/server/user/sql.gleam index 61aa6fa..5310dc1 100644 --- a/server/src/server/user/sql.gleam +++ b/server/src/server/user/sql.gleam @@ -25,7 +25,7 @@ pub type FindRow { ) } -///  Find details about an user +///  Find a user /// /// > 🐿️ This function was generated automatically using v4.6.0 of /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). @@ -44,7 +44,7 @@ pub fn find( decode.success(FindRow(id:, role:, full_name:, email:, phone:, is_active:)) } - "--  Find details about an user + "--  Find a user select u.id, u.user_role as role, @@ -61,14 +61,14 @@ where u.id = $1::uuid; |> pog.execute(db) } -/// A row you get from running the `find_by_email_prefix` query -/// defined in `./src/server/user/sql/find_by_email_prefix.sql`. +/// A row you get from running the `find_by_email` query +/// defined in `./src/server/user/sql/find_by_email.sql`. /// /// > 🐿️ This type definition was generated automatically using v4.6.0 of the /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). /// -pub type FindByEmailPrefixRow { - FindByEmailPrefixRow( +pub type FindByEmailRow { + FindByEmailRow( id: Uuid, role: UserRoleEnum, full_name: String, @@ -78,16 +78,16 @@ pub type FindByEmailPrefixRow { ) } -/// 󰇮 Find all users which email starts with the given prefix +/// 󰇮 Find users by their email /// /// > 🐿️ This function was generated automatically using v4.6.0 of /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). /// -pub fn find_by_email_prefix( +pub fn find_by_email( db: pog.Connection, arg_1: String, arg_2: Int, -) -> Result(pog.Returned(FindByEmailPrefixRow), pog.QueryError) { +) -> Result(pog.Returned(FindByEmailRow), pog.QueryError) { let decoder = { use id <- decode.field(0, uuid_decoder()) use role <- decode.field(1, user_role_enum_decoder()) @@ -95,7 +95,7 @@ pub fn find_by_email_prefix( use email <- decode.field(3, decode.string) use phone <- decode.field(4, decode.string) use is_active <- decode.field(5, decode.bool) - decode.success(FindByEmailPrefixRow( + decode.success(FindByEmailRow( id:, role:, full_name:, @@ -105,7 +105,7 @@ pub fn find_by_email_prefix( )) } - "-- 󰇮 Find all users which email starts with the given prefix + "-- 󰇮 Find users by their email select u.id, u.user_role as role, @@ -141,7 +141,7 @@ pub type FindByNameRow { ) } -///  Find a user by their name +///  Find users by their name /// /// > 🐿️ This function was generated automatically using v4.6.0 of /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). @@ -168,7 +168,7 @@ pub fn find_by_name( )) } - "--  Find a user by their name + "--  Find users by their name select u.id, u.user_role as role, diff --git a/server/src/server/user/sql/find.sql b/server/src/server/user/sql/find.sql index 0d49dcc..a2c976d 100644 --- a/server/src/server/user/sql/find.sql +++ b/server/src/server/user/sql/find.sql @@ -1,4 +1,4 @@ ---  Find details about an user +--  Find a user select u.id, u.user_role as role, diff --git a/server/src/server/user/sql/find_by_email_prefix.sql b/server/src/server/user/sql/find_by_email_prefix.sql deleted file mode 100644 index 1bf7ced..0000000 --- a/server/src/server/user/sql/find_by_email_prefix.sql +++ /dev/null @@ -1,11 +0,0 @@ --- 󰇮 Find all users which email starts with the given prefix -select - u.id, - u.user_role as role, - u.full_name, - u.email, - u.phone, - u.is_active -from user_account as u -where starts_with(u.email_lowercase, lower($1::text)) -limit $2; diff --git a/server/src/server/user/sql/find_by_name.sql b/server/src/server/user/sql/find_by_name.sql index c4ee2a4..f3d52cc 100644 --- a/server/src/server/user/sql/find_by_name.sql +++ b/server/src/server/user/sql/find_by_name.sql @@ -1,4 +1,4 @@ ---  Find a user by their name +--  Find users by their name select u.id, u.user_role as role, diff --git a/server/test/crew_test.gleam b/server/test/crew_test.gleam index 5f642b6..ff394d8 100644 --- a/server/test/crew_test.gleam +++ b/server/test/crew_test.gleam @@ -1,51 +1,51 @@ import gleam/http import gleam/int import gleam/json -import gleam/list import gleam/set import server/router import server/user import server_test import shared/contract/register_crew as register_crew_contract import shared/role +import wisp import wisp/simulate pub fn register_crew_test() -> Nil { use ctx <- server_test.with_context() - let crew_name = "teste_crew" + let crew_name = wisp.random_string(12) let crew_is_active = True let assert Ok(crew_leader) = user.register( ctx, - user_name: "leader", + user_name: wisp.random_string(12), user_role: role.Captain, - user_password: "password", - user_email: "leader@email.com", - user_phone: "9990000000", + user_password: wisp.random_string(12), + user_email: wisp.random_string(12) <> "@leader.com", + user_phone: wisp.random_string(12), is_active: True, ) - let members = - int.range(0, 5, [], fn(acc, i) { - let assert Ok(member) = - user.register( - ctx:, - user_name: "dummy", - user_role: role.None, - user_password: "dummy_password", - user_email: "dummy" <> int.to_string(i) <> "@email.com", - user_phone: "dummy_phone" <> int.to_string(i), - is_active: True, - ) + let members = { + use acc, _ <- int.range(0, 5, []) + let assert Ok(member) = + user.register( + ctx:, + user_name: wisp.random_string(12), + user_role: role.None, + user_password: wisp.random_string(12), + user_email: wisp.random_string(12) <> "@member.com", + user_phone: wisp.random_string(12), + is_active: True, + ) - [member, ..acc] - |> list.reverse() - }) + [member, ..acc] + } + let leader_id = crew_leader.id let body = - register_crew_contract.RequestBody(crew_leader:, crew_name:, members:) + register_crew_contract.RequestBody(leader_id:, crew_name:, members:) |> register_crew_contract.request_to_json let req = @@ -70,7 +70,7 @@ pub fn register_crew_test() -> Nil { let assert Ok(returned) = json.parse(body, register_crew_contract.response_decoder()) - assert returned.crew.leader == crew_leader + assert returned.crew.leader_id == crew_leader.id assert returned.crew.is_active == crew_is_active assert returned.crew.name == crew_name diff --git a/server/test/user/autocomplete_test.gleam b/server/test/user/autocomplete_test.gleam index 29ce348..b247efd 100644 --- a/server/test/user/autocomplete_test.gleam +++ b/server/test/user/autocomplete_test.gleam @@ -14,7 +14,7 @@ import wisp/simulate pub fn find_by_email_test() { use ctx <- testing.with_context() let path = "/api/user/autocomplete" - let prefix = "dummy_" + let prefix = "dummy" let first_email = prefix <> wisp.random_string(12) let assert Ok(first_dummy) = diff --git a/shared/src/shared/contract/register_crew.gleam b/shared/src/shared/contract/register_crew.gleam index 35ee051..2f242e5 100644 --- a/shared/src/shared/contract/register_crew.gleam +++ b/shared/src/shared/contract/register_crew.gleam @@ -1,7 +1,9 @@ import gleam/dynamic/decode import gleam/json import shared/crew.{type Crew} +import shared/helper import shared/user.{type User} +import youid/uuid pub const url = "/api/crew" @@ -10,23 +12,25 @@ pub const autocomplete_url = "/api/user/autocomplete" // REQUEST --------------------------------------------------------------------- pub type RequestBody { - RequestBody(crew_leader: User, crew_name: String, members: List(User)) + RequestBody(leader_id: uuid.Uuid, crew_name: String, members: List(User)) } pub fn request_decoder() -> decode.Decoder(RequestBody) { - use crew_leader <- decode.field("crew_leader", user.decoder()) + let leader_decoder = helper.uuid_decoder("leader_uuid") + + use leader_id <- decode.field("crew_leader", leader_decoder) use crew_name <- decode.field("crew_name", decode.string) use members <- decode.field("crew_members", decode.list(user.decoder())) - RequestBody(crew_leader:, crew_name:, members:) + RequestBody(leader_id:, crew_name:, members:) |> decode.success } pub fn request_to_json(request_body: RequestBody) -> json.Json { - let RequestBody(crew_leader:, crew_name:, members:) = request_body + let RequestBody(leader_id:, crew_name:, members:) = request_body json.object([ - #("crew_leader", user.to_json(crew_leader)), + #("crew_leader", helper.uuid_to_json(leader_id)), #("crew_name", json.string(crew_name)), #("crew_members", json.array(members, user.to_json)), ]) @@ -40,7 +44,7 @@ pub type ResponseBody { pub fn response_decoder() -> decode.Decoder(ResponseBody) { use crew <- decode.field("crew", crew.decoder()) - use assigned <- decode.field("crew_members", decode.list(user.decoder())) + use assigned <- decode.field("assigned", decode.list(user.decoder())) decode.success(ResponseBody(crew:, assigned:)) } @@ -48,6 +52,6 @@ pub fn response_decoder() -> decode.Decoder(ResponseBody) { pub fn response_to_json(body: ResponseBody) -> json.Json { json.object([ #("crew", crew.to_json(body.crew)), - #("crew_members", json.array(body.assigned, user.to_json)), + #("assigned", json.array(body.assigned, user.to_json)), ]) } diff --git a/shared/src/shared/crew.gleam b/shared/src/shared/crew.gleam index 7eea541..667734e 100644 --- a/shared/src/shared/crew.gleam +++ b/shared/src/shared/crew.gleam @@ -1,38 +1,27 @@ import gleam/dynamic/decode import gleam/json -import shared/user.{type User} +import shared/helper import youid/uuid pub type Crew { - Crew(id: uuid.Uuid, leader: User, name: String, is_active: Bool) + Crew(id: uuid.Uuid, leader_id: uuid.Uuid, name: String, is_active: Bool) } pub fn decoder() -> decode.Decoder(Crew) { - use id <- decode.field("id", uuid_decoder()) - use leader <- decode.field("leader", user.decoder()) + use id <- decode.field("id", helper.uuid_decoder("crew_uuid")) + use leader_id <- decode.field("leader_id", helper.uuid_decoder("leader_uuid")) use name <- decode.field("name", decode.string) use is_active <- decode.field("is_active", decode.bool) - decode.success(Crew(id:, leader:, name:, is_active:)) + + decode.success(Crew(id:, leader_id:, name:, is_active:)) } pub fn to_json(crew: Crew) -> json.Json { - let Crew(id:, leader:, name:, is_active:) = crew + let Crew(id:, leader_id:, name:, is_active:) = crew json.object([ - #("id", uuid_to_json(id)), - #("leader", user.to_json(leader)), + #("id", helper.uuid_to_json(id)), + #("leader_id", helper.uuid_to_json(leader_id)), #("name", json.string(name)), #("is_active", json.bool(is_active)), ]) } - -fn uuid_decoder() -> decode.Decoder(uuid.Uuid) { - use value <- decode.then(decode.string) - case uuid.from_string(value) { - Ok(value) -> decode.success(value) - Error(_) -> decode.failure(uuid.v7(), "crew_uuid") - } -} - -fn uuid_to_json(uuid: uuid.Uuid) { - uuid.to_string(uuid) |> json.string() -} diff --git a/shared/src/shared/helper.gleam b/shared/src/shared/helper.gleam new file mode 100644 index 0000000..f2727cd --- /dev/null +++ b/shared/src/shared/helper.gleam @@ -0,0 +1,15 @@ +import gleam/dynamic/decode +import gleam/json +import youid/uuid + +pub fn uuid_decoder(expected: String) -> decode.Decoder(uuid.Uuid) { + use value <- decode.then(decode.string) + case uuid.from_string(value) { + Ok(value) -> decode.success(value) + Error(_) -> decode.failure(uuid.v7(), expected:) + } +} + +pub fn uuid_to_json(uuid: uuid.Uuid) { + uuid.to_string(uuid) |> json.string() +}