Version 3 #40

Merged
danieljsummers merged 67 commits from version-2-3 into main 2023-02-02 23:47:28 +00:00
4 changed files with 230 additions and 111 deletions
Showing only changes of commit 7d2a2a50eb - Show all commits

View File

@ -17,16 +17,20 @@ let private fromShortGuid (it : string) =
/// Support functions for citizen IDs /// Support functions for citizen IDs
module CitizenId = module CitizenId =
/// Create a new citizen ID /// Create a new citizen ID
let create () = (Guid.NewGuid >> CitizenId) () let create () = (Guid.NewGuid >> CitizenId) ()
/// A string representation of a citizen ID /// A string representation of a citizen ID
let toString = function CitizenId it -> toShortGuid it let toString = function CitizenId it -> toShortGuid it
/// Parse a string into a citizen ID /// Parse a string into a citizen ID
let ofString = fromShortGuid >> CitizenId let ofString = fromShortGuid >> CitizenId
/// Support functions for citizens /// Support functions for citizens
module Citizen = module Citizen =
/// Get the name of the citizen (the first of real name, display name, or handle that is filled in) /// Get the name of the citizen (the first of real name, display name, or handle that is filled in)
let name x = let name x =
[ x.realName; x.displayName; Some x.mastodonUser ] [ x.realName; x.displayName; Some x.mastodonUser ]
@ -36,34 +40,46 @@ module Citizen =
/// Support functions for continent IDs /// Support functions for continent IDs
module ContinentId = module ContinentId =
/// Create a new continent ID /// Create a new continent ID
let create () = (Guid.NewGuid >> ContinentId) () let create () = (Guid.NewGuid >> ContinentId) ()
/// A string representation of a continent ID /// A string representation of a continent ID
let toString = function ContinentId it -> toShortGuid it let toString = function ContinentId it -> toShortGuid it
/// Parse a string into a continent ID /// Parse a string into a continent ID
let ofString = fromShortGuid >> ContinentId let ofString = fromShortGuid >> ContinentId
/// Support functions for listing IDs /// Support functions for listing IDs
module ListingId = module ListingId =
/// Create a new job listing ID /// Create a new job listing ID
let create () = (Guid.NewGuid >> ListingId) () let create () = (Guid.NewGuid >> ListingId) ()
/// A string representation of a listing ID /// A string representation of a listing ID
let toString = function ListingId it -> toShortGuid it let toString = function ListingId it -> toShortGuid it
/// Parse a string into a listing ID /// Parse a string into a listing ID
let ofString = fromShortGuid >> ListingId let ofString = fromShortGuid >> ListingId
/// Support functions for Markdown strings /// Support functions for Markdown strings
module MarkdownString = module MarkdownString =
/// The Markdown conversion pipeline (enables all advanced features) /// The Markdown conversion pipeline (enables all advanced features)
let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build () let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build ()
/// Convert this Markdown string to HTML /// Convert this Markdown string to HTML
let toHtml = function Text text -> Markdown.ToHtml (text, pipeline) let toHtml = function Text text -> Markdown.ToHtml (text, pipeline)
/// Convert a Markdown string to its string representation
let toString = function Text text -> text
/// Support functions for Profiles /// Support functions for Profiles
module Profile = module Profile =
// An empty profile // An empty profile
let empty = let empty =
{ id = CitizenId Guid.Empty { id = CitizenId Guid.Empty
@ -79,21 +95,28 @@ module Profile =
skills = [] skills = []
} }
/// Support functions for skill IDs /// Support functions for skill IDs
module SkillId = module SkillId =
/// Create a new skill ID /// Create a new skill ID
let create () = (Guid.NewGuid >> SkillId) () let create () = (Guid.NewGuid >> SkillId) ()
/// A string representation of a skill ID /// A string representation of a skill ID
let toString = function SkillId it -> toShortGuid it let toString = function SkillId it -> toShortGuid it
/// Parse a string into a skill ID /// Parse a string into a skill ID
let ofString = fromShortGuid >> SkillId let ofString = fromShortGuid >> SkillId
/// Support functions for success report IDs /// Support functions for success report IDs
module SuccessId = module SuccessId =
/// Create a new success report ID /// Create a new success report ID
let create () = (Guid.NewGuid >> SuccessId) () let create () = (Guid.NewGuid >> SuccessId) ()
/// A string representation of a success report ID /// A string representation of a success report ID
let toString = function SuccessId it -> toShortGuid it let toString = function SuccessId it -> toShortGuid it
/// Parse a string into a success report ID /// Parse a string into a success report ID
let ofString = fromShortGuid >> SuccessId let ofString = fromShortGuid >> SuccessId

View File

@ -11,16 +11,22 @@ open NodaTime
type ListingForm = type ListingForm =
{ /// The ID of the listing { /// The ID of the listing
id : string id : string
/// The listing title /// The listing title
title : string title : string
/// The ID of the continent on which this opportunity exists /// The ID of the continent on which this opportunity exists
continentId : string continentId : string
/// The region in which this opportunity exists /// The region in which this opportunity exists
region : string region : string
/// Whether this is a remote work opportunity /// Whether this is a remote work opportunity
remoteWork : bool remoteWork : bool
/// The text of the job listing /// The text of the job listing
text : string text : string
/// The date by which this job listing is needed /// The date by which this job listing is needed
neededBy : string option neededBy : string option
} }
@ -30,6 +36,7 @@ type ListingForm =
type ListingForView = type ListingForView =
{ /// The listing itself { /// The listing itself
listing : Listing listing : Listing
/// The continent for that listing /// The continent for that listing
continent : Continent continent : Continent
} }
@ -39,6 +46,7 @@ type ListingForView =
type ListingExpireForm = type ListingExpireForm =
{ /// Whether the job was filled from here { /// Whether the job was filled from here
fromHere : bool fromHere : bool
/// The success story written by the user /// The success story written by the user
successStory : string option successStory : string option
} }
@ -49,10 +57,13 @@ type ListingExpireForm =
type ListingSearch = type ListingSearch =
{ /// Retrieve job listings for this continent { /// Retrieve job listings for this continent
continentId : string option continentId : string option
/// Text for a search within a region /// Text for a search within a region
region : string option region : string option
/// Whether to retrieve job listings for remote work /// Whether to retrieve job listings for remote work
remoteWork : string remoteWork : string
/// Text for a search with the job listing description /// Text for a search with the job listing description
text : string option text : string option
} }
@ -62,8 +73,10 @@ type ListingSearch =
type LogOnSuccess = type LogOnSuccess =
{ /// The JSON Web Token (JWT) to use for API access { /// The JSON Web Token (JWT) to use for API access
jwt : string jwt : string
/// The ID of the logged-in citizen (as a string) /// The ID of the logged-in citizen (as a string)
citizenId : string citizenId : string
/// The name of the logged-in citizen /// The name of the logged-in citizen
name : string name : string
} }
@ -78,30 +91,41 @@ type Count =
/// An instance of a Mastodon server which is configured to work with Jobs, Jobs, Jobs /// An instance of a Mastodon server which is configured to work with Jobs, Jobs, Jobs
type MastodonInstance () = type MastodonInstance () =
/// The name of the instance /// The name of the instance
member val Name = "" with get, set member val Name = "" with get, set
/// The URL for this instance /// The URL for this instance
member val Url = "" with get, set member val Url = "" with get, set
/// The abbreviation used in the URL to distinguish this instance's return codes /// The abbreviation used in the URL to distinguish this instance's return codes
member val Abbr = "" with get, set member val Abbr = "" with get, set
/// The client ID (assigned by the Mastodon server) /// The client ID (assigned by the Mastodon server)
member val ClientId = "" with get, set member val ClientId = "" with get, set
/// The cryptographic secret (provided by the Mastodon server) /// The cryptographic secret (provided by the Mastodon server)
member val Secret = "" with get, set member val Secret = "" with get, set
/// Whether the instance is currently enabled /// Whether the instance is currently enabled
member val IsEnabled = true with get, set member val IsEnabled = true with get, set
/// If an instance is disabled, the reason for it being disabled /// If an instance is disabled, the reason for it being disabled
member val Reason = "" with get, set member val Reason = "" with get, set
/// The authorization options for Jobs, Jobs, Jobs /// The authorization options for Jobs, Jobs, Jobs
type AuthOptions () = type AuthOptions () =
/// The host for the return URL for Mastodon verification /// The host for the return URL for Mastodon verification
member val ReturnHost = "" with get, set member val ReturnHost = "" with get, set
/// The secret with which the server signs the JWTs for auth once we've verified with Mastodon /// The secret with which the server signs the JWTs for auth once we've verified with Mastodon
member val ServerSecret = "" with get, set member val ServerSecret = "" with get, set
/// The instances configured for use /// The instances configured for use
member val Instances = Array.empty<MastodonInstance> with get, set member val Instances = Array.empty<MastodonInstance> with get, set
interface IOptions<AuthOptions> with interface IOptions<AuthOptions> with
override this.Value = this override this.Value = this
@ -110,14 +134,19 @@ type AuthOptions () =
type Instance = type Instance =
{ /// The name of the instance { /// The name of the instance
name : string name : string
/// The URL for this instance /// The URL for this instance
url : string url : string
/// The abbreviation used in the URL to distinguish this instance's return codes /// The abbreviation used in the URL to distinguish this instance's return codes
abbr : string abbr : string
/// The client ID (assigned by the Mastodon server) /// The client ID (assigned by the Mastodon server)
clientId : string clientId : string
/// Whether this instance is currently enabled /// Whether this instance is currently enabled
isEnabled : bool isEnabled : bool
/// If not enabled, the reason the instance is disabled /// If not enabled, the reason the instance is disabled
reason : string reason : string
} }
@ -127,8 +156,10 @@ type Instance =
type SkillForm = type SkillForm =
{ /// The ID of this skill { /// The ID of this skill
id : string id : string
/// The description of the skill /// The description of the skill
description : string description : string
/// Notes regarding the skill /// Notes regarding the skill
notes : string option notes : string option
} }
@ -138,28 +169,38 @@ type SkillForm =
type ProfileForm = type ProfileForm =
{ /// Whether the citizen to whom this profile belongs is actively seeking employment { /// Whether the citizen to whom this profile belongs is actively seeking employment
isSeekingEmployment : bool isSeekingEmployment : bool
/// Whether this profile should appear in the public search /// Whether this profile should appear in the public search
isPublic : bool isPublic : bool
/// The user's real name /// The user's real name
realName : string realName : string
/// The ID of the continent on which the citizen is located /// The ID of the continent on which the citizen is located
continentId : string continentId : string
/// The area within that continent where the citizen is located /// The area within that continent where the citizen is located
region : string region : string
/// If the citizen is available for remote work /// If the citizen is available for remote work
remoteWork : bool remoteWork : bool
/// If the citizen is seeking full-time employment /// If the citizen is seeking full-time employment
fullTime : bool fullTime : bool
/// The user's professional biography /// The user's professional biography
biography : string biography : string
/// The user's past experience /// The user's past experience
experience : string option experience : string option
/// The skills for the user /// The skills for the user
skills : SkillForm list skills : SkillForm list
} }
/// Support functions for the ProfileForm type /// Support functions for the ProfileForm type
module ProfileForm = module ProfileForm =
/// Create an instance of this form from the given profile /// Create an instance of this form from the given profile
let fromProfile (profile : Types.Profile) = let fromProfile (profile : Types.Profile) =
{ isSeekingEmployment = profile.seekingEmployment { isSeekingEmployment = profile.seekingEmployment
@ -169,8 +210,8 @@ module ProfileForm =
region = profile.region region = profile.region
remoteWork = profile.remoteWork remoteWork = profile.remoteWork
fullTime = profile.fullTime fullTime = profile.fullTime
biography = match profile.biography with Text bio -> bio biography = MarkdownString.toString profile.biography
experience = profile.experience |> Option.map (fun x -> match x with Text exp -> exp) experience = profile.experience |> Option.map MarkdownString.toString
skills = profile.skills skills = profile.skills
|> List.map (fun s -> |> List.map (fun s ->
{ id = string s.id { id = string s.id
@ -185,10 +226,13 @@ module ProfileForm =
type ProfileSearch = type ProfileSearch =
{ /// Retrieve citizens from this continent { /// Retrieve citizens from this continent
continentId : string option continentId : string option
/// Text for a search within a citizen's skills /// Text for a search within a citizen's skills
skill : string option skill : string option
/// Text for a search with a citizen's professional biography and experience fields /// Text for a search with a citizen's professional biography and experience fields
bioExperience : string option bioExperience : string option
/// Whether to retrieve citizens who do or do not want remote work /// Whether to retrieve citizens who do or do not want remote work
remoteWork : string remoteWork : string
} }
@ -198,14 +242,19 @@ type ProfileSearch =
type ProfileSearchResult = type ProfileSearchResult =
{ /// The ID of the citizen { /// The ID of the citizen
citizenId : CitizenId citizenId : CitizenId
/// The citizen's display name /// The citizen's display name
displayName : string displayName : string
/// Whether this citizen is currently seeking employment /// Whether this citizen is currently seeking employment
seekingEmployment : bool seekingEmployment : bool
/// Whether this citizen is looking for remote work /// Whether this citizen is looking for remote work
remoteWork : bool remoteWork : bool
/// Whether this citizen is looking for full-time work /// Whether this citizen is looking for full-time work
fullTime : bool fullTime : bool
/// When this profile was last updated /// When this profile was last updated
lastUpdatedOn : Instant lastUpdatedOn : Instant
} }
@ -215,8 +264,10 @@ type ProfileSearchResult =
type ProfileForView = type ProfileForView =
{ /// The profile itself { /// The profile itself
profile : Profile profile : Profile
/// The citizen to whom the profile belongs /// The citizen to whom the profile belongs
citizen : Citizen citizen : Citizen
/// The continent for the profile /// The continent for the profile
continent : Continent continent : Continent
} }
@ -227,10 +278,13 @@ type ProfileForView =
type PublicSearch = type PublicSearch =
{ /// Retrieve citizens from this continent { /// Retrieve citizens from this continent
continentId : string option continentId : string option
/// Retrieve citizens from this region /// Retrieve citizens from this region
region : string option region : string option
/// Text for a search within a citizen's skills /// Text for a search within a citizen's skills
skill : string option skill : string option
/// Whether to retrieve citizens who do or do not want remote work /// Whether to retrieve citizens who do or do not want remote work
remoteWork : string remoteWork : string
} }
@ -250,10 +304,13 @@ module PublicSearch =
type PublicSearchResult = type PublicSearchResult =
{ /// The name of the continent on which the citizen resides { /// The name of the continent on which the citizen resides
continent : string continent : string
/// The region in which the citizen resides /// The region in which the citizen resides
region : string region : string
/// Whether this citizen is seeking remote work /// Whether this citizen is seeking remote work
remoteWork : bool remoteWork : bool
/// The skills this citizen has identified /// The skills this citizen has identified
skills : string list skills : string list
} }
@ -263,8 +320,10 @@ type PublicSearchResult =
type StoryForm = type StoryForm =
{ /// The ID of this story { /// The ID of this story
id : string id : string
/// Whether the employment was obtained from Jobs, Jobs, Jobs /// Whether the employment was obtained from Jobs, Jobs, Jobs
fromHere : bool fromHere : bool
/// The success story /// The success story
story : string story : string
} }
@ -274,14 +333,19 @@ type StoryForm =
type StoryEntry = type StoryEntry =
{ /// The ID of this success story { /// The ID of this success story
id : SuccessId id : SuccessId
/// The ID of the citizen who recorded this story /// The ID of the citizen who recorded this story
citizenId : CitizenId citizenId : CitizenId
/// The name of the citizen who recorded this story /// The name of the citizen who recorded this story
citizenName : string citizenName : string
/// When this story was recorded /// When this story was recorded
recordedOn : Instant recordedOn : Instant
/// Whether this story involves an opportunity that arose due to Jobs, Jobs, Jobs /// Whether this story involves an opportunity that arose due to Jobs, Jobs, Jobs
fromHere : bool fromHere : bool
/// Whether this report has a further story, or if it is simply a "found work" entry /// Whether this report has a further story, or if it is simply a "found work" entry
hasStory : bool hasStory : bool
} }

View File

@ -14,18 +14,25 @@ type CitizenId = CitizenId of Guid
type Citizen = type Citizen =
{ /// The ID of the user { /// The ID of the user
id : CitizenId id : CitizenId
/// The Mastodon instance abbreviation from which this citizen is authorized /// The Mastodon instance abbreviation from which this citizen is authorized
instance : string instance : string
/// The handle by which the user is known on Mastodon /// The handle by which the user is known on Mastodon
mastodonUser : string mastodonUser : string
/// The user's display name from Mastodon (updated every login) /// The user's display name from Mastodon (updated every login)
displayName : string option displayName : string option
/// The user's real name /// The user's real name
realName : string option realName : string option
/// The URL for the user's Mastodon profile /// The URL for the user's Mastodon profile
profileUrl : string profileUrl : string
/// When the user joined Jobs, Jobs, Jobs /// When the user joined Jobs, Jobs, Jobs
joinedOn : Instant joinedOn : Instant
/// When the user last logged in /// When the user last logged in
lastSeenOn : Instant lastSeenOn : Instant
} }
@ -39,6 +46,7 @@ type ContinentId = ContinentId of Guid
type Continent = type Continent =
{ /// The ID of the continent { /// The ID of the continent
id : ContinentId id : ContinentId
/// The name of the continent /// The name of the continent
name : string name : string
} }
@ -56,26 +64,37 @@ type ListingId = ListingId of Guid
type Listing = type Listing =
{ /// The ID of the job listing { /// The ID of the job listing
id : ListingId id : ListingId
/// The ID of the citizen who posted the job listing /// The ID of the citizen who posted the job listing
citizenId : CitizenId citizenId : CitizenId
/// When this job listing was created /// When this job listing was created
createdOn : Instant createdOn : Instant
/// The short title of the job listing /// The short title of the job listing
title : string title : string
/// The ID of the continent on which the job is located /// The ID of the continent on which the job is located
continentId : ContinentId continentId : ContinentId
/// The region in which the job is located /// The region in which the job is located
region : string region : string
/// Whether this listing is for remote work /// Whether this listing is for remote work
remoteWork : bool remoteWork : bool
/// Whether this listing has expired /// Whether this listing has expired
isExpired : bool isExpired : bool
/// When this listing was last updated /// When this listing was last updated
updatedOn : Instant updatedOn : Instant
/// The details of this job /// The details of this job
text : MarkdownString text : MarkdownString
/// When this job needs to be filled /// When this job needs to be filled
neededBy : LocalDate option neededBy : LocalDate option
/// Was this job filled as part of its appearance on Jobs, Jobs, Jobs? /// Was this job filled as part of its appearance on Jobs, Jobs, Jobs?
wasFilledHere : bool option wasFilledHere : bool option
} }
@ -88,8 +107,10 @@ type SkillId = SkillId of Guid
type Skill = type Skill =
{ /// The ID of the skill { /// The ID of the skill
id : SkillId id : SkillId
/// A description of the skill /// A description of the skill
description : string description : string
/// Notes regarding this skill (level, duration, etc.) /// Notes regarding this skill (level, duration, etc.)
notes : string option notes : string option
} }
@ -100,24 +121,34 @@ type Skill =
type Profile = type Profile =
{ /// The ID of the citizen to whom this profile belongs { /// The ID of the citizen to whom this profile belongs
id : CitizenId id : CitizenId
/// Whether this citizen is actively seeking employment /// Whether this citizen is actively seeking employment
seekingEmployment : bool seekingEmployment : bool
/// Whether this citizen allows their profile to be a part of the publicly-viewable, anonymous data /// Whether this citizen allows their profile to be a part of the publicly-viewable, anonymous data
isPublic : bool isPublic : bool
/// The ID of the continent on which the citizen resides /// The ID of the continent on which the citizen resides
continentId : ContinentId continentId : ContinentId
/// The region in which the citizen resides /// The region in which the citizen resides
region : string region : string
/// Whether the citizen is looking for remote work /// Whether the citizen is looking for remote work
remoteWork : bool remoteWork : bool
/// Whether the citizen is looking for full-time work /// Whether the citizen is looking for full-time work
fullTime : bool fullTime : bool
/// The citizen's professional biography /// The citizen's professional biography
biography : MarkdownString biography : MarkdownString
/// When the citizen last updated their profile /// When the citizen last updated their profile
lastUpdatedOn : Instant lastUpdatedOn : Instant
/// The citizen's experience (topical / chronological) /// The citizen's experience (topical / chronological)
experience : MarkdownString option experience : MarkdownString option
/// Skills this citizen possesses /// Skills this citizen possesses
skills : Skill list skills : Skill list
} }
@ -130,14 +161,19 @@ type SuccessId = SuccessId of Guid
type Success = type Success =
{ /// The ID of the success report { /// The ID of the success report
id : SuccessId id : SuccessId
/// The ID of the citizen who wrote this success report /// The ID of the citizen who wrote this success report
citizenId : CitizenId citizenId : CitizenId
/// When this success report was recorded /// When this success report was recorded
recordedOn : Instant recordedOn : Instant
/// Whether the success was due, at least in part, to Jobs, Jobs, Jobs /// Whether the success was due, at least in part, to Jobs, Jobs, Jobs
fromHere : bool fromHere : bool
/// The source of this success (listing or profile) /// The source of this success (listing or profile)
source : string source : string
/// The success story /// The success story
story : MarkdownString option story : MarkdownString option
} }

View File

@ -31,8 +31,7 @@ module Converters =
type MarkdownStringJsonConverter() = type MarkdownStringJsonConverter() =
inherit JsonConverter<MarkdownString>() inherit JsonConverter<MarkdownString>()
override _.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) =
let (Text text) = value writer.WriteValue (MarkdownString.toString value)
writer.WriteValue text
override _.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) =
(string >> Text) reader.Value (string >> Text) reader.Value
@ -75,16 +74,22 @@ module Converters =
/// Table names /// Table names
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Table = module Table =
/// The user (citizen of Gitmo Nation) table /// The user (citizen of Gitmo Nation) table
let Citizen = "citizen" let Citizen = "citizen"
/// The continent table /// The continent table
let Continent = "continent" let Continent = "continent"
/// The job listing table /// The job listing table
let Listing = "listing" let Listing = "listing"
/// The citizen employment profile table /// The citizen employment profile table
let Profile = "profile" let Profile = "profile"
/// The success story table /// The success story table
let Success = "success" let Success = "success"
/// All tables /// All tables
let all () = [ Citizen; Continent; Listing; Profile; Success ] let all () = [ Citizen; Continent; Listing; Profile; Success ]
@ -166,7 +171,7 @@ module Startup =
let! userIdx = fromTable Table.Citizen |> indexList |> result<string list> conn let! userIdx = fromTable Table.Citizen |> indexList |> result<string list> conn
if not (List.contains "instanceUser" userIdx) then if not (List.contains "instanceUser" userIdx) then
do! fromTable Table.Citizen do! fromTable Table.Citizen
|> indexCreateFunc "instanceUser" (fun row -> r.Array (row.G "instance", row.G "mastodonUser")) |> indexCreateFunc "instanceUser" (fun row -> [| row.G "instance"; row.G "mastodonUser" |])
|> write conn |> write conn
} }
@ -175,7 +180,21 @@ open JobsJobsJobs.Domain
open JobsJobsJobs.Domain.SharedTypes open JobsJobsJobs.Domain.SharedTypes
/// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries /// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries
let regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)%s" let private regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)%s"
/// Apply filters to a query, ensuring that types all match up
let private applyFilters (filters : (ReqlExpr -> Filter) list) query : ReqlExpr =
if List.isEmpty filters then
query
else
let first = List.head filters query
List.fold (fun q (f : ReqlExpr -> Filter) -> f q) first (List.tail filters)
/// Derive a user's display name from real name, display name, or handle (in that order)
let private deriveDisplayName (it : ReqlExpr) =
r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "mastodonUser")
/// Profile data access functions /// Profile data access functions
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -209,75 +228,62 @@ module Profile =
/// Search profiles (logged-on users) /// Search profiles (logged-on users)
let search (search : ProfileSearch) conn = let search (search : ProfileSearch) conn =
(seq<ReqlExpr -> ReqlExpr> { fromTable Table.Profile
match search.continentId with |> eqJoin "id" (fromTable Table.Citizen)
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId))) |> without [ "right.id" ]
|> zip
|> applyFilters
[ match search.continentId with
| Some contId -> yield filter {| continentId = ContinentId.ofString contId |}
| None -> () | None -> ()
match search.remoteWork with match search.remoteWork with
| "" -> () | "" -> ()
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes"))) | _ -> yield filter {| remoteWork = search.remoteWork = "yes" |}
match search.skill with match search.skill with
| Some skl -> | Some skl ->
yield (fun q -> q.Filter (ReqlFunction1(fun it -> yield filterFunc (fun it ->
it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl)))))) it.G("skills").Contains (ReqlFunction1 (fun s -> s.G("description").Match (regexContains skl))))
| None -> () | None -> ()
match search.bioExperience with match search.bioExperience with
| Some text -> | Some text ->
let txt = regexContains text let txt = regexContains text
yield (fun q -> q.Filter (ReqlFunction1(fun it -> yield filterFunc (fun it -> it.G("biography").Match(txt).Or (it.G("experience").Match txt))
it.G("biography").Match(txt).Or (it.G("experience").Match txt))))
| None -> () | None -> ()
} ]
|> Seq.toList |> mergeFunc (fun it -> {| displayName = deriveDisplayName it; citizenId = it.G "id" |})
|> List.fold
(fun q f -> f q)
(r.Table(Table.Profile)
.EqJoin("id", r.Table Table.Citizen)
.Without(r.HashMap ("right", "id"))
.Zip () :> ReqlExpr))
|> mergeFunc (fun it ->
r.HashMap("displayName",
r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "mastodonUser"))
.With ("citizenId", it.G "id"))
|> pluck [ "citizenId"; "displayName"; "seekingEmployment"; "remoteWork"; "fullTime"; "lastUpdatedOn" ] |> pluck [ "citizenId"; "displayName"; "seekingEmployment"; "remoteWork"; "fullTime"; "lastUpdatedOn" ]
|> orderByFunc (fun it -> it.G("displayName").Downcase ()) |> orderByFunc (fun it -> it.G("displayName").Downcase ())
|> result<ProfileSearchResult list> conn |> result<ProfileSearchResult list> conn
// Search profiles (public) // Search profiles (public)
let publicSearch (search : PublicSearch) conn = let publicSearch (search : PublicSearch) conn =
(seq<ReqlExpr -> ReqlExpr> { fromTable Table.Profile
|> eqJoin "continentId" (fromTable Table.Continent)
|> without [ "right.id" ]
|> zip
|> applyFilters
[ yield filter {| isPublic = true |}
match search.continentId with match search.continentId with
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId))) | Some contId -> yield filter {| continentId = ContinentId.ofString contId |}
| None -> () | None -> ()
match search.region with match search.region with
| Some reg -> | Some reg -> yield filterFunc (fun it -> it.G("region").Match (regexContains reg))
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))))
| None -> () | None -> ()
match search.remoteWork with match search.remoteWork with
| "" -> () | "" -> ()
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes"))) | _ -> yield filter {| remoteWork = search.remoteWork = "yes" |}
match search.skill with match search.skill with
| Some skl -> | Some skl ->
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> yield filterFunc (fun it ->
it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl)))))) it.G("skills").Contains (ReqlFunction1 (fun s -> s.G("description").Match (regexContains skl))))
| None -> () | None -> ()
} ]
|> Seq.toList
|> List.fold
(fun q f -> f q)
(r.Table(Table.Profile)
.EqJoin("continentId", r.Table Table.Continent)
.Without(r.HashMap ("right", "id"))
.Zip()
.Filter(r.HashMap ("isPublic", true))))
|> mergeFunc (fun it -> |> mergeFunc (fun it ->
r.HashMap("skills", {| skills = it.G("skills").Map (ReqlFunction1 (fun skill ->
it.G("skills").Map (ReqlFunction1 (fun skill ->
r.Branch(skill.G("notes").Default_("").Eq "", skill.G "description", r.Branch(skill.G("notes").Default_("").Eq "", skill.G "description",
skill.G("description").Add(" (").Add(skill.G("notes")).Add ")")))) skill.G("description").Add(" (").Add(skill.G("notes")).Add ")")))
.With("continent", it.G "name")) continent = it.G "name"
|})
|> pluck [ "continent"; "region"; "skills"; "remoteWork" ] |> pluck [ "continent"; "region"; "skills"; "remoteWork" ]
|> result<PublicSearchResult list> conn |> result<PublicSearchResult list> conn
@ -295,7 +301,7 @@ module Citizen =
let findByMastodonUser (instance : string) (mastodonUser : string) conn = task { let findByMastodonUser (instance : string) (mastodonUser : string) conn = task {
let! u = let! u =
fromTable Table.Citizen fromTable Table.Citizen
|> getAllWithIndex [ r.Array (instance, mastodonUser) ] "instanceUser" |> getAllWithIndex [ [| instance; mastodonUser |] ] "instanceUser"
|> limit 1 |> limit 1
|> result<Citizen list> conn |> result<Citizen list> conn
return List.tryHead u return List.tryHead u
@ -311,8 +317,7 @@ module Citizen =
let logOnUpdate (citizen : Citizen) conn = let logOnUpdate (citizen : Citizen) conn =
fromTable Table.Citizen fromTable Table.Citizen
|> get citizen.id |> get citizen.id
|> update (r.HashMap( nameof citizen.displayName, citizen.displayName) |> update {| displayName = citizen.displayName; lastSeenOn = citizen.lastSeenOn |}
.With (nameof citizen.lastSeenOn, citizen.lastSeenOn))
|> write conn |> write conn
/// Delete a citizen /// Delete a citizen
@ -336,7 +341,7 @@ module Citizen =
let realNameUpdate (citizenId : CitizenId) (realName : string option) conn = let realNameUpdate (citizenId : CitizenId) (realName : string option) conn =
fromTable Table.Citizen fromTable Table.Citizen
|> get citizenId |> get citizenId
|> update (r.HashMap (nameof realName, realName)) |> update {| realName = realName |}
|> write conn |> write conn
@ -362,12 +367,15 @@ module Listing =
open NodaTime open NodaTime
/// Convert a joined query to the form needed for ListingForView deserialization
let private toListingForView (it : ReqlExpr) : obj = {| listing = it.G "left"; continent = it.G "right" |}
/// Find all job listings posted by the given citizen /// Find all job listings posted by the given citizen
let findByCitizen (citizenId : CitizenId) conn = let findByCitizen (citizenId : CitizenId) conn =
fromTable Table.Listing fromTable Table.Listing
|> getAllWithIndex [ citizenId ] (nameof citizenId) |> getAllWithIndex [ citizenId ] (nameof citizenId)
|> eqJoin "continentId" (fromTable Table.Continent) |> eqJoin "continentId" (fromTable Table.Continent)
|> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")) |> mapFunc toListingForView
|> result<ListingForView list> conn |> result<ListingForView list> conn
/// Find a listing by its ID /// Find a listing by its ID
@ -380,9 +388,9 @@ module Listing =
let findByIdForView (listingId : ListingId) conn = task { let findByIdForView (listingId : ListingId) conn = task {
let! listing = let! listing =
fromTable Table.Listing fromTable Table.Listing
|> filter (r.HashMap ("id", listingId)) |> filter {| id = listingId |}
|> eqJoin "continentId" (fromTable Table.Continent) |> eqJoin "continentId" (fromTable Table.Continent)
|> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")) |> mapFunc toListingForView
|> result<ListingForView list> conn |> result<ListingForView list> conn
return List.tryHead listing return List.tryHead listing
} }
@ -404,36 +412,29 @@ module Listing =
let expire (listingId : ListingId) (fromHere : bool) (now : Instant) conn = let expire (listingId : ListingId) (fromHere : bool) (now : Instant) conn =
(fromTable Table.Listing (fromTable Table.Listing
|> get listingId) |> get listingId)
.Update (r.HashMap("isExpired", true).With("wasFilledHere", fromHere).With ("updatedOn", now)) .Update {| isExpired = true; wasFilledHere = fromHere; updatedOn = now |}
|> write conn |> write conn
/// Search job listings /// Search job listings
let search (search : ListingSearch) conn = let search (search : ListingSearch) conn =
(seq<ReqlExpr -> ReqlExpr> { fromTable Table.Listing
match search.continentId with |> getAllWithIndex [ false ] "isExpired"
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId))) |> applyFilters
[ match search.continentId with
| Some contId -> yield filter {| continentId = ContinentId.ofString contId |}
| None -> () | None -> ()
match search.region with match search.region with
| Some rgn -> | Some rgn -> yield filterFunc (fun it -> it.G(nameof search.region).Match (regexContains rgn))
yield (fun q ->
q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.region).Match (regexContains rgn))))
| None -> () | None -> ()
match search.remoteWork with match search.remoteWork with
| "" -> () | "" -> ()
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes"))) | _ -> yield filter {| remoteWork = search.remoteWork = "yes" |}
match search.text with match search.text with
| Some text -> | Some text -> yield filterFunc (fun it -> it.G(nameof search.text).Match (regexContains text))
yield (fun q ->
q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.text).Match (regexContains text))))
| None -> () | None -> ()
} ]
|> Seq.toList
|> List.fold
(fun q f -> f q)
(fromTable Table.Listing
|> getAllWithIndex [ false ] "isExpired" :> ReqlExpr))
|> eqJoin "continentId" (fromTable Table.Continent) |> eqJoin "continentId" (fromTable Table.Continent)
|> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")) |> mapFunc toListingForView
|> result<ListingForView list> conn |> result<ListingForView list> conn
@ -456,16 +457,11 @@ module Success =
// Retrieve all success stories // Retrieve all success stories
let all conn = let all conn =
(fromTable Table.Success fromTable Table.Success
|> eqJoin "citizenId" (fromTable Table.Citizen)) |> eqJoin "citizenId" (fromTable Table.Citizen)
.Without(r.HashMap ("right", "id")) |> without [ "right.id" ]
|> zip |> zip
|> mergeFunc (fun it -> |> mergeFunc (fun it -> {| citizenName = deriveDisplayName it; hasStory = it.G("story").Default_("").Gt "" |})
r.HashMap("citizenName",
r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "mastodonUser"))
.With ("hasStory", it.G("story").Default_("").Gt ""))
|> pluck [ "id"; "citizenId"; "citizenName"; "recordedOn"; "fromHere"; "hasStory" ] |> pluck [ "id"; "citizenId"; "citizenName"; "recordedOn"; "fromHere"; "hasStory" ]
|> orderByDescending "recordedOn" |> orderByDescending "recordedOn"
|> result<StoryEntry list> conn |> result<StoryEntry list> conn