Version 3 #40
|
@ -6,12 +6,13 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="SupportTypes.fs" />
|
||||||
<Compile Include="Types.fs" />
|
<Compile Include="Types.fs" />
|
||||||
<Compile Include="Modules.fs" />
|
|
||||||
<Compile Include="SharedTypes.fs" />
|
<Compile Include="SharedTypes.fs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Giraffe" Version="6.0.0" />
|
||||||
<PackageReference Include="Markdig" Version="0.30.2" />
|
<PackageReference Include="Markdig" Version="0.30.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
||||||
<PackageReference Include="NodaTime" Version="3.1.0" />
|
<PackageReference Include="NodaTime" Version="3.1.0" />
|
||||||
|
|
|
@ -1,19 +1,10 @@
|
||||||
/// Modules to provide support functions for types
|
namespace JobsJobsJobs.Domain
|
||||||
[<AutoOpen>]
|
|
||||||
module JobsJobsJobs.Domain.Modules
|
|
||||||
|
|
||||||
open Markdig
|
|
||||||
open System
|
open System
|
||||||
open Types
|
open Giraffe
|
||||||
|
|
||||||
/// Format a GUID as a Short GUID
|
|
||||||
let private toShortGuid (guid : Guid) =
|
|
||||||
Convert.ToBase64String(guid.ToByteArray ()).Replace('/', '_').Replace('+', '-')[0..21]
|
|
||||||
|
|
||||||
/// Turn a Short GUID back into a GUID
|
|
||||||
let private fromShortGuid (it : string) =
|
|
||||||
(Convert.FromBase64String >> Guid) $"{it.Replace('_', '/').Replace('-', '+')}=="
|
|
||||||
|
|
||||||
|
/// The ID of a user (a citizen of Gitmo Nation)
|
||||||
|
type CitizenId = CitizenId of Guid
|
||||||
|
|
||||||
/// Support functions for citizen IDs
|
/// Support functions for citizen IDs
|
||||||
module CitizenId =
|
module CitizenId =
|
||||||
|
@ -22,24 +13,17 @@ module CitizenId =
|
||||||
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 -> ShortGuid.fromGuid it
|
||||||
|
|
||||||
/// Parse a string into a citizen ID
|
/// Parse a string into a citizen ID
|
||||||
let ofString = fromShortGuid >> CitizenId
|
let ofString = ShortGuid.toGuid >> CitizenId
|
||||||
|
|
||||||
/// Get the GUID value of a citizen ID
|
/// Get the GUID value of a citizen ID
|
||||||
let value = function CitizenId guid -> guid
|
let value = function CitizenId guid -> guid
|
||||||
|
|
||||||
|
|
||||||
/// Support functions for citizens
|
/// The ID of a continent
|
||||||
module Citizen =
|
type ContinentId = ContinentId of Guid
|
||||||
|
|
||||||
/// Get the name of the citizen (the first of real name, display name, or handle that is filled in)
|
|
||||||
let name x =
|
|
||||||
[ x.realName; x.displayName; Some x.mastodonUser ]
|
|
||||||
|> List.find Option.isSome
|
|
||||||
|> Option.get
|
|
||||||
|
|
||||||
|
|
||||||
/// Support functions for continent IDs
|
/// Support functions for continent IDs
|
||||||
module ContinentId =
|
module ContinentId =
|
||||||
|
@ -48,15 +32,18 @@ module ContinentId =
|
||||||
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 -> ShortGuid.fromGuid it
|
||||||
|
|
||||||
/// Parse a string into a continent ID
|
/// Parse a string into a continent ID
|
||||||
let ofString = fromShortGuid >> ContinentId
|
let ofString = ShortGuid.toGuid >> ContinentId
|
||||||
|
|
||||||
/// Get the GUID value of a continent ID
|
/// Get the GUID value of a continent ID
|
||||||
let value = function ContinentId guid -> guid
|
let value = function ContinentId guid -> guid
|
||||||
|
|
||||||
|
|
||||||
|
/// The ID of a job listing
|
||||||
|
type ListingId = ListingId of Guid
|
||||||
|
|
||||||
/// Support functions for listing IDs
|
/// Support functions for listing IDs
|
||||||
module ListingId =
|
module ListingId =
|
||||||
|
|
||||||
|
@ -64,18 +51,23 @@ module ListingId =
|
||||||
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 -> ShortGuid.fromGuid it
|
||||||
|
|
||||||
/// Parse a string into a listing ID
|
/// Parse a string into a listing ID
|
||||||
let ofString = fromShortGuid >> ListingId
|
let ofString = ShortGuid.toGuid >> ListingId
|
||||||
|
|
||||||
/// Get the GUID value of a listing ID
|
/// Get the GUID value of a listing ID
|
||||||
let value = function ListingId guid -> guid
|
let value = function ListingId guid -> guid
|
||||||
|
|
||||||
|
|
||||||
|
/// A string of Markdown text
|
||||||
|
type MarkdownString = Text of string
|
||||||
|
|
||||||
/// Support functions for Markdown strings
|
/// Support functions for Markdown strings
|
||||||
module MarkdownString =
|
module MarkdownString =
|
||||||
|
|
||||||
|
open Markdig
|
||||||
|
|
||||||
/// 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 ()
|
||||||
|
|
||||||
|
@ -86,26 +78,19 @@ module MarkdownString =
|
||||||
let toString = function Text text -> text
|
let toString = function Text text -> text
|
||||||
|
|
||||||
|
|
||||||
/// Support functions for Profiles
|
/// Another way to contact a citizen from this site
|
||||||
module Profile =
|
type OtherContact =
|
||||||
|
{ /// The name of the contact (Email, No Agenda Social, LinkedIn, etc.)
|
||||||
// An empty profile
|
Name : string
|
||||||
let empty =
|
|
||||||
{ id = CitizenId Guid.Empty
|
/// The value for the contact (e-mail address, user name, URL, etc.)
|
||||||
seekingEmployment = false
|
Value : string
|
||||||
isPublic = false
|
}
|
||||||
isPublicLinkable = false
|
|
||||||
continentId = ContinentId Guid.Empty
|
|
||||||
region = ""
|
|
||||||
remoteWork = false
|
|
||||||
fullTime = false
|
|
||||||
biography = Text ""
|
|
||||||
lastUpdatedOn = NodaTime.Instant.MinValue
|
|
||||||
experience = None
|
|
||||||
skills = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
/// The ID of a skill
|
||||||
|
type SkillId = SkillId of Guid
|
||||||
|
|
||||||
/// Support functions for skill IDs
|
/// Support functions for skill IDs
|
||||||
module SkillId =
|
module SkillId =
|
||||||
|
|
||||||
|
@ -113,15 +98,18 @@ module SkillId =
|
||||||
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 -> ShortGuid.fromGuid it
|
||||||
|
|
||||||
/// Parse a string into a skill ID
|
/// Parse a string into a skill ID
|
||||||
let ofString = fromShortGuid >> SkillId
|
let ofString = ShortGuid.toGuid >> SkillId
|
||||||
|
|
||||||
/// Get the GUID value of a skill ID
|
/// Get the GUID value of a skill ID
|
||||||
let value = function SkillId guid -> guid
|
let value = function SkillId guid -> guid
|
||||||
|
|
||||||
|
|
||||||
|
/// The ID of a success report
|
||||||
|
type SuccessId = SuccessId of Guid
|
||||||
|
|
||||||
/// Support functions for success report IDs
|
/// Support functions for success report IDs
|
||||||
module SuccessId =
|
module SuccessId =
|
||||||
|
|
||||||
|
@ -129,10 +117,10 @@ module SuccessId =
|
||||||
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 -> ShortGuid.fromGuid it
|
||||||
|
|
||||||
/// Parse a string into a success report ID
|
/// Parse a string into a success report ID
|
||||||
let ofString = fromShortGuid >> SuccessId
|
let ofString = ShortGuid.toGuid >> SuccessId
|
||||||
|
|
||||||
/// Get the GUID value of a success ID
|
/// Get the GUID value of a success ID
|
||||||
let value = function SuccessId guid -> guid
|
let value = function SuccessId guid -> guid
|
|
@ -1,45 +1,49 @@
|
||||||
/// Types within Jobs, Jobs, Jobs
|
namespace JobsJobsJobs.Domain
|
||||||
module JobsJobsJobs.Domain.Types
|
|
||||||
|
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open System
|
open System
|
||||||
|
|
||||||
// fsharplint:disable FieldNames
|
// fsharplint:disable FieldNames
|
||||||
|
|
||||||
/// The ID of a user (a citizen of Gitmo Nation)
|
/// A user of Jobs, Jobs, Jobs; a citizen of Gitmo Nation
|
||||||
type CitizenId = CitizenId of Guid
|
|
||||||
|
|
||||||
/// A user of Jobs, Jobs, Jobs
|
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
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
|
|
||||||
instance : string
|
|
||||||
|
|
||||||
/// The handle by which the user is known on Mastodon
|
|
||||||
mastodonUser : string
|
|
||||||
|
|
||||||
/// The user's display name from Mastodon (updated every login)
|
|
||||||
displayName : string option
|
|
||||||
|
|
||||||
/// The user's real name
|
|
||||||
realName : string option
|
|
||||||
|
|
||||||
/// The URL for the user's Mastodon profile
|
|
||||||
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
|
||||||
|
|
||||||
|
/// The user's e-mail address
|
||||||
|
email : string
|
||||||
|
|
||||||
|
/// The user's first name
|
||||||
|
firstName : string
|
||||||
|
|
||||||
|
/// The user's last name
|
||||||
|
lastName : string
|
||||||
|
|
||||||
|
/// The hash of the user's password
|
||||||
|
passwordHash : string
|
||||||
|
|
||||||
|
/// The name displayed for this user throughout the site
|
||||||
|
displayName : string option
|
||||||
|
|
||||||
|
/// The other contacts for this user
|
||||||
|
otherContacts : OtherContact list
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Support functions for citizens
|
||||||
|
module Citizen =
|
||||||
|
|
||||||
|
/// Get the name of the citizen (either their preferred display name or first/last names)
|
||||||
|
let name x =
|
||||||
|
match x.displayName with Some it -> it | None -> $"{x.firstName} {x.lastName}"
|
||||||
|
|
||||||
/// The ID of a continent
|
|
||||||
type ContinentId = ContinentId of Guid
|
|
||||||
|
|
||||||
/// A continent
|
/// A continent
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
@ -52,13 +56,6 @@ type Continent =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A string of Markdown text
|
|
||||||
type MarkdownString = Text of string
|
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a job listing
|
|
||||||
type ListingId = ListingId of Guid
|
|
||||||
|
|
||||||
/// A job listing
|
/// A job listing
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
type Listing =
|
type Listing =
|
||||||
|
@ -100,9 +97,6 @@ type Listing =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// The ID of a skill
|
|
||||||
type SkillId = SkillId of Guid
|
|
||||||
|
|
||||||
/// A skill the job seeker possesses
|
/// A skill the job seeker possesses
|
||||||
type Skill =
|
type Skill =
|
||||||
{ /// The ID of the skill
|
{ /// The ID of the skill
|
||||||
|
@ -156,8 +150,25 @@ type Profile =
|
||||||
skills : Skill list
|
skills : Skill list
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The ID of a success report
|
/// Support functions for Profiles
|
||||||
type SuccessId = SuccessId of Guid
|
module Profile =
|
||||||
|
|
||||||
|
// An empty profile
|
||||||
|
let empty =
|
||||||
|
{ id = CitizenId Guid.Empty
|
||||||
|
seekingEmployment = false
|
||||||
|
isPublic = false
|
||||||
|
isPublicLinkable = false
|
||||||
|
continentId = ContinentId Guid.Empty
|
||||||
|
region = ""
|
||||||
|
remoteWork = false
|
||||||
|
fullTime = false
|
||||||
|
biography = Text ""
|
||||||
|
lastUpdatedOn = Instant.MinValue
|
||||||
|
experience = None
|
||||||
|
skills = []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A record of success finding employment
|
/// A record of success finding employment
|
||||||
[<CLIMutable; NoComparison; NoEquality>]
|
[<CLIMutable; NoComparison; NoEquality>]
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
/// Data access functions for Jobs, Jobs, Jobs
|
/// Data access functions for Jobs, Jobs, Jobs
|
||||||
module JobsJobsJobs.Api.Data
|
module JobsJobsJobs.Api.Data
|
||||||
|
|
||||||
open JobsJobsJobs.Domain.Types
|
open JobsJobsJobs.Domain
|
||||||
|
|
||||||
/// JSON converters used with RethinkDB persistence
|
/// JSON converters used with RethinkDB persistence
|
||||||
module Converters =
|
module Converters =
|
||||||
|
|
||||||
open JobsJobsJobs.Domain
|
|
||||||
open Microsoft.FSharpLu.Json
|
open Microsoft.FSharpLu.Json
|
||||||
open Newtonsoft.Json
|
open Newtonsoft.Json
|
||||||
open System
|
open System
|
||||||
|
@ -154,12 +153,16 @@ module Startup =
|
||||||
name TEXT NOT NULL)"
|
name TEXT NOT NULL)"
|
||||||
if needsTable "citizen" then
|
if needsTable "citizen" then
|
||||||
"CREATE TABLE jjj.citizen (
|
"CREATE TABLE jjj.citizen (
|
||||||
id UUID NOT NULL PRIMARY KEY,
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
display_name TEXT,
|
joined_on TIMESTAMPTZ NOT NULL,
|
||||||
profile_urls TEXT[] NOT NULL DEFAULT '{}',
|
last_seen_on TIMESTAMPTZ NOT NULL,
|
||||||
joined_on TIMESTAMPTZ NOT NULL,
|
email TEXT NOT NULL UNIQUE,
|
||||||
last_seen_on TIMESTAMPTZ NOT NULL,
|
first_name TEXT NOT NULL,
|
||||||
is_legacy BOOLEAN NOT NULL)"
|
last_name TEXT NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
is_legacy BOOLEAN NOT NULL,
|
||||||
|
display_name TEXT,
|
||||||
|
other_contacts TEXT)"
|
||||||
if needsTable "profile" then
|
if needsTable "profile" then
|
||||||
"CREATE TABLE jjj.profile (
|
"CREATE TABLE jjj.profile (
|
||||||
citizen_id UUID NOT NULL PRIMARY KEY,
|
citizen_id UUID NOT NULL PRIMARY KEY,
|
||||||
|
@ -228,7 +231,6 @@ module Startup =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -281,6 +283,20 @@ module Sql =
|
||||||
/// Map data results to domain types
|
/// Map data results to domain types
|
||||||
module Map =
|
module Map =
|
||||||
|
|
||||||
|
/// Create a citizen from a data row
|
||||||
|
let toCitizen (row : RowReader) : Citizen =
|
||||||
|
{ id = (row.uuid >> CitizenId) "id"
|
||||||
|
joinedOn = row.fieldValue<Instant> "joined_on"
|
||||||
|
lastSeenOn = row.fieldValue<Instant> "last_seen_on"
|
||||||
|
email = row.string "email"
|
||||||
|
firstName = row.string "first_name"
|
||||||
|
lastName = row.string "last_name"
|
||||||
|
passwordHash = row.string "password_hash"
|
||||||
|
displayName = row.stringOrNone "display_name"
|
||||||
|
// TODO: deserialize from JSON
|
||||||
|
otherContacts = [] // row.stringOrNone "other_contacts"
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a continent from a data row
|
/// Create a continent from a data row
|
||||||
let toContinent (row : RowReader) : Continent =
|
let toContinent (row : RowReader) : Continent =
|
||||||
{ id = (row.uuid >> ContinentId) "continent_id"
|
{ id = (row.uuid >> ContinentId) "continent_id"
|
||||||
|
@ -517,33 +533,67 @@ module Profile =
|
||||||
module Citizen =
|
module Citizen =
|
||||||
|
|
||||||
/// Find a citizen by their ID
|
/// Find a citizen by their ID
|
||||||
let findById (citizenId : CitizenId) conn =
|
let findById citizenId conn = backgroundTask {
|
||||||
fromTable Table.Citizen
|
let! citizen =
|
||||||
|> get citizenId
|
Sql.existingConnection conn
|
||||||
|> resultOption<Citizen> conn
|
|> Sql.query "SELECT * FROM jjj.citizen WHERE id = @id AND is_legacy = FALSE"
|
||||||
|
|> Sql.parameters [ "@id", Sql.citizenId citizenId ]
|
||||||
|
|> Sql.executeAsync Map.toCitizen
|
||||||
|
return List.tryHead citizen
|
||||||
|
}
|
||||||
|
|
||||||
/// Find a citizen by their Mastodon username
|
/// Find a citizen by their e-mail address
|
||||||
let findByMastodonUser (instance : string) (mastodonUser : string) conn = task {
|
let findByEmail email conn = backgroundTask {
|
||||||
let! u =
|
let! citizen =
|
||||||
fromTable Table.Citizen
|
Sql.existingConnection conn
|
||||||
|> getAllWithIndex [ [| instance; mastodonUser |] ] "instanceUser"
|
|> Sql.query "SELECT * FROM jjj.citizen WHERE email = @email AND is_legacy = FALSE"
|
||||||
|> limit 1
|
|> Sql.parameters [ "@email", Sql.string email ]
|
||||||
|> result<Citizen list> conn
|
|> Sql.executeAsync Map.toCitizen
|
||||||
return List.tryHead u
|
return List.tryHead citizen
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a citizen
|
/// Add or update a citizen
|
||||||
let add (citizen : Citizen) conn =
|
let save (citizen : Citizen) conn = backgroundTask {
|
||||||
fromTable Table.Citizen
|
let! _ =
|
||||||
|> insert citizen
|
Sql.existingConnection conn
|
||||||
|> write conn
|
|> Sql.query
|
||||||
|
"INSERT INTO jjj.citizen (
|
||||||
|
id, joined_on, last_seen_on, email, first_name, last_name, password_hash, display_name,
|
||||||
|
other_contacts, is_legacy
|
||||||
|
) VALUES (
|
||||||
|
@id, @joinedOn, @lastSeenOn, @email, @firstName, @lastName, @passwordHash, @displayName,
|
||||||
|
@otherContacts, FALSE
|
||||||
|
) ON CONFLICT (id) DO UPDATE
|
||||||
|
SET email = EXCLUDED.email,
|
||||||
|
first_name = EXCLUDED.first_name,
|
||||||
|
last_name = EXCLUDED.last_name,
|
||||||
|
password_hash = EXCLUDED.password_hash,
|
||||||
|
display_name = EXCLUDED.display_name,
|
||||||
|
other_contacts = EXCLUDED.other_contacts"
|
||||||
|
|> Sql.parameters
|
||||||
|
[ "@id", Sql.citizenId citizen.id
|
||||||
|
"@joinedOn" |>Sql.param<| citizen.joinedOn
|
||||||
|
"@lastSeenOn" |>Sql.param<| citizen.lastSeenOn
|
||||||
|
"@email", Sql.string citizen.email
|
||||||
|
"@firstName", Sql.string citizen.firstName
|
||||||
|
"@lastName", Sql.string citizen.lastName
|
||||||
|
"@passwordHash", Sql.string citizen.passwordHash
|
||||||
|
"@displayName", Sql.stringOrNone citizen.displayName
|
||||||
|
"@otherContacts", Sql.stringOrNone (if List.isEmpty citizen.otherContacts then None else Some "")
|
||||||
|
]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the display name and last seen on date for a citizen
|
/// Update the last seen on date for a citizen
|
||||||
let logOnUpdate (citizen : Citizen) conn =
|
let logOnUpdate (citizen : Citizen) conn = backgroundTask {
|
||||||
fromTable Table.Citizen
|
let! _ =
|
||||||
|> get citizen.id
|
Sql.existingConnection conn
|
||||||
|> update {| displayName = citizen.displayName; lastSeenOn = citizen.lastSeenOn |}
|
|> Sql.query "UPDATE jjj.citizen SET last_seen_on = @lastSeenOn WHERE id = @id"
|
||||||
|> write conn
|
|> Sql.parameters [ "@id", Sql.citizenId citizen.id; "@lastSeenOn" |>Sql.param<| citizen.lastSeenOn ]
|
||||||
|
|> Sql.executeNonQueryAsync
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a citizen
|
/// Delete a citizen
|
||||||
let delete citizenId conn = backgroundTask {
|
let delete citizenId conn = backgroundTask {
|
||||||
|
@ -555,13 +605,6 @@ module Citizen =
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a citizen's real name
|
|
||||||
let realNameUpdate (citizenId : CitizenId) (realName : string option) conn =
|
|
||||||
fromTable Table.Citizen
|
|
||||||
|> get citizenId
|
|
||||||
|> update {| realName = realName |}
|
|
||||||
|> write conn
|
|
||||||
|
|
||||||
|
|
||||||
/// Continent data access functions
|
/// Continent data access functions
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user