diff --git a/src/JobsJobsJobs/Citizens/Data.fs b/src/JobsJobsJobs/Citizens/Data.fs index 74aa8b2..9b685c8 100644 --- a/src/JobsJobsJobs/Citizens/Data.fs +++ b/src/JobsJobsJobs/Citizens/Data.fs @@ -13,13 +13,14 @@ let private locker = obj () /// Delete a citizen by their ID using the given connection properties let private doDeleteById citizenId connProps = backgroundTask { + let citId = CitizenId.toString citizenId let! _ = connProps |> Sql.query $" - DELETE FROM {Table.Success} WHERE data ->> 'citizenId' = @id; - DELETE FROM {Table.Listing} WHERE data ->> 'citizenId' = @id; - DELETE FROM {Table.Citizen} WHERE id = @id" - |> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ] + DELETE FROM {Table.Success} WHERE data @> @criteria; + DELETE FROM {Table.Listing} WHERE data @> @criteria; + DELETE FROM {Table.Citizen} WHERE id = @id" + |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| citizenId = citId |}); "@id", Sql.string citId ] |> Sql.executeNonQueryAsync () } @@ -89,15 +90,11 @@ let register citizen (security : SecurityInfo) = backgroundTask { } /// 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 = connProps - |> Sql.query $" - SELECT * - FROM {Table.SecurityInfo} - WHERE data ->> 'token' = @token - AND data ->> 'tokenUsage' = 'confirm'" - |> Sql.parameters [ "@token", Sql.string token ] + |> Sql.query $" SELECT * FROM {Table.SecurityInfo} WHERE data @> @criteria" + |> Sql.parameters [ "criteria", Sql.jsonb (mkDoc {| token = token; tokenUsage = "confirm" |}) ] |> Sql.executeAsync toDocument return List.tryHead tryInfo } @@ -132,12 +129,8 @@ let tryLogOn email password (pwVerify : Citizen -> string -> bool option) (pwHas let connProps = dataSource () let! tryCitizen = connProps - |> Sql.query $" - SELECT * - FROM {Table.Citizen} - WHERE data ->> 'email' = @email - AND data ->> 'isLegacy' = 'false'" - |> Sql.parameters [ "@email", Sql.string email ] + |> Sql.query $"SELECT * FROM {Table.Citizen} WHERE data @> @criteria" + |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| email = email; isLegacy = false |}) ] |> Sql.executeAsync toDocument match List.tryHead tryCitizen with | Some citizen -> @@ -176,8 +169,8 @@ let tryByEmailWithSecurity email = backgroundTask { SELECT c.*, s.data AS sec_data FROM {Table.Citizen} c INNER JOIN {Table.SecurityInfo} s ON s.id = c.id - WHERE c.data ->> 'email' = @email" - |> Sql.parameters [ "@email", Sql.string email ] + WHERE c.data @> @criteria" + |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| email = email |}) ] |> Sql.executeAsync toCitizenSecurityPair return List.tryHead results } @@ -188,12 +181,12 @@ let saveSecurityInfo security = backgroundTask { } /// Try to retrieve security information by the given token -let trySecurityByToken token = backgroundTask { +let trySecurityByToken (token : string) = backgroundTask { do! checkForPurge false let! results = dataSource () - |> Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data ->> 'token' = @token" - |> Sql.parameters [ "@token", Sql.string token ] + |> Sql.query $"SELECT * FROM {Table.SecurityInfo} WHERE data @> @criteria" + |> Sql.parameters [ "@criteria", Sql.jsonb (mkDoc {| token = token |}) ] |> Sql.executeAsync toDocument return List.tryHead results } @@ -204,7 +197,11 @@ let trySecurityByToken token = backgroundTask { let legacy () = backgroundTask { return! 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 } @@ -212,13 +209,13 @@ let legacy () = backgroundTask { let current () = backgroundTask { return! dataSource () - |> Sql.query $" + |> Sql.query $""" SELECT c.* FROM {Table.Citizen} c INNER JOIN {Table.SecurityInfo} si ON si.id = c.id - WHERE c.data ->> 'isLegacy' = 'false' - AND si.data ->> 'accountLocked' = 'false' - AND NOT EXISTS (SELECT 1 FROM {Table.Profile} p WHERE p.id = c.id)" + WHERE c.data @> '{{ "isLegacy": false }}'::jsonb + AND si.data @> '{{ "accountLocked": false }}'::jsonb + AND NOT EXISTS (SELECT 1 FROM {Table.Profile} p WHERE p.id = c.id)""" |> Sql.executeAsync toDocument } @@ -241,11 +238,12 @@ let migrateLegacy currentId legacyId = backgroundTask { Table.Profile (CitizenId.toString currentId) (Sql.existingConnection conn) (mkDoc { profile with Id = currentId; IsLegacy = false }) | None -> () - let! listings = + let oldCriteria = mkDoc {| citizenId = oldId |} + let! listings = conn |> Sql.existingConnection - |> Sql.query $"SELECT * FROM {Table.Listing} WHERE data ->> 'citizenId' = @oldId" - |> Sql.parameters [ "@oldId", Sql.string oldId ] + |> Sql.query $"SELECT * FROM {Table.Listing} WHERE data @> @criteria" + |> Sql.parameters [ "@criteria", Sql.jsonb oldCriteria ] |> Sql.executeAsync toDocument for listing in listings do let newListing = { listing with Id = ListingId.create (); CitizenId = currentId; IsLegacy = false } @@ -254,8 +252,8 @@ let migrateLegacy currentId legacyId = backgroundTask { let! successes = conn |> Sql.existingConnection - |> Sql.query $"SELECT * FROM {Table.Success} WHERE data ->> 'citizenId' = @oldId" - |> Sql.parameters [ "@oldId", Sql.string oldId ] + |> Sql.query $"SELECT * FROM {Table.Success} WHERE data @> @criteria" + |> Sql.parameters [ "@criteria", Sql.string oldCriteria ] |> Sql.executeAsync toDocument for success in successes do let newSuccess = { success with Id = SuccessId.create (); CitizenId = currentId } @@ -266,10 +264,10 @@ let migrateLegacy currentId legacyId = backgroundTask { conn |> Sql.existingConnection |> Sql.query $" - DELETE FROM {Table.Success} WHERE data ->> 'citizenId' = @oldId; - DELETE FROM {Table.Listing} WHERE data ->> 'citizenId' = @oldId; - DELETE FROM {Table.Citizen} WHERE id = @oldId" - |> Sql.parameters [ "@oldId", Sql.string oldId ] + DELETE FROM {Table.Success} WHERE data @> @criteria; + DELETE FROM {Table.Listing} WHERE data @> @criteria; + DELETE FROM {Table.Citizen} WHERE id = @oldId" + |> Sql.parameters [ "@criteria", Sql.jsonb oldCriteria; "@oldId", Sql.string oldId ] |> Sql.executeNonQueryAsync do! txn.CommitAsync () return Ok "" diff --git a/src/JobsJobsJobs/Citizens/Views.fs b/src/JobsJobsJobs/Citizens/Views.fs index 2bdebdd..06f77f2 100644 --- a/src/JobsJobsJobs/Citizens/Views.fs +++ b/src/JobsJobsJobs/Citizens/Views.fs @@ -189,7 +189,7 @@ let dashboard (citizen : Citizen) (profile : Profile option) profileCount tz = emptyP p [] [ txt "To see how this application works, check out “How It Works” in the sidebar (last updated " - txt "August 29th, 2021)." + txt "February 2nd, 2023)." ] ] diff --git a/src/JobsJobsJobs/Common/Cache.fs b/src/JobsJobsJobs/Common/Cache.fs index 2e89c9c..c7ef8f7 100644 --- a/src/JobsJobsJobs/Common/Cache.fs +++ b/src/JobsJobsJobs/Common/Cache.fs @@ -87,7 +87,7 @@ type DistributedCache () = expire_at TIMESTAMPTZ NOT NULL, sliding_expiration INTERVAL, absolute_expiration TIMESTAMPTZ); - CREATE INDEX idx_session_expiration ON session (expire_at)" + CREATE INDEX idx_session_expiration ON jjj.session (expire_at)" |> Sql.executeNonQueryAsync () } |> sync diff --git a/src/JobsJobsJobs/Common/Views.fs b/src/JobsJobsJobs/Common/Views.fs index 29afec3..d3a0112 100644 --- a/src/JobsJobsJobs/Common/Views.fs +++ b/src/JobsJobsJobs/Common/Views.fs @@ -297,7 +297,7 @@ module Layout = let version = seq { string v.Major - if v.Minor > 0 then + if v.Minor > 0 || v.Build > 0 then "."; string v.Minor if v.Build > 0 then "."; string v.Build diff --git a/src/JobsJobsJobs/Directory.Build.props b/src/JobsJobsJobs/Directory.Build.props index 7fd3942..e803ac5 100644 --- a/src/JobsJobsJobs/Directory.Build.props +++ b/src/JobsJobsJobs/Directory.Build.props @@ -4,7 +4,7 @@ enable embedded false - 3.0.0.0 - 3.0.0.0 + 3.0.1.0 + 3.0.1.0 diff --git a/src/JobsJobsJobs/Home/Views.fs b/src/JobsJobsJobs/Home/Views.fs index b70439f..e9974ec 100644 --- a/src/JobsJobsJobs/Home/Views.fs +++ b/src/JobsJobsJobs/Home/Views.fs @@ -28,7 +28,7 @@ let privacyPolicy = let appName = txt "Jobs, Jobs, Jobs" article [] [ h3 [] [ txt "Privacy Policy" ] - p [ _class "fst-italic" ] [ txt "(as of December 27th, 2022)" ] + p [ _class "fst-italic" ] [ txt "(as of February 2nd, 2023)" ] p [] [ appName; txt " (“we,” “our,” or “us”) is committed to protecting your " @@ -477,7 +477,7 @@ let privacyPolicy = hr [] - p [ _class "fst-italic" ] [ txt "Changes for "; appName; txt " v3 (December 27th, 2022)" ] + p [ _class "fst-italic" ] [ txt "Changes for "; appName; txt " v3 (February 2nd, 2023)" ] ul [] [ li [ _class "fst-italic" ] [ txt "Removed references to Mastodon" ] li [ _class "fst-italic" ] [ txt "Added references to job listings" ] @@ -494,7 +494,7 @@ let privacyPolicy = let termsOfService = article [] [ h3 [] [ txt "Terms of Service" ] - p [ _class "fst-italic" ] [ txt "(as of August 30th, 2022)" ] + p [ _class "fst-italic" ] [ txt "(as of February 2nd, 2023)" ] h4 [] [ txt "Acceptance of Terms" ] p [] [ 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 [] p [ _class "fst-italic" ] [ - txt "Change on August 30th, 2022 – added references to job listings, removed references " + txt "Change on February 2nd, 2023 – added references to job listings, removed references " txt "to Mastodon instances." ] p [ _class "fst-italic" ] [ @@ -945,7 +945,7 @@ module Help = let index = article [] [ h3 [ _class "mb-0" ] [ txt "How It Works" ] - h6 [ _class "mb-3 text-muted fst-italic" ] [ txt "Last Updated January 22nd, 2023" ] + h6 [ _class "mb-3 text-muted fst-italic" ] [ txt "Last Updated February 2nd, 2023" ] p [ _class "fst-italic" ] [ txt "Show me how to "; a [ _href "/how-it-works/listings#searching" ] [ txt "find a job" ] diff --git a/src/JobsJobsJobs/Listings/Data.fs b/src/JobsJobsJobs/Listings/Data.fs index f7f0529..ea0d82d 100644 --- a/src/JobsJobsJobs/Listings/Data.fs +++ b/src/JobsJobsJobs/Listings/Data.fs @@ -22,8 +22,9 @@ let private toListingForView row = /// Find all job listings posted by the given citizen let findByCitizen citizenId = dataSource () - |> Sql.query $"{viewSql} WHERE l.data ->> 'citizenId' = @citizenId AND l.data ->> 'isLegacy' = 'false'" - |> Sql.parameters [ "@citizenId", Sql.string (CitizenId.toString citizenId) ] + |> Sql.query $"{viewSql} WHERE l.data @> @criteria" + |> Sql.parameters + [ "@criteria", Sql.jsonb (mkDoc {| citizenId = CitizenId.toString citizenId; isLegacy = false |}) ] |> Sql.executeAsync toListingForView /// Find a listing by its ID @@ -38,7 +39,7 @@ let findById listingId = backgroundTask { let findByIdForView listingId = backgroundTask { let! tryListing = 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.executeAsync toListingForView return List.tryHead tryListing @@ -52,18 +53,18 @@ let save (listing : Listing) = let search (search : ListingSearchForm) = let searches = [ 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 "l.data ->> 'region' ILIKE @region", [ "@region", like search.Region ] 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 "l.data ->> 'text' ILIKE @text", [ "@text", like search.Text ] ] dataSource () - |> Sql.query $" + |> Sql.query $""" {viewSql} - WHERE l.data ->> 'isExpired' = 'false' AND l.data ->> 'isLegacy' = 'false' - {searchSql searches}" + WHERE l.data @> '{{ "isExpired": false, "isLegacy": false }}'::jsonb + {searchSql searches}""" |> Sql.parameters (searches |> List.collect snd) |> Sql.executeAsync toListingForView diff --git a/src/JobsJobsJobs/Profiles/Data.fs b/src/JobsJobsJobs/Profiles/Data.fs index ac58848..1dff246 100644 --- a/src/JobsJobsJobs/Profiles/Data.fs +++ b/src/JobsJobsJobs/Profiles/Data.fs @@ -8,7 +8,8 @@ open Npgsql.FSharp /// Count the current profiles let count () = 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") /// Delete a profile by its ID @@ -40,13 +41,13 @@ let private toProfileForView row = let findByIdForView citizenId = backgroundTask { let! tryCitizen = dataSource () - |> Sql.query $" + |> Sql.query $""" SELECT p.*, c.data AS cit_data, o.data AS cont_data - FROM {Table.Profile} p - INNER JOIN {Table.Citizen} c ON c.id = p.id - INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId' - WHERE p.id = @id - AND p.data ->> 'isLegacy' = 'false'" + FROM {Table.Profile} p + INNER JOIN {Table.Citizen} c ON c.id = p.id + INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId' + WHERE p.id = @id + AND p.data @> '{{ "isLegacy": false }}'::jsonb""" |> Sql.parameters [ "@id", Sql.string (CitizenId.toString citizenId) ] |> Sql.executeAsync toProfileForView return List.tryHead tryCitizen @@ -60,26 +61,28 @@ let save (profile : Profile) = let search (search : ProfileSearchForm) isPublic = backgroundTask { let searches = [ 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 - "p.data ->> 'isRemote' = @remote", [ "@remote", jsonBool (search.RemoteWork = "yes") ] + "p.data @> @remote", [ "@remote", Sql.jsonb (mkDoc {| isRemote = search.RemoteWork = "yes" |}) ] if search.Text <> "" then "p.text_search @@ plainto_tsquery(@text_search)", [ "@text_search", Sql.string search.Text ] ] let vizSql = if isPublic then - sprintf "IN ('%s', '%s')" (ProfileVisibility.toString Public) (ProfileVisibility.toString Anonymous) - else sprintf "<> '%s'" (ProfileVisibility.toString Hidden) + sprintf "(p.data @> '%s'::jsonb OR p.data @> '%s'::jsonb)" + (mkDoc {| visibility = ProfileVisibility.toString Public |}) + (mkDoc {| visibility = ProfileVisibility.toString Anonymous |}) + else sprintf "p.data ->> 'visibility' <> '%s'" (ProfileVisibility.toString Hidden) let! results = dataSource () - |> Sql.query $" + |> Sql.query $""" SELECT p.*, c.data AS cit_data, o.data AS cont_data - FROM {Table.Profile} p - INNER JOIN {Table.Citizen} c ON c.id = p.id - INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId' - WHERE p.data ->> 'isLegacy' = 'false' - AND p.data ->> 'visibility' {vizSql} - {searchSql searches}" + FROM {Table.Profile} p + INNER JOIN {Table.Citizen} c ON c.id = p.id + INNER JOIN {Table.Continent} o ON o.id = p.data ->> 'continentId' + WHERE p.data @> '{{ "isLegacy": false }}'::jsonb + AND {vizSql} + {searchSql searches}""" |> Sql.parameters (searches |> List.collect snd) |> Sql.executeAsync toProfileForView return results |> List.sortBy (fun pfv -> (Citizen.name pfv.Citizen).ToLowerInvariant ()) diff --git a/src/JobsJobsJobs/SuccessStories/Data.fs b/src/JobsJobsJobs/SuccessStories/Data.fs index 67cb200..f179529 100644 --- a/src/JobsJobsJobs/SuccessStories/Data.fs +++ b/src/JobsJobsJobs/SuccessStories/Data.fs @@ -30,4 +30,4 @@ let findById successId = /// Save a success story let save (success : Success) = - dataSource () |> saveDocument Table.Success (SuccessId.toString success.Id) <| mkDoc success + (dataSource (), mkDoc success) ||> saveDocument Table.Success (SuccessId.toString success.Id)