Env swap #21
|
@ -8,34 +8,12 @@ open Microsoft.Extensions.Hosting
|
|||
open Giraffe
|
||||
open Giraffe.EndpointRouting
|
||||
|
||||
/// All available routes for the application
|
||||
let webApp = [
|
||||
subRoute "/api" [
|
||||
subRoute "/citizen" [
|
||||
GET_HEAD [
|
||||
routef "/log-on/%s" Handlers.Citizen.logOn
|
||||
routef "/get/%O" Handlers.Citizen.get
|
||||
]
|
||||
DELETE [ route "" Handlers.Citizen.delete ]
|
||||
]
|
||||
GET_HEAD [ route "/continent/all" Handlers.Continent.all ]
|
||||
subRoute "/profile" [
|
||||
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 ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
/// Configure the ASP.NET Core pipeline to use Giraffe
|
||||
let configureApp (app : IApplicationBuilder) =
|
||||
app
|
||||
.UseRouting()
|
||||
.UseEndpoints(fun e -> e.MapGiraffeEndpoints webApp)
|
||||
.UseEndpoints(fun e -> e.MapGiraffeEndpoints Handlers.allEndpoints)
|
||||
|> ignore
|
||||
|
||||
open NodaTime
|
||||
|
|
|
@ -185,11 +185,15 @@ let withReconn (conn : IConnection) =
|
|||
(conn :?> Connection).Reconnect()
|
||||
| false -> ()))
|
||||
|
||||
open JobsJobsJobs.Domain.SharedTypes
|
||||
|
||||
/// Profile data access functions
|
||||
[<RequireQualifiedAccess>]
|
||||
module Profile =
|
||||
|
||||
open JobsJobsJobs.Domain
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let count conn =
|
||||
withReconn(conn).ExecuteAsync(fun () ->
|
||||
r.Table(Table.Profile)
|
||||
|
@ -228,6 +232,67 @@ module Profile =
|
|||
()
|
||||
})
|
||||
|
||||
/// Search profiles (logged-on users)
|
||||
let search (srch : ProfileSearch) conn = task {
|
||||
let results =
|
||||
seq {
|
||||
match srch.continentId with
|
||||
| Some conId ->
|
||||
yield (fun (q : ReqlExpr) ->
|
||||
q.Filter(r.HashMap(nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||
| None -> ()
|
||||
match srch.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||
match srch.skill with
|
||||
| Some skl ->
|
||||
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
||||
upcast it.G("skills.description").Downcase().Match(skl.ToLowerInvariant ()))) :> ReqlExpr)
|
||||
| None -> ()
|
||||
match srch.bioExperience with
|
||||
| Some text ->
|
||||
let txt = text.ToLowerInvariant ()
|
||||
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
||||
upcast it.G("biography" ).Downcase().Match(txt)
|
||||
.Or(it.G("experience").Downcase().Match(txt)))) :> ReqlExpr)
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|> List.fold (fun q f -> f q) (r.Table(Table.Profile) :> ReqlExpr)
|
||||
// TODO: pluck fields, include display name
|
||||
return! results.RunResultAsync<ProfileSearchResult list> conn
|
||||
}
|
||||
|
||||
// Search profiles (public)
|
||||
let publicSearch (srch : PublicSearch) conn = task {
|
||||
let results =
|
||||
seq {
|
||||
match srch.continentId with
|
||||
| Some conId ->
|
||||
yield (fun (q : ReqlExpr) ->
|
||||
q.Filter(r.HashMap(nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||
| None -> ()
|
||||
match srch.region with
|
||||
| Some reg ->
|
||||
yield (fun q ->
|
||||
q.Filter(ReqlFunction1(fun it ->
|
||||
upcast it.G("region").Downcase().Match(reg.ToLowerInvariant ()))) :> ReqlExpr)
|
||||
| None -> ()
|
||||
match srch.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield (fun q -> q.Filter(r.HashMap(nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||
match srch.skill with
|
||||
| Some skl ->
|
||||
yield (fun q -> q.Filter(ReqlFunction1(fun it ->
|
||||
upcast it.G("skills.description").Downcase().Match(skl.ToLowerInvariant ()))) :> ReqlExpr)
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|> List.fold (fun q f -> f q) (r.Table(Table.Profile) :> ReqlExpr)
|
||||
// TODO: pluck fields, compile skills
|
||||
return! results.RunResultAsync<PublicSearchResult list> conn
|
||||
}
|
||||
|
||||
|
||||
/// Citizen data access functions
|
||||
[<RequireQualifiedAccess>]
|
||||
|
@ -315,6 +380,18 @@ module Continent =
|
|||
.RunResultAsync<Continent list> conn)
|
||||
|
||||
|
||||
/// Job listing data access functions
|
||||
[<RequireQualifiedAccess>]
|
||||
module Listing =
|
||||
|
||||
/// Find all job listings posted by the given citizen
|
||||
let findByCitizen (citizenId : CitizenId) conn =
|
||||
withReconn(conn).ExecuteAsync(fun () ->
|
||||
r.Table(Table.Listing)
|
||||
.GetAll(citizenId).OptArg("index", nameof citizenId)
|
||||
.RunResultAsync<Listing list> conn)
|
||||
|
||||
|
||||
/// Success story data access functions
|
||||
[<RequireQualifiedAccess>]
|
||||
module Success =
|
||||
|
@ -339,3 +416,10 @@ module Success =
|
|||
.RunWriteAsync conn)
|
||||
()
|
||||
}
|
||||
|
||||
// Retrieve all success stories
|
||||
let all conn =
|
||||
// TODO: identify query and fields that will make StoryEntry meaningful
|
||||
withReconn(conn).ExecuteAsync(fun () ->
|
||||
r.Table(Table.Success)
|
||||
.RunResultAsync<StoryEntry list> conn)
|
||||
|
|
|
@ -86,6 +86,7 @@ module Helpers =
|
|||
|
||||
|
||||
/// Handlers for /api/citizen routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module Citizen =
|
||||
|
||||
// GET: /api/citizen/log-on/[code]
|
||||
|
@ -161,6 +162,18 @@ module Continent =
|
|||
}
|
||||
|
||||
|
||||
/// Handlers for /api/listing[s] routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module Listing =
|
||||
|
||||
// GET: /api/listing/mine
|
||||
let mine : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let! listings = Data.Listing.findByCitizen (currentCitizenId ctx) (conn ctx)
|
||||
return! json listings next ctx
|
||||
}
|
||||
|
||||
/// Handlers for /api/profile routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module Profile =
|
||||
|
@ -247,3 +260,117 @@ module Profile =
|
|||
return! ok next ctx
|
||||
}
|
||||
|
||||
// GET: /api/profile/search
|
||||
let search : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let search = ctx.BindQueryString<ProfileSearch> ()
|
||||
let! results = Data.Profile.search search (conn ctx)
|
||||
return! json results next ctx
|
||||
}
|
||||
|
||||
// GET: /api/profile/public-search
|
||||
let publicSearch : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let search = ctx.BindQueryString<PublicSearch> ()
|
||||
let! results = Data.Profile.publicSearch search (conn ctx)
|
||||
return! json results next ctx
|
||||
}
|
||||
|
||||
|
||||
/// Handlers for /api/success routes
|
||||
[<RequireQualifiedAccess>]
|
||||
module Success =
|
||||
|
||||
open System
|
||||
|
||||
// GET: /api/success/[id]
|
||||
let get successId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
match! Data.Success.findById (SuccessId successId) (conn ctx) with
|
||||
| Some story -> return! json story next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /api/success/list
|
||||
let all : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let! stories = Data.Success.all (conn ctx)
|
||||
return! json stories next ctx
|
||||
}
|
||||
|
||||
// POST: /api/success/save
|
||||
let save : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let dbConn = conn ctx
|
||||
let now = (clock ctx).GetCurrentInstant ()
|
||||
let! form = ctx.BindJsonAsync<StoryForm> ()
|
||||
let! success = task {
|
||||
match form.id with
|
||||
| "new" ->
|
||||
return Some { id = (Guid.NewGuid >> SuccessId) ()
|
||||
citizenId = citizenId
|
||||
recordedOn = now
|
||||
fromHere = form.fromHere
|
||||
source = "profile"
|
||||
story = noneIfEmpty form.story |> Option.map Text
|
||||
}
|
||||
| successId ->
|
||||
match! Data.Success.findById (SuccessId.ofString successId) dbConn with
|
||||
| Some story when story.citizenId = citizenId ->
|
||||
return Some { story with
|
||||
fromHere = form.fromHere
|
||||
story = noneIfEmpty form.story |> Option.map Text
|
||||
}
|
||||
| Some _ | None -> return None
|
||||
}
|
||||
match success with
|
||||
| Some story ->
|
||||
do! Data.Success.save story dbConn
|
||||
return! ok next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
||||
open Giraffe.EndpointRouting
|
||||
|
||||
/// All available endpoints for the application
|
||||
let allEndpoints = [
|
||||
subRoute "/api" [
|
||||
subRoute "/citizen" [
|
||||
GET_HEAD [
|
||||
routef "/log-on/%s" Citizen.logOn
|
||||
routef "/get/%O" Citizen.get
|
||||
]
|
||||
DELETE [ route "" Citizen.delete ]
|
||||
]
|
||||
GET_HEAD [ route "/continent/all" Continent.all ]
|
||||
subRoute "/listing" [
|
||||
GET_HEAD [
|
||||
route "s/mine" Listing.mine
|
||||
]
|
||||
]
|
||||
subRoute "/profile" [
|
||||
GET_HEAD [
|
||||
route "" Profile.current
|
||||
route "/count" Profile.count
|
||||
routef "/get/%O" Profile.get
|
||||
route "/public-search" Profile.publicSearch
|
||||
route "/search" Profile.search
|
||||
]
|
||||
PATCH [ route "/employment-found" Profile.employmentFound ]
|
||||
POST [ route "/save" Profile.save ]
|
||||
]
|
||||
subRoute "/success" [
|
||||
GET_HEAD [
|
||||
routef "/get/%O" Success.get
|
||||
route "/list" Success.all
|
||||
]
|
||||
POST [ route "/save" Success.save ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
module JobsJobsJobs.Domain.SharedTypes
|
||||
|
||||
open JobsJobsJobs.Domain.Types
|
||||
open NodaTime
|
||||
|
||||
// fsharplint:disable FieldNames
|
||||
|
||||
|
@ -102,3 +103,84 @@ module ProfileSearch =
|
|||
match search.remoteWork with "" -> Some search.remoteWork | _ -> None
|
||||
]
|
||||
|> List.exists Option.isSome
|
||||
|
||||
|
||||
/// A user matching the profile search
|
||||
type ProfileSearchResult = {
|
||||
// The ID of the citizen
|
||||
citizenId : CitizenId
|
||||
// The citizen's display name
|
||||
displayName : string
|
||||
// Whether this citizen is currently seeking employment
|
||||
seekingEmployment : bool
|
||||
// Whether this citizen is looking for remote work
|
||||
remoteWork : bool
|
||||
// Whether this citizen is looking for full-time work
|
||||
fullTime : bool
|
||||
// When this profile was last updated
|
||||
lastUpdated : Instant
|
||||
}
|
||||
|
||||
|
||||
/// The parameters for a public job search
|
||||
type PublicSearch = {
|
||||
/// Retrieve citizens from this continent
|
||||
continentId : string option
|
||||
/// Retrieve citizens from this region
|
||||
region : string option
|
||||
/// Text for a search within a citizen's skills
|
||||
skill : string option
|
||||
/// Whether to retrieve citizens who do or do not want remote work
|
||||
remoteWork : string
|
||||
}
|
||||
|
||||
/// Support functions for pblic searches
|
||||
module PublicSearch =
|
||||
/// Is the search empty?
|
||||
let isEmptySearch (srch : PublicSearch) =
|
||||
[ srch.continentId
|
||||
srch.skill
|
||||
match srch.remoteWork with "" -> Some srch.remoteWork | _ -> None
|
||||
]
|
||||
|> List.exists Option.isSome
|
||||
|
||||
|
||||
/// A public profile search result
|
||||
type PublicSearchResult = {
|
||||
/// The name of the continent on which the citizen resides
|
||||
continent : string
|
||||
/// The region in which the citizen resides
|
||||
region : string
|
||||
/// Whether this citizen is seeking remote work
|
||||
remoteWork : bool
|
||||
/// The skills this citizen has identified
|
||||
skills : string list
|
||||
}
|
||||
|
||||
|
||||
/// The data required to provide a success story
|
||||
type StoryForm = {
|
||||
/// The ID of this story
|
||||
id : string
|
||||
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
||||
fromHere : bool
|
||||
/// The success story
|
||||
story : string
|
||||
}
|
||||
|
||||
|
||||
/// An entry in the list of success stories
|
||||
type StoryEntry = {
|
||||
/// The ID of this success story
|
||||
id : SuccessId
|
||||
/// The ID of the citizen who recorded this story
|
||||
citizenId : CitizenId
|
||||
/// The name of the citizen who recorded this story
|
||||
citizenName : string
|
||||
/// When this story was recorded
|
||||
RecordedOn : Instant
|
||||
/// Whether this story involves an opportunity that arose due to Jobs, Jobs, Jobs
|
||||
fromHere : bool
|
||||
/// Whether this report has a further story, or if it is simply a "found work" entry
|
||||
hasStory : bool
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user