diff --git a/src/JobsJobsJobs/Api/App.fs b/src/JobsJobsJobs/Api/App.fs index d5dde63..9404005 100644 --- a/src/JobsJobsJobs/Api/App.fs +++ b/src/JobsJobsJobs/Api/App.fs @@ -12,21 +12,20 @@ open Giraffe.EndpointRouting let webApp = [ subRoute "/api" [ subRoute "/citizen" [ - GET [ + GET_HEAD [ routef "/log-on/%s" Handlers.Citizen.logOn routef "/get/%O" Handlers.Citizen.get ] DELETE [ route "" Handlers.Citizen.delete ] ] - subRoute "/continent" [ - GET [ route "/all" Handlers.Continent.all ] - ] + GET_HEAD [ route "/continent/all" Handlers.Continent.all ] subRoute "/profile" [ - GET [ + GET_HEAD [ route "" Handlers.Profile.current route "/count" Handlers.Profile.count routef "/get/%O" Handlers.Profile.get ] + PATCH [ route "/employment-found" Handlers.Profile.employmentFound ] POST [ route "/save" Handlers.Profile.save ] ] ] diff --git a/src/JobsJobsJobs/Api/Data.fs b/src/JobsJobsJobs/Api/Data.fs index 6648ccb..e4fa42c 100644 --- a/src/JobsJobsJobs/Api/Data.fs +++ b/src/JobsJobsJobs/Api/Data.fs @@ -186,6 +186,49 @@ let withReconn (conn : IConnection) = | false -> ())) +/// Profile data access functions +[] +module Profile = + + let count conn = + withReconn(conn).ExecuteAsync(fun () -> + r.Table(Table.Profile) + .Count() + .RunResultAsync conn) + + /// Find a profile by citizen ID + let findById (citizenId : CitizenId) conn = task { + let! profile = + withReconn(conn).ExecuteAsync(fun () -> + r.Table(Table.Profile) + .Get(citizenId) + .RunResultAsync conn) + return toOption profile + } + + /// Insert or update a profile + let save (profile : Profile) conn = + withReconn(conn).ExecuteAsync(fun () -> task { + let! _ = + r.Table(Table.Profile) + .Get(profile.id) + .Replace(profile) + .RunWriteAsync conn + () + }) + + /// Delete a citizen's profile + let delete (citizenId : CitizenId) conn = + withReconn(conn).ExecuteAsync(fun () -> task { + let! _ = + r.Table(Table.Profile) + .Get(citizenId) + .Delete() + .RunWriteAsync conn + () + }) + + /// Citizen data access functions [] module Citizen = @@ -233,13 +276,9 @@ module Citizen = }) /// Delete a citizen - let delete (citizenId : CitizenId) conn = + let delete citizenId conn = withReconn(conn).ExecuteAsync(fun () -> task { - let! _ = - r.Table(Table.Profile) - .Get(citizenId) - .Delete() - .RunWriteAsync conn + do! Profile.delete citizenId conn let! _ = r.Table(Table.Success) .GetAll(citizenId).OptArg("index", "citizenId") @@ -276,38 +315,6 @@ module Continent = .RunResultAsync conn) -/// Profile data access functions -[] -module Profile = - - let count conn = - withReconn(conn).ExecuteAsync(fun () -> - r.Table(Table.Profile) - .Count() - .RunResultAsync conn) - - /// Find a profile by citizen ID - let findById (citizenId : CitizenId) conn = task { - let! profile = - withReconn(conn).ExecuteAsync(fun () -> - r.Table(Table.Profile) - .Get(citizenId) - .RunResultAsync conn) - return toOption profile - } - - /// Insert or update a profile - let save (profile : Profile) conn = - withReconn(conn).ExecuteAsync(fun () -> task { - let! _ = - r.Table(Table.Profile) - .Get(profile.id) - .Replace(profile) - .RunWriteAsync conn - () - }) - - /// Success story data access functions [] module Success = diff --git a/src/JobsJobsJobs/Api/Handlers.fs b/src/JobsJobsJobs/Api/Handlers.fs index 202f74c..0124973 100644 --- a/src/JobsJobsJobs/Api/Handlers.fs +++ b/src/JobsJobsJobs/Api/Handlers.fs @@ -59,13 +59,12 @@ module Helpers = /// Get the RethinkDB connection from the request context let conn (ctx : HttpContext) = ctx.GetService () - /// Return None if the string is null, empty, or whitespace; otherwise, return Some and the trimmed string - let noneIfEmpty x = - match (defaultArg (Option.ofObj x) "").Trim () with | "" -> None | it -> Some it + /// `None` if a `string option` is `None`, whitespace, or empty + let noneIfBlank (s : string option) = + s |> Option.map (fun x -> match x.Trim () with "" -> None | _ -> Some x) |> Option.flatten - /// Return None if an optional string is None or empty - let noneIfBlank s = - s |> Option.map (fun x -> match x with "" -> None | _ -> Some x) |> Option.flatten + /// `None` if a `string` is null, empty, or whitespace; otherwise, `Some` and the trimmed string + let noneIfEmpty = Option.ofObj >> noneIfBlank /// Try to get the current user let tryUser (ctx : HttpContext) = @@ -79,7 +78,10 @@ module Helpers = /// Get the ID of the currently logged in citizen // NOTE: if no one is logged in, this will raise an exception - let currentCitizenId ctx = (tryUser >> Option.get >> CitizenId.ofString) ctx + let currentCitizenId = tryUser >> Option.get >> CitizenId.ofString + + /// Return an empty OK response + let ok : HttpHandler = Successful.OK "" @@ -143,7 +145,7 @@ module Citizen = authorize >=> fun next ctx -> task { do! Data.Citizen.delete (currentCitizenId ctx) (conn ctx) - return! Successful.OK "" next ctx + return! ok next ctx } @@ -189,7 +191,7 @@ module Profile = >=> fun next ctx -> task { let! theCount = Data.Profile.count (conn ctx) return! json { count = theCount } next ctx - } + } // POST: /api/profile/save let save : HttpHandler = @@ -222,6 +224,26 @@ module Profile = }) } dbConn do! Data.Citizen.realNameUpdate citizenId (noneIfBlank (Some form.realName)) dbConn - return! Successful.OK "" next ctx - } + return! ok next ctx + } + + // PATCH: /api/profile/employment-found + let employmentFound : HttpHandler = + authorize + >=> fun next ctx -> task { + let dbConn = conn ctx + match! Data.Profile.findById (currentCitizenId ctx) dbConn with + | Some profile -> + do! Data.Profile.save { profile with seekingEmployment = false } dbConn + return! ok next ctx + | None -> return! Error.notFound next ctx + } + + // DELETE: /api/profile + let delete : HttpHandler = + authorize + >=> fun next ctx -> task { + do! Data.Profile.delete (currentCitizenId ctx) (conn ctx) + return! ok next ctx + } \ No newline at end of file diff --git a/src/JobsJobsJobs/Domain/SharedTypes.fs b/src/JobsJobsJobs/Domain/SharedTypes.fs index 4fcda5d..0505c9d 100644 --- a/src/JobsJobsJobs/Domain/SharedTypes.fs +++ b/src/JobsJobsJobs/Domain/SharedTypes.fs @@ -78,3 +78,27 @@ module ProfileForm = notes = s.notes }) } + + +/// The various ways profiles can be searched +type ProfileSearch = { + /// Retrieve citizens from this continent + continentId : string option + /// Text for a search within a citizen's skills + skill : string option + /// Text for a search with a citizen's professional biography and experience fields + bioExperience : string option + /// Whether to retrieve citizens who do or do not want remote work + remoteWork : string + } + +/// Support functions for profile searches +module ProfileSearch = + /// Is the search empty? + let isEmptySearch search = + [ search.continentId + search.skill + search.bioExperience + match search.remoteWork with "" -> Some search.remoteWork | _ -> None + ] + |> List.exists Option.isSome