Update dates for help and legal pages

- Switch to containment queries for exact matches
- Bump version to 3.0.1
This commit is contained in:
Daniel J. Summers 2023-02-05 21:16:42 -05:00
parent e3f3baa13f
commit f8cdd393ff
9 changed files with 75 additions and 73 deletions

View File

@ -13,13 +13,14 @@ let private locker = obj ()
/// Delete a citizen by their ID using the given connection properties /// Delete a citizen by their ID using the given connection properties
let private doDeleteById citizenId connProps = backgroundTask { let private doDeleteById citizenId connProps = backgroundTask {
let citId = CitizenId.toString citizenId
let! _ = let! _ =
connProps connProps
|> Sql.query $" |> Sql.query $"
DELETE FROM {Table.Success} WHERE data ->> 'citizenId' = @id; DELETE FROM {Table.Success} WHERE data @> @criteria;
DELETE FROM {Table.Listing} WHERE data ->> 'citizenId' = @id; DELETE FROM {Table.Listing} WHERE data @> @criteria;
DELETE FROM {Table.Citizen} WHERE id = @id" DELETE FROM {Table.Citizen} WHERE id = @id"
|> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ] |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| citizenId = citId |}); "@id", Sql.string citId ]
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
() ()
} }
@ -89,15 +90,11 @@ let register citizen (security : SecurityInfo) = backgroundTask {
} }
/// Try to find the security information matching a confirmation token /// Try to find the security information matching a confirmation token
let private tryConfirmToken token connProps = backgroundTask { let private tryConfirmToken (token : string) connProps = backgroundTask {
let! tryInfo = let! tryInfo =
connProps connProps
|> Sql.query $" |> Sql.query $" SELECT * FROM {Table.SecurityInfo} WHERE data @> @criteria"
SELECT * |> Sql.parameters [ "criteria", Sql.jsonb (mkDoc {| token = token; tokenUsage = "confirm" |}) ]
FROM {Table.SecurityInfo}
WHERE data ->> 'token' = @token
AND data ->> 'tokenUsage' = 'confirm'"
|> Sql.parameters [ "@token", Sql.string token ]
|> Sql.executeAsync toDocument<SecurityInfo> |> Sql.executeAsync toDocument<SecurityInfo>
return List.tryHead tryInfo return List.tryHead tryInfo
} }
@ -132,12 +129,8 @@ let tryLogOn email password (pwVerify : Citizen -> string -> bool option) (pwHas
let connProps = dataSource () let connProps = dataSource ()
let! tryCitizen = let! tryCitizen =
connProps connProps
|> Sql.query $" |> Sql.query $"SELECT * FROM {Table.Citizen} WHERE data @> @criteria"
SELECT * |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| email = email; isLegacy = false |}) ]
FROM {Table.Citizen}
WHERE data ->> 'email' = @email
AND data ->> 'isLegacy' = 'false'"
|> Sql.parameters [ "@email", Sql.string email ]
|> Sql.executeAsync toDocument<Citizen> |> Sql.executeAsync toDocument<Citizen>
match List.tryHead tryCitizen with match List.tryHead tryCitizen with
| Some citizen -> | Some citizen ->
@ -176,8 +169,8 @@ let tryByEmailWithSecurity email = backgroundTask {
SELECT c.*, s.data AS sec_data SELECT c.*, s.data AS sec_data
FROM {Table.Citizen} c FROM {Table.Citizen} c
INNER JOIN {Table.SecurityInfo} s ON s.id = c.id INNER JOIN {Table.SecurityInfo} s ON s.id = c.id
WHERE c.data ->> 'email' = @email" WHERE c.data @> @criteria"
|> Sql.parameters [ "@email", Sql.string email ] |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| email = email |}) ]
|> Sql.executeAsync toCitizenSecurityPair |> Sql.executeAsync toCitizenSecurityPair
return List.tryHead results return List.tryHead results
} }
@ -188,12 +181,12 @@ let saveSecurityInfo security = backgroundTask {
} }
/// Try to retrieve security information by the given token /// Try to retrieve security information by the given token
let trySecurityByToken token = backgroundTask { let trySecurityByToken (token : string) = backgroundTask {
do! checkForPurge false do! checkForPurge false
let! results = let! results =
dataSource () dataSource ()
|> Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'token' = @token" |> Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data @> @criteria"
|> Sql.parameters [ "@token", Sql.string token ] |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| token = token |}) ]
|> Sql.executeAsync toDocument<SecurityInfo> |> Sql.executeAsync toDocument<SecurityInfo>
return List.tryHead results return List.tryHead results
} }
@ -204,7 +197,11 @@ let trySecurityByToken token = backgroundTask {
let legacy () = backgroundTask { let legacy () = backgroundTask {
return! return!
dataSource () dataSource ()
|> Sql.query $"SELECT * FROM {Table.Citizen} WHERE data ->> 'isLegacy' = 'true' ORDER BY data ->> 'firstName'" |> Sql.query $"""
SELECT *
FROM {Table.Citizen}
WHERE data @> '{{ "isLegacy": true }}'::jsonb
ORDER BY data ->> 'firstName'"""
|> Sql.executeAsync toDocument<Citizen> |> Sql.executeAsync toDocument<Citizen>
} }
@ -212,13 +209,13 @@ let legacy () = backgroundTask {
let current () = backgroundTask { let current () = backgroundTask {
return! return!
dataSource () dataSource ()
|> Sql.query $" |> Sql.query $"""
SELECT c.* SELECT c.*
FROM {Table.Citizen} c FROM {Table.Citizen} c
INNER JOIN {Table.SecurityInfo} si ON si.id = c.id INNER JOIN {Table.SecurityInfo} si ON si.id = c.id
WHERE c.data ->> 'isLegacy' = 'false' WHERE c.data @> '{{ "isLegacy": false }}'::jsonb
AND si.data ->> 'accountLocked' = 'false' AND si.data @> '{{ "accountLocked": false }}'::jsonb
AND NOT EXISTS (SELECT 1 FROM {Table.Profile} p WHERE p.id = c.id)" AND NOT EXISTS (SELECT 1 FROM {Table.Profile} p WHERE p.id = c.id)"""
|> Sql.executeAsync toDocument<Citizen> |> Sql.executeAsync toDocument<Citizen>
} }
@ -241,11 +238,12 @@ let migrateLegacy currentId legacyId = backgroundTask {
Table.Profile (CitizenId.toString currentId) (Sql.existingConnection conn) Table.Profile (CitizenId.toString currentId) (Sql.existingConnection conn)
(mkDoc { profile with Id = currentId; IsLegacy = false }) (mkDoc { profile with Id = currentId; IsLegacy = false })
| None -> () | None -> ()
let oldCriteria = mkDoc {| citizenId = oldId |}
let! listings = let! listings =
conn conn
|> Sql.existingConnection |> Sql.existingConnection
|> Sql.query $"SELECT * FROM {Table.Listing} WHERE data ->> 'citizenId' = @oldId" |> Sql.query $"SELECT * FROM {Table.Listing} WHERE data @> @criteria"
|> Sql.parameters [ "@oldId", Sql.string oldId ] |> Sql.parameters [ "@criteria", Sql.jsonb oldCriteria ]
|> Sql.executeAsync toDocument<Listing> |> Sql.executeAsync toDocument<Listing>
for listing in listings do for listing in listings do
let newListing = { listing with Id = ListingId.create (); CitizenId = currentId; IsLegacy = false } let newListing = { listing with Id = ListingId.create (); CitizenId = currentId; IsLegacy = false }
@ -254,8 +252,8 @@ let migrateLegacy currentId legacyId = backgroundTask {
let! successes = let! successes =
conn conn
|> Sql.existingConnection |> Sql.existingConnection
|> Sql.query $"SELECT * FROM {Table.Success} WHERE data ->> 'citizenId' = @oldId" |> Sql.query $"SELECT * FROM {Table.Success} WHERE data @> @criteria"
|> Sql.parameters [ "@oldId", Sql.string oldId ] |> Sql.parameters [ "@criteria", Sql.string oldCriteria ]
|> Sql.executeAsync toDocument<Success> |> Sql.executeAsync toDocument<Success>
for success in successes do for success in successes do
let newSuccess = { success with Id = SuccessId.create (); CitizenId = currentId } let newSuccess = { success with Id = SuccessId.create (); CitizenId = currentId }
@ -266,10 +264,10 @@ let migrateLegacy currentId legacyId = backgroundTask {
conn conn
|> Sql.existingConnection |> Sql.existingConnection
|> Sql.query $" |> Sql.query $"
DELETE FROM {Table.Success} WHERE data ->> 'citizenId' = @oldId; DELETE FROM {Table.Success} WHERE data @> @criteria;
DELETE FROM {Table.Listing} WHERE data ->> 'citizenId' = @oldId; DELETE FROM {Table.Listing} WHERE data @> @criteria;
DELETE FROM {Table.Citizen} WHERE id = @oldId" DELETE FROM {Table.Citizen} WHERE id = @oldId"
|> Sql.parameters [ "@oldId", Sql.string oldId ] |> Sql.parameters [ "@criteria", Sql.jsonb oldCriteria; "@oldId", Sql.string oldId ]
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
do! txn.CommitAsync () do! txn.CommitAsync ()
return Ok "" return Ok ""

View File

@ -189,7 +189,7 @@ let dashboard (citizen : Citizen) (profile : Profile option) profileCount tz =
emptyP emptyP
p [] [ p [] [
txt "To see how this application works, check out &ldquo;How It Works&rdquo; in the sidebar (last updated " txt "To see how this application works, check out &ldquo;How It Works&rdquo; in the sidebar (last updated "
txt "August 29<sup>th</sup>, 2021)." txt "February 2<sup>nd</sup>, 2023)."
] ]
] ]

View File

@ -87,7 +87,7 @@ type DistributedCache () =
expire_at TIMESTAMPTZ NOT NULL, expire_at TIMESTAMPTZ NOT NULL,
sliding_expiration INTERVAL, sliding_expiration INTERVAL,
absolute_expiration TIMESTAMPTZ); absolute_expiration TIMESTAMPTZ);
CREATE INDEX idx_session_expiration ON session (expire_at)" CREATE INDEX idx_session_expiration ON jjj.session (expire_at)"
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
() ()
} |> sync } |> sync

View File

@ -297,7 +297,7 @@ module Layout =
let version = let version =
seq { seq {
string v.Major string v.Major
if v.Minor > 0 then if v.Minor > 0 || v.Build > 0 then
"."; string v.Minor "."; string v.Minor
if v.Build > 0 then if v.Build > 0 then
"."; string v.Build "."; string v.Build

View File

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyVersion>3.0.0.0</AssemblyVersion> <AssemblyVersion>3.0.1.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion> <FileVersion>3.0.1.0</FileVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -28,7 +28,7 @@ let privacyPolicy =
let appName = txt "Jobs, Jobs, Jobs" let appName = txt "Jobs, Jobs, Jobs"
article [] [ article [] [
h3 [] [ txt "Privacy Policy" ] h3 [] [ txt "Privacy Policy" ]
p [ _class "fst-italic" ] [ txt "(as of December 27<sup>th</sup>, 2022)" ] p [ _class "fst-italic" ] [ txt "(as of February 2<sup>nd</sup>, 2023)" ]
p [] [ p [] [
appName; txt " (&ldquo;we,&rdquo; &ldquo;our,&rdquo; or &ldquo;us&rdquo;) is committed to protecting your " appName; txt " (&ldquo;we,&rdquo; &ldquo;our,&rdquo; or &ldquo;us&rdquo;) is committed to protecting your "
@ -477,7 +477,7 @@ let privacyPolicy =
hr [] hr []
p [ _class "fst-italic" ] [ txt "Changes for "; appName; txt " v3 (December 27<sup>th</sup>, 2022)" ] p [ _class "fst-italic" ] [ txt "Changes for "; appName; txt " v3 (February 2<sup>nd</sup>, 2023)" ]
ul [] [ ul [] [
li [ _class "fst-italic" ] [ txt "Removed references to Mastodon" ] li [ _class "fst-italic" ] [ txt "Removed references to Mastodon" ]
li [ _class "fst-italic" ] [ txt "Added references to job listings" ] li [ _class "fst-italic" ] [ txt "Added references to job listings" ]
@ -494,7 +494,7 @@ let privacyPolicy =
let termsOfService = let termsOfService =
article [] [ article [] [
h3 [] [ txt "Terms of Service" ] h3 [] [ txt "Terms of Service" ]
p [ _class "fst-italic" ] [ txt "(as of August 30<sup>th</sup>, 2022)" ] p [ _class "fst-italic" ] [ txt "(as of February 2<sup>nd</sup>, 2023)" ]
h4 [] [ txt "Acceptance of Terms" ] h4 [] [ txt "Acceptance of Terms" ]
p [] [ p [] [
txt "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you " txt "By accessing this web site, you are agreeing to be bound by these Terms and Conditions, and that you "
@ -528,7 +528,7 @@ let termsOfService =
] ]
hr [] hr []
p [ _class "fst-italic" ] [ p [ _class "fst-italic" ] [
txt "Change on August 30<sup>th</sup>, 2022 &ndash; added references to job listings, removed references " txt "Change on February 2<sup>nd</sup>, 2023 &ndash; added references to job listings, removed references "
txt "to Mastodon instances." txt "to Mastodon instances."
] ]
p [ _class "fst-italic" ] [ p [ _class "fst-italic" ] [
@ -945,7 +945,7 @@ module Help =
let index = let index =
article [] [ article [] [
h3 [ _class "mb-0" ] [ txt "How It Works" ] h3 [ _class "mb-0" ] [ txt "How It Works" ]
h6 [ _class "mb-3 text-muted fst-italic" ] [ txt "Last Updated January 22<sup>nd</sup>, 2023" ] h6 [ _class "mb-3 text-muted fst-italic" ] [ txt "Last Updated February 2<sup>nd</sup>, 2023" ]
p [ _class "fst-italic" ] [ p [ _class "fst-italic" ] [
txt "Show me how to "; a [ _href "/how-it-works/listings#searching" ] [ txt "find a job" ] txt "Show me how to "; a [ _href "/how-it-works/listings#searching" ] [ txt "find a job" ]

View File

@ -22,8 +22,9 @@ let private toListingForView row =
/// Find all job listings posted by the given citizen /// Find all job listings posted by the given citizen
let findByCitizen citizenId = let findByCitizen citizenId =
dataSource () dataSource ()
|> Sql.query $"{viewSql} WHERE l.data ->> 'citizenId' = @citizenId AND l.data ->> 'isLegacy' = 'false'" |> Sql.query $"{viewSql} WHERE l.data @> @criteria"
|> Sql.parameters [ "@citizenId", Sql.string (CitizenId.toString citizenId) ] |> Sql.parameters
[ "@criteria", Sql.jsonb (mkDoc {| citizenId = CitizenId.toString citizenId; isLegacy = false |}) ]
|> Sql.executeAsync toListingForView |> Sql.executeAsync toListingForView
/// Find a listing by its ID /// Find a listing by its ID
@ -38,7 +39,7 @@ let findById listingId = backgroundTask {
let findByIdForView listingId = backgroundTask { let findByIdForView listingId = backgroundTask {
let! tryListing = let! tryListing =
dataSource () dataSource ()
|> Sql.query $"{viewSql} WHERE l.id = @id AND l.data ->> 'isLegacy' = 'false'" |> Sql.query $"""{viewSql} WHERE l.id = @id AND l.data @> '{{ "isLegacy": false }}'::jsonb"""
|> Sql.parameters [ "@id", Sql.string (ListingId.toString listingId) ] |> Sql.parameters [ "@id", Sql.string (ListingId.toString listingId) ]
|> Sql.executeAsync toListingForView |> Sql.executeAsync toListingForView
return List.tryHead tryListing return List.tryHead tryListing
@ -52,18 +53,18 @@ let save (listing : Listing) =
let search (search : ListingSearchForm) = let search (search : ListingSearchForm) =
let searches = [ let searches = [
if search.ContinentId <> "" then if search.ContinentId <> "" then
"l.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string search.ContinentId ] "l.data @> @continent", [ "@continent", Sql.jsonb (mkDoc {| continentId = search.ContinentId |}) ]
if search.Region <> "" then if search.Region <> "" then
"l.data ->> 'region' ILIKE @region", [ "@region", like search.Region ] "l.data ->> 'region' ILIKE @region", [ "@region", like search.Region ]
if search.RemoteWork <> "" then if search.RemoteWork <> "" then
"l.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ] "l.data @> @remote", [ "@remote", Sql.jsonb (mkDoc {| isRemote = search.RemoteWork = "yes" |}) ]
if search.Text <> "" then if search.Text <> "" then
"l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ] "l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ]
] ]
dataSource () dataSource ()
|> Sql.query $" |> Sql.query $"""
{viewSql} {viewSql}
WHERE l.data ->> 'isExpired' = 'false' AND l.data ->> 'isLegacy' = 'false' WHERE l.data @> '{{ "isExpired": false, "isLegacy": false }}'::jsonb
{searchSql searches}" {searchSql searches}"""
|> Sql.parameters (searches |> List.collect snd) |> Sql.parameters (searches |> List.collect snd)
|> Sql.executeAsync toListingForView |> Sql.executeAsync toListingForView

View File

@ -8,7 +8,8 @@ open Npgsql.FSharp
/// Count the current profiles /// Count the current profiles
let count () = let count () =
dataSource () dataSource ()
|> Sql.query $"SELECT COUNT(id) AS the_count FROM {Table.Profile} WHERE data ->> 'isLegacy' = 'false'" |> Sql.query
$"""SELECT COUNT(id) AS the_count FROM {Table.Profile} WHERE data @> '{{ "isLegacy": false }}'::jsonb"""
|> Sql.executeRowAsync (fun row -> row.int64 "the_count") |> Sql.executeRowAsync (fun row -> row.int64 "the_count")
/// Delete a profile by its ID /// Delete a profile by its ID
@ -40,13 +41,13 @@ let private toProfileForView row =
let findByIdForView citizenId = backgroundTask { let findByIdForView citizenId = backgroundTask {
let! tryCitizen = let! tryCitizen =
dataSource () dataSource ()
|> Sql.query $" |> Sql.query $"""
SELECT p.*, c.data AS cit_data, o.data AS cont_data SELECT p.*, c.data AS cit_data, o.data AS cont_data
FROM {Table.Profile} p FROM {Table.Profile} p
INNER JOIN {Table.Citizen} c ON c.id = p.id INNER JOIN {Table.Citizen} c ON c.id = p.id
INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId' INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId'
WHERE p.id = @id WHERE p.id = @id
AND p.data ->> 'isLegacy' = 'false'" AND p.data @> '{{ "isLegacy": false }}'::jsonb"""
|> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ] |> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ]
|> Sql.executeAsync toProfileForView |> Sql.executeAsync toProfileForView
return List.tryHead tryCitizen return List.tryHead tryCitizen
@ -60,26 +61,28 @@ let save (profile : Profile) =
let search (search : ProfileSearchForm) isPublic = backgroundTask { let search (search : ProfileSearchForm) isPublic = backgroundTask {
let searches = [ let searches = [
if search.ContinentId <> "" then if search.ContinentId <> "" then
"p.data ->> 'continentId' = @continentId", [ "@continentId", Sql.string search.ContinentId ] "p.data @> @continent", [ "@continent", Sql.jsonb (mkDoc {| continentId = search.ContinentId |}) ]
if search.RemoteWork <> "" then if search.RemoteWork <> "" then
"p.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ] "p.data @> @remote", [ "@remote", Sql.jsonb (mkDoc {| isRemote = search.RemoteWork = "yes" |}) ]
if search.Text <> "" then if search.Text <> "" then
"p.text_search @@ plainto_tsquery(@text_search)", [ "@text_search", Sql.string search.Text ] "p.text_search @@ plainto_tsquery(@text_search)", [ "@text_search", Sql.string search.Text ]
] ]
let vizSql = let vizSql =
if isPublic then if isPublic then
sprintf "IN ('%s', '%s')" (ProfileVisibility.toString Public) (ProfileVisibility.toString Anonymous) sprintf "(p.data @> '%s'::jsonb OR p.data @> '%s'::jsonb)"
else sprintf "<> '%s'" (ProfileVisibility.toString Hidden) (mkDoc {| visibility = ProfileVisibility.toString Public |})
(mkDoc {| visibility = ProfileVisibility.toString Anonymous |})
else sprintf "p.data ->> 'visibility' <> '%s'" (ProfileVisibility.toString Hidden)
let! results = let! results =
dataSource () dataSource ()
|> Sql.query $" |> Sql.query $"""
SELECT p.*, c.data AS cit_data, o.data AS cont_data SELECT p.*, c.data AS cit_data, o.data AS cont_data
FROM {Table.Profile} p FROM {Table.Profile} p
INNER JOIN {Table.Citizen} c ON c.id = p.id INNER JOIN {Table.Citizen} c ON c.id = p.id
INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId' INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId'
WHERE p.data ->> 'isLegacy' = 'false' WHERE p.data @> '{{ "isLegacy": false }}'::jsonb
AND p.data ->> 'visibility' {vizSql} AND {vizSql}
{searchSql searches}" {searchSql searches}"""
|> Sql.parameters (searches |> List.collect snd) |> Sql.parameters (searches |> List.collect snd)
|> Sql.executeAsync toProfileForView |> Sql.executeAsync toProfileForView
return results |> List.sortBy (fun pfv -> (Citizen.name pfv.Citizen).ToLowerInvariant ()) return results |> List.sortBy (fun pfv -> (Citizen.name pfv.Citizen).ToLowerInvariant ())

View File

@ -30,4 +30,4 @@ let findById successId =
/// Save a success story /// Save a success story
let save (success : Success) = let save (success : Success) =
dataSource () |> saveDocument Table.Success (SuccessId.toString success.Id) <| mkDoc success (dataSource (), mkDoc success) ||> saveDocument Table.Success (SuccessId.toString success.Id)