Env swap #21
|
@ -8,34 +8,12 @@ open Microsoft.Extensions.Hosting
|
||||||
open Giraffe
|
open Giraffe
|
||||||
open Giraffe.EndpointRouting
|
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
|
/// Configure the ASP.NET Core pipeline to use Giraffe
|
||||||
let configureApp (app : IApplicationBuilder) =
|
let configureApp (app : IApplicationBuilder) =
|
||||||
app
|
app
|
||||||
.UseRouting()
|
.UseRouting()
|
||||||
.UseEndpoints(fun e -> e.MapGiraffeEndpoints webApp)
|
.UseEndpoints(fun e -> e.MapGiraffeEndpoints Handlers.allEndpoints)
|
||||||
|> ignore
|
|> ignore
|
||||||
|
|
||||||
open NodaTime
|
open NodaTime
|
||||||
|
|
|
@ -185,11 +185,15 @@ let withReconn (conn : IConnection) =
|
||||||
(conn :?> Connection).Reconnect()
|
(conn :?> Connection).Reconnect()
|
||||||
| false -> ()))
|
| false -> ()))
|
||||||
|
|
||||||
|
open JobsJobsJobs.Domain.SharedTypes
|
||||||
|
|
||||||
/// Profile data access functions
|
/// Profile data access functions
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Profile =
|
module Profile =
|
||||||
|
|
||||||
|
open JobsJobsJobs.Domain
|
||||||
|
open RethinkDb.Driver.Ast
|
||||||
|
|
||||||
let count conn =
|
let count conn =
|
||||||
withReconn(conn).ExecuteAsync(fun () ->
|
withReconn(conn).ExecuteAsync(fun () ->
|
||||||
r.Table(Table.Profile)
|
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
|
/// Citizen data access functions
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
|
@ -315,6 +380,18 @@ module Continent =
|
||||||
.RunResultAsync<Continent list> conn)
|
.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
|
/// Success story data access functions
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Success =
|
module Success =
|
||||||
|
@ -339,3 +416,10 @@ module Success =
|
||||||
.RunWriteAsync conn)
|
.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
|
/// Handlers for /api/citizen routes
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
module Citizen =
|
module Citizen =
|
||||||
|
|
||||||
// GET: /api/citizen/log-on/[code]
|
// 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
|
/// Handlers for /api/profile routes
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module Profile =
|
module Profile =
|
||||||
|
@ -247,3 +260,117 @@ module Profile =
|
||||||
return! ok next ctx
|
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
|
module JobsJobsJobs.Domain.SharedTypes
|
||||||
|
|
||||||
open JobsJobsJobs.Domain.Types
|
open JobsJobsJobs.Domain.Types
|
||||||
|
open NodaTime
|
||||||
|
|
||||||
// fsharplint:disable FieldNames
|
// fsharplint:disable FieldNames
|
||||||
|
|
||||||
|
@ -102,3 +103,84 @@ module ProfileSearch =
|
||||||
match search.remoteWork with "" -> Some search.remoteWork | _ -> None
|
match search.remoteWork with "" -> Some search.remoteWork | _ -> None
|
||||||
]
|
]
|
||||||
|> List.exists Option.isSome
|
|> 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