Partially integrate RethinkDB F# driver (#34)

This commit is contained in:
Daniel J. Summers 2022-07-11 20:43:15 -04:00
parent 5f9156f6c2
commit cc21849220
6 changed files with 195 additions and 272 deletions

View File

@ -63,7 +63,8 @@ Target.create "All" ignore
"BuildClient" "BuildClient"
?=> "BuildServer" ?=> "BuildServer"
"BuildClient" "BuildClient"
==> "RunServer" ?=> "RunServer"
"BuildClient"
==> "BuildAndRun" ==> "BuildAndRun"
"BuildClient" "BuildClient"
==> "Publish" ==> "Publish"
@ -71,4 +72,7 @@ Target.create "All" ignore
"BuildServer" "BuildServer"
==> "All" ==> "All"
"RunServer"
==> "BuildAndRun"
Target.runOrDefault "All" Target.runOrDefault "All"

View File

@ -1,12 +1,12 @@
{ {
"name": "jobs-jobs-jobs", "name": "jobs-jobs-jobs",
"version": "2.2.0", "version": "2.2.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jobs-jobs-jobs", "name": "jobs-jobs-jobs",
"version": "2.2.0", "version": "2.2.2",
"dependencies": { "dependencies": {
"@mdi/js": "^5.9.55", "@mdi/js": "^5.9.55",
"@vuelidate/core": "^2.0.0-alpha.24", "@vuelidate/core": "^2.0.0-alpha.24",

View File

@ -1,6 +1,6 @@
{ {
"name": "jobs-jobs-jobs", "name": "jobs-jobs-jobs",
"version": "2.2.1", "version": "2.2.2",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

View File

@ -66,7 +66,7 @@ let configureServices (svc : IServiceCollection) =
let log = svcs.GetRequiredService<ILoggerFactory>().CreateLogger "JobsJobsJobs.Api.Data.Startup" let log = svcs.GetRequiredService<ILoggerFactory>().CreateLogger "JobsJobsJobs.Api.Data.Startup"
let conn = Data.Startup.createConnection dbCfg log let conn = Data.Startup.createConnection dbCfg log
svc.AddSingleton conn |> ignore svc.AddSingleton conn |> ignore
Data.Startup.establishEnvironment dbCfg log conn |> Data.awaitIgnore Data.Startup.establishEnvironment dbCfg log conn |> Async.AwaitTask |> Async.RunSynchronously
[<EntryPoint>] [<EntryPoint>]
let main _ = let main _ =

View File

@ -2,17 +2,6 @@
module JobsJobsJobs.Api.Data module JobsJobsJobs.Api.Data
open JobsJobsJobs.Domain.Types open JobsJobsJobs.Domain.Types
open Polly
open RethinkDb.Driver
open RethinkDb.Driver.Net
open RethinkDb.Driver.Ast
/// Shorthand for the RethinkDB R variable (how every command starts)
let private r = RethinkDB.R
/// Shorthand for await task / run sync / ignore (used in non-async contexts)
let awaitIgnore x = x |> Async.AwaitTask |> Async.RunSynchronously |> ignore
/// JSON converters used with RethinkDB persistence /// JSON converters used with RethinkDB persistence
module Converters = module Converters =
@ -100,6 +89,28 @@ module Table =
let all () = [ Citizen; Continent; Listing; Profile; Success ] let all () = [ Citizen; Continent; Listing; Profile; Success ]
open RethinkDb.Driver.FSharp.Functions
open RethinkDb.Driver.Net
/// Reconnection functions (if the RethinkDB driver has a network error, it will not reconnect on its own)
[<AutoOpen>]
module private Reconnect =
/// Retrieve a result using the F# driver's default retry policy
let result<'T> conn expr = runResult<'T> expr |> withRetryDefault |> withConn conn
/// Retrieve an optional result using the F# driver's default retry policy
let resultOption<'T> conn expr = runResult<'T> expr |> withRetryDefault |> asOption |> withConn conn
/// Write a query using the F# driver's default retry policy, ignoring the result
let write conn expr = runWrite expr |> withRetryDefault |> ignoreResult |> withConn conn
open RethinkDb.Driver.Ast
/// Shorthand for the RethinkDB R variable (how every command starts)
let private r = RethinkDb.Driver.RethinkDB.R
/// Functions run at startup /// Functions run at startup
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Startup = module Startup =
@ -108,156 +119,93 @@ module Startup =
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open NodaTime open NodaTime
open NodaTime.Serialization.JsonNet open NodaTime.Serialization.JsonNet
open RethinkDb.Driver.FSharp
/// Create a RethinkDB connection /// Create a RethinkDB connection
let createConnection (cfg : IConfigurationSection) (log : ILogger) = let createConnection (cfg : IConfigurationSection) (log : ILogger) =
// Add all required JSON converters // Add all required JSON converters
Converter.Serializer.ConfigureForNodaTime DateTimeZoneProviders.Tzdb |> ignore Converter.Serializer.ConfigureForNodaTime DateTimeZoneProviders.Tzdb |> ignore
Converters.all () Converters.all ()
|> List.iter Converter.Serializer.Converters.Add |> List.iter Converter.Serializer.Converters.Add
// Read the configuration and create a connection // Connect to the database
let bldr = let config = DataConfig.FromConfiguration cfg
seq<Connection.Builder -> Connection.Builder> { log.LogInformation $"Connecting to rethinkdb://{config.Hostname}:{config.Port}/{config.Database}"
yield fun b -> match cfg["Hostname"] with null -> b | host -> b.Hostname host config.CreateConnection ()
yield fun b -> match cfg["Port"] with null -> b | port -> (int >> b.Port) port
yield fun b -> match cfg["AuthKey"] with null -> b | key -> b.AuthKey key
yield fun b -> match cfg["Db"] with null -> b | db -> b.Db db
yield fun b -> match cfg["Timeout"] with null -> b | time -> (int >> b.Timeout) time
}
|> Seq.fold (fun b step -> step b) (r.Connection ())
match log.IsEnabled LogLevel.Debug with
| true -> log.LogDebug $"RethinkDB: Connecting to {bldr.Hostname}:{bldr.Port}, database {bldr.Db}"
| false -> ()
bldr.Connect () :> IConnection
/// Ensure the data, tables, and indexes that are required exist /// Ensure the data, tables, and indexes that are required exist
let establishEnvironment (cfg : IConfigurationSection) (log : ILogger) conn = task { let establishEnvironment (cfg : IConfigurationSection) (log : ILogger) conn = task {
// Ensure the database exists // Ensure the database exists
match cfg["Db"] |> Option.ofObj with match cfg["database"] |> Option.ofObj with
| Some database -> | Some database ->
let! dbs = r.DbList().RunResultAsync<string list> conn let! dbs = dbList () |> result<string list> conn
match dbs |> List.contains database with match dbs |> List.contains database with
| true -> () | true -> ()
| false -> | false ->
log.LogInformation $"Creating database {database}..." log.LogInformation $"Creating database {database}..."
let! _ = r.DbCreate(database).RunWriteAsync conn do! dbCreate database |> write conn
() ()
| None -> () | None -> ()
// Ensure the tables exist // Ensure the tables exist
let! tables = r.TableList().RunResultAsync<string list> conn let! tables = tableListFromDefault () |> result<string list> conn
Table.all () for table in Table.all () do
|> List.iter ( if not (List.contains table tables) then
fun tbl -> log.LogInformation $"Creating {table} table..."
match tables |> List.contains tbl with do! tableCreateInDefault table |> write conn
| true -> ()
| false ->
log.LogInformation $"Creating {tbl} table..."
r.TableCreate(tbl).RunWriteAsync conn |> awaitIgnore)
// Ensure the indexes exist // Ensure the indexes exist
let ensureIndexes table indexes = task { let ensureIndexes table indexes = task {
let! tblIdxs = r.Table(table).IndexList().RunResultAsync<string list> conn let! tblIndexes = fromTable table |> indexList |> result<string list> conn
indexes for index in indexes do
|> List.iter ( if not (List.contains index tblIndexes) then
fun idx -> log.LogInformation $"Creating \"{index}\" index on {table}"
match tblIdxs |> List.contains idx with do! fromTable table |> indexCreate index |> write conn
| true -> ()
| false ->
log.LogInformation $"Creating \"{idx}\" index on {table}"
r.Table(table).IndexCreate(idx).RunWriteAsync conn |> awaitIgnore)
} }
do! ensureIndexes Table.Listing [ "citizenId"; "continentId"; "isExpired" ] do! ensureIndexes Table.Listing [ "citizenId"; "continentId"; "isExpired" ]
do! ensureIndexes Table.Profile [ "continentId" ] do! ensureIndexes Table.Profile [ "continentId" ]
do! ensureIndexes Table.Success [ "citizenId" ] do! ensureIndexes Table.Success [ "citizenId" ]
// The instance/user is a compound index // The instance/user is a compound index
let! userIdx = r.Table(Table.Citizen).IndexList().RunResultAsync<string list> conn let! userIdx = fromTable Table.Citizen |> indexList |> result<string list> conn
match userIdx |> List.contains "instanceUser" with if not (List.contains "instanceUser" userIdx) then
| true -> () do! fromTable Table.Citizen
| false -> |> indexCreateFunc "instanceUser" (fun row -> r.Array (row.G "instance", row.G "mastodonUser"))
let! _ = |> write conn
r.Table(Table.Citizen)
.IndexCreate("instanceUser",
ReqlFunction1 (fun row -> upcast r.Array (row.G "instance", row.G "mastodonUser")))
.RunWriteAsync conn
()
} }
/// Determine if a record type (not nullable) is null
let toOption x = match x |> box |> isNull with true -> None | false -> Some x
[<AutoOpen>]
module private Reconnect =
open System.Threading.Tasks
/// Execute a query with a retry policy that will reconnect to RethinkDB if it has gone away
let withReconn (conn : IConnection) (f : IConnection -> Task<'T>) =
Policy
.Handle<ReqlDriverError>()
.RetryAsync(System.Action<exn, int> (fun ex _ ->
printf "Encountered RethinkDB exception: %s" ex.Message
match ex.Message.Contains "socket" with
| true ->
printf "Reconnecting to RethinkDB"
(conn :?> Connection).Reconnect false
| false -> ()))
.ExecuteAsync(fun () -> f conn)
/// Execute a query that returns one or none item, using the reconnect logic
let withReconnOption (conn : IConnection) (f : IConnection -> Task<'T>) =
fun c -> task {
let! it = f c
return toOption it
}
|> withReconn conn
/// Execute a query that does not return a result, using the above reconnect logic
let withReconnIgnore (conn : IConnection) (f : IConnection -> Task<'T>) =
fun c -> task {
let! _ = f c
()
}
|> withReconn conn
/// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries
let regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)%s"
open JobsJobsJobs.Domain open JobsJobsJobs.Domain
open JobsJobsJobs.Domain.SharedTypes open JobsJobsJobs.Domain.SharedTypes
/// Sanitize user input, and create a "contains" pattern for use with RethinkDB queries
let regexContains = System.Text.RegularExpressions.Regex.Escape >> sprintf "(?i)%s"
/// Profile data access functions /// Profile data access functions
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Profile = module Profile =
/// Count the current profiles
let count conn = let count conn =
r.Table(Table.Profile) fromTable Table.Profile
.Count() |> count
.RunResultAsync<int64> |> result<int64> conn
|> withReconn conn
/// Find a profile by citizen ID /// Find a profile by citizen ID
let findById (citizenId : CitizenId) conn = let findById (citizenId : CitizenId) conn =
r.Table(Table.Profile) fromTable Table.Profile
.Get(citizenId) |> get citizenId
.RunResultAsync<Profile> |> resultOption<Profile> conn
|> withReconnOption conn
/// Insert or update a profile /// Insert or update a profile
let save (profile : Profile) conn = let save (profile : Profile) conn =
r.Table(Table.Profile) fromTable Table.Profile
.Get(profile.id) |> get profile.id
.Replace(profile) |> replace profile
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Delete a citizen's profile /// Delete a citizen's profile
let delete (citizenId : CitizenId) conn = let delete (citizenId : CitizenId) conn =
r.Table(Table.Profile) fromTable Table.Profile
.Get(citizenId) |> get citizenId
.Delete() |> delete
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Search profiles (logged-on users) /// Search profiles (logged-on users)
let search (search : ProfileSearch) conn = let search (search : ProfileSearch) conn =
@ -287,32 +235,30 @@ module Profile =
.EqJoin("id", r.Table Table.Citizen) .EqJoin("id", r.Table Table.Citizen)
.Without(r.HashMap ("right", "id")) .Without(r.HashMap ("right", "id"))
.Zip () :> ReqlExpr)) .Zip () :> ReqlExpr))
.Merge(ReqlFunction1 (fun it -> |> mergeFunc (fun it ->
upcast r r.HashMap("displayName",
.HashMap("displayName",
r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName", r.Branch (it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName", it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "mastodonUser")) it.G "mastodonUser"))
.With ("citizenId", it.G "id"))) .With ("citizenId", it.G "id"))
.Pluck("citizenId", "displayName", "seekingEmployment", "remoteWork", "fullTime", "lastUpdatedOn") |> pluck [ "citizenId"; "displayName"; "seekingEmployment"; "remoteWork"; "fullTime"; "lastUpdatedOn" ]
.OrderBy(ReqlFunction1 (fun it -> upcast it.G("displayName").Downcase ())) |> orderByFunc (fun it -> it.G("displayName").Downcase ())
.RunResultAsync<ProfileSearchResult list> |> result<ProfileSearchResult list> conn
|> withReconn conn
// Search profiles (public) // Search profiles (public)
let publicSearch (srch : PublicSearch) conn = let publicSearch (search : PublicSearch) conn =
(seq<ReqlExpr -> ReqlExpr> { (seq<ReqlExpr -> ReqlExpr> {
match srch.continentId with match search.continentId with
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString cId))) | Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
| None -> () | None -> ()
match srch.region with match search.region with
| Some reg -> | Some reg ->
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg)))) yield (fun q -> q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))))
| None -> () | None -> ()
match srch.remoteWork with match search.remoteWork with
| "" -> () | "" -> ()
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes"))) | _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
match srch.skill with match search.skill with
| Some skl -> | Some skl ->
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> yield (fun q -> q.Filter (ReqlFunction1 (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))))))
@ -326,16 +272,14 @@ module Profile =
.Without(r.HashMap ("right", "id")) .Without(r.HashMap ("right", "id"))
.Zip() .Zip()
.Filter(r.HashMap ("isPublic", true)))) .Filter(r.HashMap ("isPublic", true))))
.Merge(ReqlFunction1 (fun it -> |> mergeFunc (fun it ->
upcast r r.HashMap("skills",
.HashMap("skills",
it.G("skills").Map (ReqlFunction1 (fun skill -> it.G("skills").Map (ReqlFunction1 (fun skill ->
upcast 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"))) .With("continent", it.G "name"))
.Pluck("continent", "region", "skills", "remoteWork") |> pluck [ "continent"; "region"; "skills"; "remoteWork" ]
.RunResultAsync<PublicSearchResult list> |> result<PublicSearchResult list> conn
|> withReconn conn
/// Citizen data access functions /// Citizen data access functions
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
@ -343,68 +287,57 @@ module Citizen =
/// Find a citizen by their ID /// Find a citizen by their ID
let findById (citizenId : CitizenId) conn = let findById (citizenId : CitizenId) conn =
r.Table(Table.Citizen) fromTable Table.Citizen
.Get(citizenId) |> get citizenId
.RunResultAsync<Citizen> |> resultOption<Citizen> conn
|> withReconnOption conn
/// Find a citizen by their Mastodon username /// Find a citizen by their Mastodon username
let findByMastodonUser (instance : string) (mastodonUser : string) conn = let findByMastodonUser (instance : string) (mastodonUser : string) conn = task {
fun c -> task {
let! u = let! u =
r.Table(Table.Citizen) fromTable Table.Citizen
.GetAll(r.Array (instance, mastodonUser)).OptArg("index", "instanceUser").Limit(1) |> getAllWithIndex [ r.Array (instance, mastodonUser) ] "instanceUser"
.RunResultAsync<Citizen list> c |> limit 1
return u |> List.tryHead |> result<Citizen list> conn
return List.tryHead u
} }
|> withReconn conn
/// Add a citizen /// Add a citizen
let add (citizen : Citizen) conn = let add (citizen : Citizen) conn =
r.Table(Table.Citizen) fromTable Table.Citizen
.Insert(citizen) |> insert citizen
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Update the display name and last seen on date for a citizen /// Update the display name and last seen on date for a citizen
let logOnUpdate (citizen : Citizen) conn = let logOnUpdate (citizen : Citizen) conn =
r.Table(Table.Citizen) fromTable Table.Citizen
.Get(citizen.id) |> get citizen.id
.Update(r.HashMap( nameof citizen.displayName, citizen.displayName) |> update (r.HashMap( nameof citizen.displayName, citizen.displayName)
.With (nameof citizen.lastSeenOn, citizen.lastSeenOn)) .With (nameof citizen.lastSeenOn, citizen.lastSeenOn))
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Delete a citizen /// Delete a citizen
let delete citizenId conn = let delete citizenId conn = task {
fun c -> task { do! Profile.delete citizenId conn
do! Profile.delete citizenId c do! fromTable Table.Success
let! _ = |> getAllWithIndex [ citizenId ] "citizenId"
r.Table(Table.Success) |> delete
.GetAll(citizenId).OptArg("index", "citizenId") |> write conn
.Delete() do! fromTable Table.Listing
.RunWriteAsync c |> getAllWithIndex [ citizenId ] "citizenId"
let! _ = |> delete
r.Table(Table.Listing) |> write conn
.GetAll(citizenId).OptArg("index", "citizenId") do! fromTable Table.Citizen
.Delete() |> get citizenId
.RunWriteAsync c |> delete
let! _ = |> write conn
r.Table(Table.Citizen)
.Get(citizenId)
.Delete()
.RunWriteAsync c
()
} }
|> withReconnIgnore conn
/// Update a citizen's real name /// Update a citizen's real name
let realNameUpdate (citizenId : CitizenId) (realName : string option) conn = let realNameUpdate (citizenId : CitizenId) (realName : string option) conn =
r.Table(Table.Citizen) fromTable Table.Citizen
.Get(citizenId) |> get citizenId
.Update(r.HashMap (nameof realName, realName)) |> update (r.HashMap (nameof realName, realName))
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Continent data access functions /// Continent data access functions
@ -413,16 +346,14 @@ module Continent =
/// Get all continents /// Get all continents
let all conn = let all conn =
r.Table(Table.Continent) fromTable Table.Continent
.RunResultAsync<Continent list> |> result<Continent list> conn
|> withReconn conn
/// Get a continent by its ID /// Get a continent by its ID
let findById (contId : ContinentId) conn = let findById (contId : ContinentId) conn =
r.Table(Table.Continent) fromTable Table.Continent
.Get(contId) |> get contId
.RunResultAsync<Continent> |> resultOption<Continent> conn
|> withReconnOption conn
/// Job listing data access functions /// Job listing data access functions
@ -433,55 +364,48 @@ module Listing =
/// 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 =
r.Table(Table.Listing) fromTable Table.Listing
.GetAll(citizenId).OptArg("index", nameof citizenId) |> getAllWithIndex [ citizenId ] (nameof citizenId)
.EqJoin("continentId", r.Table Table.Continent) |> eqJoin "continentId" (fromTable Table.Continent)
.Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))) |> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))
.RunResultAsync<ListingForView list> |> result<ListingForView list> conn
|> withReconn conn
/// Find a listing by its ID /// Find a listing by its ID
let findById (listingId : ListingId) conn = let findById (listingId : ListingId) conn =
r.Table(Table.Listing) fromTable Table.Listing
.Get(listingId) |> get listingId
.RunResultAsync<Listing> |> resultOption<Listing> conn
|> withReconnOption conn
/// Find a listing by its ID for viewing (includes continent information) /// Find a listing by its ID for viewing (includes continent information)
let findByIdForView (listingId : ListingId) conn = let findByIdForView (listingId : ListingId) conn = task {
fun c -> task {
let! listing = let! listing =
r.Table(Table.Listing) fromTable Table.Listing
.Filter(r.HashMap ("id", listingId)) |> filter (r.HashMap ("id", listingId))
.EqJoin("continentId", r.Table Table.Continent) |> eqJoin "continentId" (fromTable Table.Continent)
.Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))) |> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))
.RunResultAsync<ListingForView list> c |> result<ListingForView list> conn
return List.tryHead listing return List.tryHead listing
} }
|> withReconn conn
/// Add a listing /// Add a listing
let add (listing : Listing) conn = let add (listing : Listing) conn =
r.Table(Table.Listing) fromTable Table.Listing
.Insert(listing) |> insert listing
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Update a listing /// Update a listing
let update (listing : Listing) conn = let update (listing : Listing) conn =
r.Table(Table.Listing) fromTable Table.Listing
.Get(listing.id) |> get listing.id
.Replace(listing) |> replace listing
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Expire a listing /// Expire a listing
let expire (listingId : ListingId) (fromHere : bool) (now : Instant) conn = let expire (listingId : ListingId) (fromHere : bool) (now : Instant) conn =
r.Table(Table.Listing) (fromTable Table.Listing
.Get(listingId) |> get listingId)
.Update(r.HashMap("isExpired", true).With("wasFilledHere", fromHere).With ("updatedOn", now)) .Update (r.HashMap("isExpired", true).With("wasFilledHere", fromHere).With ("updatedOn", now))
.RunWriteAsync |> write conn
|> withReconnIgnore conn
/// Search job listings /// Search job listings
let search (search : ListingSearch) conn = let search (search : ListingSearch) conn =
@ -506,12 +430,11 @@ module Listing =
|> Seq.toList |> Seq.toList
|> List.fold |> List.fold
(fun q f -> f q) (fun q f -> f q)
(r.Table(Table.Listing) (fromTable Table.Listing
.GetAll(false).OptArg ("index", "isExpired"))) |> getAllWithIndex [ false ] "isExpired" :> ReqlExpr))
.EqJoin("continentId", r.Table Table.Continent) |> eqJoin "continentId" (fromTable Table.Continent)
.Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))) |> mapFunc (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right"))
.RunResultAsync<ListingForView list> |> result<ListingForView list> conn
|> withReconn conn
/// Success story data access functions /// Success story data access functions
@ -520,32 +443,29 @@ module Success =
/// Find a success report by its ID /// Find a success report by its ID
let findById (successId : SuccessId) conn = let findById (successId : SuccessId) conn =
r.Table(Table.Success) fromTable Table.Success
.Get(successId) |> get successId
.RunResultAsync<Success> |> resultOption conn
|> withReconnOption conn
/// Insert or update a success story /// Insert or update a success story
let save (success : Success) conn = let save (success : Success) conn =
r.Table(Table.Success) fromTable Table.Success
.Get(success.id) |> get success.id
.Replace(success) |> replace success
.RunWriteAsync |> write conn
|> withReconnIgnore conn
// Retrieve all success stories // Retrieve all success stories
let all conn = let all conn =
r.Table(Table.Success) (fromTable Table.Success
.EqJoin("citizenId", r.Table Table.Citizen) |> eqJoin "citizenId" (fromTable Table.Citizen))
.Without(r.HashMap ("right", "id")) .Without(r.HashMap ("right", "id"))
.Zip() |> zip
.Merge(ReqlFunction1 (fun it -> |> mergeFunc (fun it ->
r.HashMap("citizenName", r.HashMap("citizenName",
r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName", r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName", it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "mastodonUser")) it.G "mastodonUser"))
.With ("hasStory", it.G("story").Default_("").Gt ""))) .With ("hasStory", it.G("story").Default_("").Gt ""))
.Pluck("id", "citizenId", "citizenName", "recordedOn", "fromHere", "hasStory") |> pluck [ "id"; "citizenId"; "citizenName"; "recordedOn"; "fromHere"; "hasStory" ]
.OrderBy(r.Desc "recordedOn") |> orderByDescending "recordedOn"
.RunResultAsync<StoryEntry list> |> result<StoryEntry list> conn
|> withReconn conn

View File

@ -23,11 +23,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="5.0.0" /> <PackageReference Include="Giraffe" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.6" />
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" /> <PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
<PackageReference Include="Polly" Version="7.2.2" />
<PackageReference Include="RethinkDb.Driver" Version="2.3.150" /> <PackageReference Include="RethinkDb.Driver" Version="2.3.150" />
<PackageReference Include="RethinkDb.Driver.FSharp" Version="0.9.0-beta-05" /> <PackageReference Include="RethinkDb.Driver.FSharp" Version="0.9.0-beta-05" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.21.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.21.0" />