From 00651728d8844071b23b5d36453b022e90825438 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 7 Oct 2023 11:14:30 -0400 Subject: [PATCH 01/11] Move LiteDB data impl --- src/MyPrayerJournal/Handlers.fs | 52 ++++++++++---------- src/MyPrayerJournal/{Data.fs => LiteData.fs} | 2 +- src/MyPrayerJournal/MyPrayerJournal.fsproj | 2 +- src/MyPrayerJournal/Program.fs | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) rename src/MyPrayerJournal/{Data.fs => LiteData.fs} (99%) diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs index a214345..79d92da 100644 --- a/src/MyPrayerJournal/Handlers.fs +++ b/src/MyPrayerJournal/Handlers.fs @@ -127,7 +127,7 @@ module private Helpers = let pageContext (ctx : HttpContext) pageTitle content = backgroundTask { let! hasSnoozed = match ctx.CurrentUser with - | Some _ -> Data.hasSnoozed ctx.UserId (ctx.Now ()) ctx.Db + | Some _ -> LiteData.hasSnoozed ctx.UserId (ctx.Now ()) ctx.Db | None -> Task.FromResult false return { IsAuthenticated = Option.isSome ctx.CurrentUser @@ -238,7 +238,7 @@ module Models = } -open MyPrayerJournal.Data.Extensions +open MyPrayerJournal.LiteData.Extensions open NodaTime.Text /// Handlers for less-than-full-page HTML requests @@ -254,14 +254,14 @@ module Components = | Some snooze, _ when snooze < now -> true | _, Some hide when hide < now -> true | _, _ -> false - let! journal = Data.journalByUserId ctx.UserId ctx.Db + let! journal = LiteData.journalByUserId ctx.UserId ctx.Db let shown = journal |> List.filter shouldBeShown return! renderComponent [ Views.Journal.journalItems now ctx.TimeZone shown ] next ctx } // GET /components/request-item/[req-id] let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task { - match! Data.tryJournalById (RequestId.ofString reqId) ctx.UserId ctx.Db with + match! LiteData.tryJournalById (RequestId.ofString reqId) ctx.UserId ctx.Db with | Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) ctx.TimeZone req ] next ctx | None -> return! Error.notFound next ctx } @@ -272,7 +272,7 @@ module Components = // GET /components/request/[req-id]/notes let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let! notes = Data.notesById (RequestId.ofString requestId) ctx.UserId ctx.Db + let! notes = LiteData.notesById (RequestId.ofString requestId) ctx.UserId ctx.Db return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone (List.ofArray notes)) next ctx } @@ -333,7 +333,7 @@ module Request = return! partial "Add Prayer Request" (Views.Request.edit (JournalRequest.ofRequestLite Request.empty) returnTo true) next ctx | _ -> - match! Data.tryJournalById (RequestId.ofString requestId) ctx.UserId ctx.Db with + match! LiteData.tryJournalById (RequestId.ofString requestId) ctx.UserId ctx.Db with | Some req -> debug ctx "Found - sending view" return! partial "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx @@ -347,15 +347,15 @@ module Request = let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! Data.tryRequestById reqId userId db with + match! LiteData.tryRequestById reqId userId db with | Some req -> let now = ctx.Now () - do! Data.addHistory reqId userId { AsOf = now; Status = Prayed; Text = None } db + do! LiteData.addHistory reqId userId { AsOf = now; Status = Prayed; Text = None } db let nextShow = match Recurrence.duration req.Recurrence with | 0L -> None | duration -> Some <| now.Plus (Duration.FromSeconds duration) - do! Data.updateShowAfter reqId userId nextShow db + do! LiteData.updateShowAfter reqId userId nextShow db do! db.SaveChanges () return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx | None -> return! Error.notFound next ctx @@ -366,10 +366,10 @@ module Request = let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! Data.tryRequestById reqId userId db with + match! LiteData.tryRequestById reqId userId db with | Some _ -> let! notes = ctx.BindFormAsync () - do! Data.addNote reqId userId { AsOf = ctx.Now (); Notes = notes.notes } db + do! LiteData.addNote reqId userId { AsOf = ctx.Now (); Notes = notes.notes } db do! db.SaveChanges () return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx | None -> return! Error.notFound next ctx @@ -377,13 +377,13 @@ module Request = // GET /requests/active let active : HttpHandler = requireUser >=> fun next ctx -> task { - let! reqs = Data.journalByUserId ctx.UserId ctx.Db + let! reqs = LiteData.journalByUserId ctx.UserId ctx.Db return! partial "Active Requests" (Views.Request.active (ctx.Now ()) ctx.TimeZone reqs) next ctx } // GET /requests/snoozed let snoozed : HttpHandler = requireUser >=> fun next ctx -> task { - let! reqs = Data.journalByUserId ctx.UserId ctx.Db + let! reqs = LiteData.journalByUserId ctx.UserId ctx.Db let now = ctx.Now () let snoozed = reqs |> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false) @@ -392,13 +392,13 @@ module Request = // GET /requests/answered let answered : HttpHandler = requireUser >=> fun next ctx -> task { - let! reqs = Data.answeredRequests ctx.UserId ctx.Db + let! reqs = LiteData.answeredRequests ctx.UserId ctx.Db return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) ctx.TimeZone reqs) next ctx } // GET /request/[req-id]/full let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task { - match! Data.tryFullRequestById (RequestId.ofString requestId) ctx.UserId ctx.Db with + match! LiteData.tryFullRequestById (RequestId.ofString requestId) ctx.UserId ctx.Db with | Some req -> return! partial "Prayer Request" (Views.Request.full ctx.Clock ctx.TimeZone req) next ctx | None -> return! Error.notFound next ctx } @@ -408,9 +408,9 @@ module Request = let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! Data.tryRequestById reqId userId db with + match! LiteData.tryRequestById reqId userId db with | Some _ -> - do! Data.updateShowAfter reqId userId None db + do! LiteData.updateShowAfter reqId userId None db do! db.SaveChanges () return! (withSuccessMessage "Request now shown" >=> Components.requestItem requestId) next ctx | None -> return! Error.notFound next ctx @@ -421,14 +421,14 @@ module Request = let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! Data.tryRequestById reqId userId db with + match! LiteData.tryRequestById reqId userId db with | Some _ -> let! until = ctx.BindFormAsync () let date = LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd").Parse(until.until).Value .AtStartOfDayInZone(DateTimeZone.Utc) .ToInstant () - do! Data.updateSnoozed reqId userId (Some date) db + do! LiteData.updateSnoozed reqId userId (Some date) db do! db.SaveChanges () return! (withSuccessMessage $"Request snoozed until {until.until}" @@ -442,9 +442,9 @@ module Request = let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! Data.tryRequestById reqId userId db with + match! LiteData.tryRequestById reqId userId db with | Some _ -> - do! Data.updateSnoozed reqId userId None db + do! LiteData.updateSnoozed reqId userId None db do! db.SaveChanges () return! (withSuccessMessage "Request unsnoozed" >=> Components.requestItem requestId) next ctx | None -> return! Error.notFound next ctx @@ -475,7 +475,7 @@ module Request = } |] } - Data.addRequest req db + LiteData.addRequest req db do! db.SaveChanges () Messages.pushSuccess ctx "Added prayer request" "/journal" return! seeOther "/journal" next ctx @@ -486,21 +486,21 @@ module Request = let! form = ctx.BindModelAsync () let db = ctx.Db let userId = ctx.UserId - match! Data.tryJournalById (RequestId.ofString form.requestId) userId db with + match! LiteData.tryJournalById (RequestId.ofString form.requestId) userId db with | Some req -> // update recurrence if changed let recur = parseRecurrence form match recur = req.Recurrence with | true -> () | false -> - do! Data.updateRecurrence req.RequestId userId recur db + do! LiteData.updateRecurrence req.RequestId userId recur db match recur with - | Immediate -> do! Data.updateShowAfter req.RequestId userId None db + | Immediate -> do! LiteData.updateShowAfter req.RequestId userId None db | _ -> () // append history let upd8Text = form.requestText.Trim () let text = if upd8Text = req.Text then None else Some upd8Text - do! Data.addHistory req.RequestId userId + do! LiteData.addHistory req.RequestId userId { AsOf = ctx.Now (); Status = (Option.get >> RequestAction.ofString) form.status; Text = text } db do! db.SaveChanges () let nextUrl = diff --git a/src/MyPrayerJournal/Data.fs b/src/MyPrayerJournal/LiteData.fs similarity index 99% rename from src/MyPrayerJournal/Data.fs rename to src/MyPrayerJournal/LiteData.fs index 6af5797..b930f3f 100644 --- a/src/MyPrayerJournal/Data.fs +++ b/src/MyPrayerJournal/LiteData.fs @@ -1,4 +1,4 @@ -module MyPrayerJournal.Data +module MyPrayerJournal.LiteData open LiteDB open MyPrayerJournal diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index e0b999f..c0f892b 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -8,7 +8,7 @@ - + diff --git a/src/MyPrayerJournal/Program.fs b/src/MyPrayerJournal/Program.fs index 134068d..70ed17a 100644 --- a/src/MyPrayerJournal/Program.fs +++ b/src/MyPrayerJournal/Program.fs @@ -131,7 +131,7 @@ module Configure = let jsonOptions = JsonSerializerOptions () jsonOptions.Converters.Add (JsonFSharpConverter ()) let db = new LiteDatabase (bldr.Configuration.GetConnectionString "db") - Data.Startup.ensureDb db + LiteData.Startup.ensureDb db let _ = bldr.Services.AddSingleton jsonOptions let _ = bldr.Services.AddSingleton () let _ = bldr.Services.AddSingleton db -- 2.45.1 From 121cde0db68fd5ced459aa96d153fc18130c33fa Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 7 Oct 2023 15:02:06 -0400 Subject: [PATCH 02/11] WIP on PostgreSQL doc db --- .../MyPrayerJournal.ToPostgres.fsproj | 16 ++ src/MyPrayerJournal.ToPostgres/Program.fs | 2 + src/MyPrayerJournal/Data.fs | 146 ++++++++++++++++++ src/MyPrayerJournal/MyPrayerJournal.fsproj | 2 + 4 files changed, 166 insertions(+) create mode 100644 src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj create mode 100644 src/MyPrayerJournal.ToPostgres/Program.fs create mode 100644 src/MyPrayerJournal/Data.fs diff --git a/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj b/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj new file mode 100644 index 0000000..7913b82 --- /dev/null +++ b/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj @@ -0,0 +1,16 @@ + + + + Exe + net7.0 + + + + + + + + + + + diff --git a/src/MyPrayerJournal.ToPostgres/Program.fs b/src/MyPrayerJournal.ToPostgres/Program.fs new file mode 100644 index 0000000..d6818ab --- /dev/null +++ b/src/MyPrayerJournal.ToPostgres/Program.fs @@ -0,0 +1,2 @@ +// For more information see https://aka.ms/fsharp-console-apps +printfn "Hello from F#" diff --git a/src/MyPrayerJournal/Data.fs b/src/MyPrayerJournal/Data.fs new file mode 100644 index 0000000..51cda0b --- /dev/null +++ b/src/MyPrayerJournal/Data.fs @@ -0,0 +1,146 @@ +module MyPrayerJournal.Data + +/// Table(s) used by myPrayerJournal +module Table = + + /// Requests + [] + let Request = "mpj.request" + + +open BitBadger.Npgsql.FSharp.Documents + +module DataConnection = + + let ensureDb () = backgroundTask { + do! Custom.nonQuery "CREATE SCHEMA IF NOT EXISTS mpj" [] + do! Definition.ensureTable Table.Request + do! Definition.ensureIndex Table.Request Optimized + } + + +/// Data access functions for requests +[] +module Request = + + open NodaTime + + /// Add a request + let add req = backgroundTask { + do! insert Table.Request (RequestId.toString req.Id) req + } + + /// Retrieve a request by its ID and user ID (includes history and notes) + let tryByIdFull reqId userId = backgroundTask { + match! Find.byId Table.Request (RequestId.toString reqId) with + | Some req when req.UserId = userId -> return Some req + | _ -> return None + } + + /// Retrieve a request by its ID and user ID (excludes history and notes) + let tryById reqId userId = backgroundTask { + match! tryByIdFull reqId userId with + | Some req -> return Some { req with History = [||]; Notes = [||] } + | None -> return None + } + + /// Does a request exist for the given request ID and user ID? + let private existsById (reqId : RequestId) (userId : UserId) = + Exists.byContains Table.Request {| Id = reqId; UserId = userId |} + + /// Update recurrence for a request + let updateRecurrence reqId userId (recurType : Recurrence) = backgroundTask { + let dbId = RequestId.toString reqId + match! existsById reqId userId with + | true -> do! Update.partialById Table.Request dbId {| Recurrence = recurType |} + | false -> invalidOp "Request ID {dbId} not found" + } + + /// Update the show-after time for a request + let updateShowAfter reqId userId (showAfter : Instant) = backgroundTask { + let dbId = RequestId.toString reqId + match! existsById reqId userId with + | true -> do! Update.partialById Table.Request dbId {| ShowAfter = showAfter |} + | false -> invalidOp "Request ID {dbId} not found" + } + + /// Update the snoozed and show-after values for a request + let updateSnoozed reqId userId (until : Instant) = backgroundTask { + let dbId = RequestId.toString reqId + match! existsById reqId userId with + | true -> do! Update.partialById Table.Request dbId {| SnoozedUntil = until; ShowAfter = until |} + | false -> invalidOp "Request ID {dbId} not found" + } + + +/// Specific manipulation of history entries +[] +module History = + + /// Add a history entry + let add reqId userId hist = backgroundTask { + let dbId = RequestId.toString reqId + match! Request.tryByIdFull reqId userId with + | Some req -> do! Update.partialById Table.Request dbId {| History = Array.append [| hist |] req.History |} + | None -> invalidOp $"Request ID {dbId} not found" + } + + +/// Data access functions for journal-style requests +[] +module Journal = + + /// Retrieve a user's answered requests + let answered userId = backgroundTask { + // TODO: only retrieve answered requests + let! reqs = Find.byContains Table.Request {| UserId = UserId.toString userId |} + return + reqs + |> Seq.ofList + |> Seq.map JournalRequest.ofRequestFull + |> Seq.filter (fun it -> it.LastStatus = Answered) + |> Seq.sortByDescending (fun it -> it.AsOf) + |> List.ofSeq + } + + /// Retrieve a user's current prayer journal (includes snoozed and non-immediate recurrence) + let forUser userId = backgroundTask { + // TODO: only retrieve unanswered requests + let! reqs = Find.byContains Table.Request {| UserId = UserId.toString userId |} + return + reqs + |> Seq.ofList + |> Seq.map JournalRequest.ofRequestFull + |> Seq.filter (fun it -> it.LastStatus = Answered) + |> Seq.sortByDescending (fun it -> it.AsOf) + |> List.ofSeq + } + + /// Does the user's journal have any snoozed requests? + let hasSnoozed userId now = backgroundTask { + let! jrnl = forUser userId + return jrnl |> List.exists (fun r -> defaultArg (r.SnoozedUntil |> Option.map (fun it -> it > now)) false) + } + + let tryById reqId userId = backgroundTask { + let! req = Request.tryById reqId userId + return req |> Option.map JournalRequest.ofRequestLite + } + + +/// Specific manipulation of note entries +[] +module Note = + + /// Add a note + let add reqId userId note = backgroundTask { + let dbId = RequestId.toString reqId + match! Request.tryByIdFull reqId userId with + | Some req -> do! Update.partialById Table.Request dbId {| Notes = Array.append [| note |] req.Notes |} + | None -> invalidOp $"Request ID {dbId} not found" + } + + /// Retrieve notes for a request by the request ID + let byRequestId reqId userId = backgroundTask { + match! Request.tryByIdFull reqId userId with Some req -> return req.Notes | None -> return [||] + } diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index c0f892b..3616f90 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -9,6 +9,7 @@ + @@ -19,6 +20,7 @@ + -- 2.45.1 From cc4347bc6ede635efbaf417bf51d0876e2586d60 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 7 Oct 2023 16:19:23 -0400 Subject: [PATCH 03/11] Replace LiteDB calls with Pg doc calls --- src/MyPrayerJournal/Data.fs | 71 ++++++++++++-- src/MyPrayerJournal/Handlers.fs | 102 +++++++++------------ src/MyPrayerJournal/MyPrayerJournal.fsproj | 11 ++- src/MyPrayerJournal/Program.fs | 15 +-- 4 files changed, 116 insertions(+), 83 deletions(-) diff --git a/src/MyPrayerJournal/Data.fs b/src/MyPrayerJournal/Data.fs index 51cda0b..6380765 100644 --- a/src/MyPrayerJournal/Data.fs +++ b/src/MyPrayerJournal/Data.fs @@ -1,6 +1,6 @@ module MyPrayerJournal.Data -/// Table(s) used by myPrayerJournal +/// Table(!) used by myPrayerJournal module Table = /// Requests @@ -8,16 +8,69 @@ module Table = let Request = "mpj.request" +/// JSON serialization customizations +[] +module Json = + + open System.Text.Json.Serialization + + /// Convert a wrapped DU to/from its string representation + type WrappedJsonConverter<'T> (wrap : string -> 'T, unwrap : 'T -> string) = + inherit JsonConverter<'T> () + override _.Read(reader, _, _) = + wrap (reader.GetString ()) + override _.Write(writer, value, _) = + writer.WriteStringValue (unwrap value) + + open System.Text.Json + open NodaTime.Serialization.SystemTextJson + + /// JSON serializer options to support the target domain + let options = + let opts = JsonSerializerOptions () + [ WrappedJsonConverter (Recurrence.ofString, Recurrence.toString) :> JsonConverter + WrappedJsonConverter (RequestAction.ofString, RequestAction.toString) + WrappedJsonConverter (RequestId.ofString, RequestId.toString) + WrappedJsonConverter (UserId, UserId.toString) + JsonFSharpConverter () + ] + |> List.iter opts.Converters.Add + let _ = opts.ConfigureForNodaTime NodaTime.DateTimeZoneProviders.Tzdb + opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + opts + + open BitBadger.Npgsql.FSharp.Documents -module DataConnection = +/// Connection +[] +module Connection = - let ensureDb () = backgroundTask { + open BitBadger.Npgsql.Documents + open Microsoft.Extensions.Configuration + open Npgsql + open System.Text.Json + + /// Ensure the database is ready to use + let private ensureDb () = backgroundTask { do! Custom.nonQuery "CREATE SCHEMA IF NOT EXISTS mpj" [] do! Definition.ensureTable Table.Request do! Definition.ensureIndex Table.Request Optimized } + /// Set up the data environment + let setUp (cfg : IConfiguration) = backgroundTask { + let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "mpj") + let _ = builder.UseNodaTime () + Configuration.useDataSource (builder.Build ()) + Configuration.useSerializer + { new IDocumentSerializer with + member _.Serialize<'T> (it : 'T) = JsonSerializer.Serialize (it, Json.options) + member _.Deserialize<'T> (it : string) = JsonSerializer.Deserialize<'T> (it, Json.options) + } + do! ensureDb () + } + /// Data access functions for requests [] @@ -30,6 +83,10 @@ module Request = do! insert Table.Request (RequestId.toString req.Id) req } + /// Does a request exist for the given request ID and user ID? + let existsById (reqId : RequestId) (userId : UserId) = + Exists.byContains Table.Request {| Id = reqId; UserId = userId |} + /// Retrieve a request by its ID and user ID (includes history and notes) let tryByIdFull reqId userId = backgroundTask { match! Find.byId Table.Request (RequestId.toString reqId) with @@ -44,10 +101,6 @@ module Request = | None -> return None } - /// Does a request exist for the given request ID and user ID? - let private existsById (reqId : RequestId) (userId : UserId) = - Exists.byContains Table.Request {| Id = reqId; UserId = userId |} - /// Update recurrence for a request let updateRecurrence reqId userId (recurType : Recurrence) = backgroundTask { let dbId = RequestId.toString reqId @@ -57,7 +110,7 @@ module Request = } /// Update the show-after time for a request - let updateShowAfter reqId userId (showAfter : Instant) = backgroundTask { + let updateShowAfter reqId userId (showAfter : Instant option) = backgroundTask { let dbId = RequestId.toString reqId match! existsById reqId userId with | true -> do! Update.partialById Table.Request dbId {| ShowAfter = showAfter |} @@ -65,7 +118,7 @@ module Request = } /// Update the snoozed and show-after values for a request - let updateSnoozed reqId userId (until : Instant) = backgroundTask { + let updateSnoozed reqId userId (until : Instant option) = backgroundTask { let dbId = RequestId.toString reqId match! existsById reqId userId with | true -> do! Update.partialById Table.Request dbId {| SnoozedUntil = until; ShowAfter = until |} diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs index 79d92da..c81f837 100644 --- a/src/MyPrayerJournal/Handlers.fs +++ b/src/MyPrayerJournal/Handlers.fs @@ -45,16 +45,12 @@ module Error = open System.Security.Claims -open LiteDB open Microsoft.AspNetCore.Http open NodaTime /// Extensions on the HTTP context type HttpContext with - /// The LiteDB database - member this.Db = this.GetService () - /// The "sub" for the current user (None if no user is authenticated) member this.CurrentUser = this.User @@ -83,6 +79,8 @@ type HttpContext with | None -> DateTimeZone.Utc +open MyPrayerJournal.Data + /// Handler helpers [] module private Helpers = @@ -127,7 +125,7 @@ module private Helpers = let pageContext (ctx : HttpContext) pageTitle content = backgroundTask { let! hasSnoozed = match ctx.CurrentUser with - | Some _ -> LiteData.hasSnoozed ctx.UserId (ctx.Now ()) ctx.Db + | Some _ -> Journal.hasSnoozed ctx.UserId (ctx.Now ()) | None -> Task.FromResult false return { IsAuthenticated = Option.isSome ctx.CurrentUser @@ -155,17 +153,17 @@ module private Helpers = /// Push a new message into the list let push (ctx : HttpContext) message url = lock upd8 (fun () -> - messages <- messages.Add (ctx.UserId, (message, url))) + messages <- messages.Add (ctx.UserId, (message, url))) /// Add a success message header to the response let pushSuccess ctx message url = - push ctx $"success|||%s{message}" url + push ctx $"success|||%s{message}" url /// Pop the messages for the given user let pop userId = lock upd8 (fun () -> - let msg = messages.TryFind userId - msg |> Option.iter (fun _ -> messages <- messages.Remove userId) - msg) + let msg = messages.TryFind userId + msg |> Option.iter (fun _ -> messages <- messages.Remove userId) + msg) /// Send a partial result if this is not a full page load (does not append no-cache headers) let partialStatic (pageTitle : string) content : HttpHandler = fun next ctx -> task { @@ -238,7 +236,6 @@ module Models = } -open MyPrayerJournal.LiteData.Extensions open NodaTime.Text /// Handlers for less-than-full-page HTML requests @@ -254,14 +251,14 @@ module Components = | Some snooze, _ when snooze < now -> true | _, Some hide when hide < now -> true | _, _ -> false - let! journal = LiteData.journalByUserId ctx.UserId ctx.Db + let! journal = Journal.forUser ctx.UserId let shown = journal |> List.filter shouldBeShown return! renderComponent [ Views.Journal.journalItems now ctx.TimeZone shown ] next ctx } // GET /components/request-item/[req-id] let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task { - match! LiteData.tryJournalById (RequestId.ofString reqId) ctx.UserId ctx.Db with + match! Journal.tryById (RequestId.ofString reqId) ctx.UserId with | Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) ctx.TimeZone req ] next ctx | None -> return! Error.notFound next ctx } @@ -272,7 +269,7 @@ module Components = // GET /components/request/[req-id]/notes let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let! notes = LiteData.notesById (RequestId.ofString requestId) ctx.UserId ctx.Db + let! notes = Note.byRequestId (RequestId.ofString requestId) ctx.UserId return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone (List.ofArray notes)) next ctx } @@ -333,7 +330,7 @@ module Request = return! partial "Add Prayer Request" (Views.Request.edit (JournalRequest.ofRequestLite Request.empty) returnTo true) next ctx | _ -> - match! LiteData.tryJournalById (RequestId.ofString requestId) ctx.UserId ctx.Db with + match! Journal.tryById (RequestId.ofString requestId) ctx.UserId with | Some req -> debug ctx "Found - sending view" return! partial "Edit Prayer Request" (Views.Request.edit req returnTo false) next ctx @@ -344,46 +341,42 @@ module Request = // PATCH /request/[req-id]/prayed let prayed requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! LiteData.tryRequestById reqId userId db with + match! Journal.tryById reqId userId with | Some req -> let now = ctx.Now () - do! LiteData.addHistory reqId userId { AsOf = now; Status = Prayed; Text = None } db + do! History.add reqId userId { AsOf = now; Status = Prayed; Text = None } let nextShow = match Recurrence.duration req.Recurrence with | 0L -> None | duration -> Some <| now.Plus (Duration.FromSeconds duration) - do! LiteData.updateShowAfter reqId userId nextShow db - do! db.SaveChanges () + do! Request.updateShowAfter reqId userId nextShow return! (withSuccessMessage "Request marked as prayed" >=> Components.journalItems) next ctx | None -> return! Error.notFound next ctx } - /// POST /request/[req-id]/note + // POST /request/[req-id]/note let addNote requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! LiteData.tryRequestById reqId userId db with - | Some _ -> + match! Request.existsById reqId userId with + | true -> let! notes = ctx.BindFormAsync () - do! LiteData.addNote reqId userId { AsOf = ctx.Now (); Notes = notes.notes } db - do! db.SaveChanges () + do! Note.add reqId userId { AsOf = ctx.Now (); Notes = notes.notes } return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx - | None -> return! Error.notFound next ctx + | false -> return! Error.notFound next ctx } // GET /requests/active let active : HttpHandler = requireUser >=> fun next ctx -> task { - let! reqs = LiteData.journalByUserId ctx.UserId ctx.Db + let! reqs = Journal.forUser ctx.UserId return! partial "Active Requests" (Views.Request.active (ctx.Now ()) ctx.TimeZone reqs) next ctx } // GET /requests/snoozed let snoozed : HttpHandler = requireUser >=> fun next ctx -> task { - let! reqs = LiteData.journalByUserId ctx.UserId ctx.Db + let! reqs = Journal.forUser ctx.UserId let now = ctx.Now () let snoozed = reqs |> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false) @@ -392,62 +385,56 @@ module Request = // GET /requests/answered let answered : HttpHandler = requireUser >=> fun next ctx -> task { - let! reqs = LiteData.answeredRequests ctx.UserId ctx.Db + let! reqs = Journal.answered ctx.UserId return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) ctx.TimeZone reqs) next ctx } // GET /request/[req-id]/full let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task { - match! LiteData.tryFullRequestById (RequestId.ofString requestId) ctx.UserId ctx.Db with + match! Request.tryByIdFull (RequestId.ofString requestId) ctx.UserId with | Some req -> return! partial "Prayer Request" (Views.Request.full ctx.Clock ctx.TimeZone req) next ctx | None -> return! Error.notFound next ctx } // PATCH /request/[req-id]/show let show requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! LiteData.tryRequestById reqId userId db with - | Some _ -> - do! LiteData.updateShowAfter reqId userId None db - do! db.SaveChanges () + match! Request.existsById reqId userId with + | true -> + do! Request.updateShowAfter reqId userId None return! (withSuccessMessage "Request now shown" >=> Components.requestItem requestId) next ctx - | None -> return! Error.notFound next ctx + | false -> return! Error.notFound next ctx } // PATCH /request/[req-id]/snooze let snooze requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! LiteData.tryRequestById reqId userId db with - | Some _ -> + match! Request.existsById reqId userId with + | true -> let! until = ctx.BindFormAsync () let date = LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd").Parse(until.until).Value .AtStartOfDayInZone(DateTimeZone.Utc) .ToInstant () - do! LiteData.updateSnoozed reqId userId (Some date) db - do! db.SaveChanges () + do! Request.updateSnoozed reqId userId (Some date) return! (withSuccessMessage $"Request snoozed until {until.until}" >=> hideModal "snooze" >=> Components.journalItems) next ctx - | None -> return! Error.notFound next ctx + | false -> return! Error.notFound next ctx } // PATCH /request/[req-id]/cancel-snooze let cancelSnooze requestId : HttpHandler = requireUser >=> fun next ctx -> task { - let db = ctx.Db let userId = ctx.UserId let reqId = RequestId.ofString requestId - match! LiteData.tryRequestById reqId userId db with - | Some _ -> - do! LiteData.updateSnoozed reqId userId None db - do! db.SaveChanges () + match! Request.existsById reqId userId with + | true -> + do! Request.updateSnoozed reqId userId None return! (withSuccessMessage "Request unsnoozed" >=> Components.requestItem requestId) next ctx - | None -> return! Error.notFound next ctx + | false -> return! Error.notFound next ctx } /// Derive a recurrence from its representation in the form @@ -458,7 +445,6 @@ module Request = // POST /request let add : HttpHandler = requireUser >=> fun next ctx -> task { let! form = ctx.BindModelAsync () - let db = ctx.Db let userId = ctx.UserId let now = ctx.Now () let req = @@ -475,8 +461,7 @@ module Request = } |] } - LiteData.addRequest req db - do! db.SaveChanges () + do! Request.add req Messages.pushSuccess ctx "Added prayer request" "/journal" return! seeOther "/journal" next ctx } @@ -484,25 +469,24 @@ module Request = // PATCH /request let update : HttpHandler = requireUser >=> fun next ctx -> task { let! form = ctx.BindModelAsync () - let db = ctx.Db let userId = ctx.UserId - match! LiteData.tryJournalById (RequestId.ofString form.requestId) userId db with + // TODO: update the instance and save rather than all these little updates + match! Journal.tryById (RequestId.ofString form.requestId) userId with | Some req -> // update recurrence if changed let recur = parseRecurrence form match recur = req.Recurrence with | true -> () | false -> - do! LiteData.updateRecurrence req.RequestId userId recur db + do! Request.updateRecurrence req.RequestId userId recur match recur with - | Immediate -> do! LiteData.updateShowAfter req.RequestId userId None db + | Immediate -> do! Request.updateShowAfter req.RequestId userId None | _ -> () // append history let upd8Text = form.requestText.Trim () let text = if upd8Text = req.Text then None else Some upd8Text - do! LiteData.addHistory req.RequestId userId - { AsOf = ctx.Now (); Status = (Option.get >> RequestAction.ofString) form.status; Text = text } db - do! db.SaveChanges () + do! History.add req.RequestId userId + { AsOf = ctx.Now (); Status = (Option.get >> RequestAction.ofString) form.status; Text = text } let nextUrl = match form.returnTo with | "active" -> "/requests/active" diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index 3616f90..20289fa 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -21,15 +21,16 @@ - + - - + + - - + + + diff --git a/src/MyPrayerJournal/Program.fs b/src/MyPrayerJournal/Program.fs index 70ed17a..0181106 100644 --- a/src/MyPrayerJournal/Program.fs +++ b/src/MyPrayerJournal/Program.fs @@ -20,7 +20,7 @@ module Configure = .SetBasePath(bldr.Environment.ContentRootPath) .AddJsonFile("appsettings.json", optional = false, reloadOnChange = true) .AddJsonFile($"appsettings.{bldr.Environment.EnvironmentName}.json", optional = true, reloadOnChange = true) - .AddEnvironmentVariables () + .AddEnvironmentVariables "MPJ_" |> ignore bldr @@ -53,16 +53,15 @@ module Configure = open Giraffe - open LiteDB open Microsoft.AspNetCore.Authentication.Cookies open Microsoft.AspNetCore.Authentication.OpenIdConnect open Microsoft.AspNetCore.Http open Microsoft.Extensions.DependencyInjection open Microsoft.IdentityModel.Protocols.OpenIdConnect + open MyPrayerJournal.Data open NodaTime open System open System.Text.Json - open System.Text.Json.Serialization open System.Threading.Tasks /// Configure dependency injection @@ -128,13 +127,9 @@ module Configure = ctx.ProtocolMessage.RedirectUri <- string bldr Task.CompletedTask) - let jsonOptions = JsonSerializerOptions () - jsonOptions.Converters.Add (JsonFSharpConverter ()) - let db = new LiteDatabase (bldr.Configuration.GetConnectionString "db") - LiteData.Startup.ensureDb db - let _ = bldr.Services.AddSingleton jsonOptions - let _ = bldr.Services.AddSingleton () - let _ = bldr.Services.AddSingleton db + let _ = bldr.Services.AddSingleton Json.options + let _ = bldr.Services.AddSingleton (SystemTextJson.Serializer Json.options) + let _ = Connection.setUp bldr.Configuration |> Async.AwaitTask |> Async.RunSynchronously bldr.Build () -- 2.45.1 From 399b15db9ccea4a368858426758e29aacb033ffe Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 7 Oct 2023 19:37:44 -0400 Subject: [PATCH 04/11] WIP on data migration --- src/MyPrayerJournal.ToPostgres/LiteData.fs | 106 ++++++++++ .../MyPrayerJournal.ToPostgres.fsproj | 7 + src/MyPrayerJournal.ToPostgres/Program.fs | 11 +- src/MyPrayerJournal.sln | 6 +- src/MyPrayerJournal/Data.fs | 12 +- src/MyPrayerJournal/Domain.fs | 29 ++- src/MyPrayerJournal/Handlers.fs | 6 +- src/MyPrayerJournal/LiteData.fs | 199 ------------------ src/MyPrayerJournal/MyPrayerJournal.fsproj | 5 +- src/MyPrayerJournal/Views/Request.fs | 27 +-- 10 files changed, 168 insertions(+), 240 deletions(-) create mode 100644 src/MyPrayerJournal.ToPostgres/LiteData.fs delete mode 100644 src/MyPrayerJournal/LiteData.fs diff --git a/src/MyPrayerJournal.ToPostgres/LiteData.fs b/src/MyPrayerJournal.ToPostgres/LiteData.fs new file mode 100644 index 0000000..c7277b2 --- /dev/null +++ b/src/MyPrayerJournal.ToPostgres/LiteData.fs @@ -0,0 +1,106 @@ +module MyPrayerJournal.LiteData + +open LiteDB +open MyPrayerJournal +open NodaTime + +/// Request is the identifying record for a prayer request +[] +type OldRequest = + { /// The ID of the request + Id : RequestId + + /// The time this request was initially entered + EnteredOn : Instant + + /// The ID of the user to whom this request belongs ("sub" from the JWT) + UserId : UserId + + /// The time at which this request should reappear in the user's journal by manual user choice + SnoozedUntil : Instant option + + /// The time at which this request should reappear in the user's journal by recurrence + ShowAfter : Instant option + + /// The recurrence for this request + Recurrence : Recurrence + + /// The history entries for this request + History : History[] + + /// The notes for this request + Notes : Note[] + } + + +/// LiteDB extensions +[] +module Extensions = + + /// Extensions on the LiteDatabase class + type LiteDatabase with + + /// The Request collection + member this.Requests = this.GetCollection "request" + + +/// Map domain to LiteDB +// It does mapping, but since we're so DU-heavy, this gives us control over the JSON representation +[] +module Mapping = + + open NodaTime.Text + + /// A NodaTime instant pattern to use for parsing instants from the database + let instantPattern = InstantPattern.CreateWithInvariantCulture "g" + + /// Mapping for NodaTime's Instant type + module Instant = + let fromBson (value : BsonValue) = (instantPattern.Parse value.AsString).Value + let toBson (value : Instant) : BsonValue = value.ToString ("g", null) + + /// Mapping for option types + module Option = + let instantFromBson (value : BsonValue) = if value.IsNull then None else Some (Instant.fromBson value) + let instantToBson (value : Instant option) = match value with Some it -> Instant.toBson it | None -> null + + let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x + let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> "" + + /// Mapping for Recurrence + module Recurrence = + let fromBson (value : BsonValue) = Recurrence.ofString value + let toBson (value : Recurrence) : BsonValue = Recurrence.toString value + + /// Mapping for RequestAction + module RequestAction = + let fromBson (value : BsonValue) = RequestAction.ofString value.AsString + let toBson (value : RequestAction) : BsonValue = RequestAction.toString value + + /// Mapping for RequestId + module RequestId = + let fromBson (value : BsonValue) = RequestId.ofString value.AsString + let toBson (value : RequestId) : BsonValue = RequestId.toString value + + /// Mapping for UserId + module UserId = + let fromBson (value : BsonValue) = UserId value.AsString + let toBson (value : UserId) : BsonValue = UserId.toString value + + /// Set up the mapping + let register () = + BsonMapper.Global.RegisterType(Instant.toBson, Instant.fromBson) + BsonMapper.Global.RegisterType(Option.instantToBson, Option.instantFromBson) + BsonMapper.Global.RegisterType(Recurrence.toBson, Recurrence.fromBson) + BsonMapper.Global.RegisterType(RequestAction.toBson, RequestAction.fromBson) + BsonMapper.Global.RegisterType(RequestId.toBson, RequestId.fromBson) + BsonMapper.Global.RegisterType(Option.stringToBson, Option.stringFromBson) + BsonMapper.Global.RegisterType(UserId.toBson, UserId.fromBson) + +/// Code to be run at startup +module Startup = + + /// Ensure the database is set up + let ensureDb (db : LiteDatabase) = + db.Requests.EnsureIndex (fun it -> it.UserId) |> ignore + Mapping.register () diff --git a/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj b/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj index 7913b82..018a50c 100644 --- a/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj +++ b/src/MyPrayerJournal.ToPostgres/MyPrayerJournal.ToPostgres.fsproj @@ -3,9 +3,11 @@ Exe net7.0 + 3391 + @@ -13,4 +15,9 @@ + + + + + diff --git a/src/MyPrayerJournal.ToPostgres/Program.fs b/src/MyPrayerJournal.ToPostgres/Program.fs index d6818ab..d1b4ad3 100644 --- a/src/MyPrayerJournal.ToPostgres/Program.fs +++ b/src/MyPrayerJournal.ToPostgres/Program.fs @@ -1,2 +1,9 @@ -// For more information see https://aka.ms/fsharp-console-apps -printfn "Hello from F#" +open LiteDB +open MyPrayerJournal.Domain +open MyPrayerJournal.LiteData + + +let lite = new LiteDatabase "Filename=./mpj.db" +Startup.ensureDb lite + + diff --git a/src/MyPrayerJournal.sln b/src/MyPrayerJournal.sln index 393866a..fdc0553 100644 --- a/src/MyPrayerJournal.sln +++ b/src/MyPrayerJournal.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 16.0.30114.105 MinimumVisualStudioVersion = 10.0.40219.1 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal", "MyPrayerJournal\MyPrayerJournal.fsproj", "{6BD5A3C8-F859-42A0-ACD7-A5819385E828}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal.ConvertRecurrence", "MyPrayerJournal.ConvertRecurrence\MyPrayerJournal.ConvertRecurrence.fsproj", "{72B57736-8721-4636-A309-49FA4222416E}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyPrayerJournal.ToPostgres", "MyPrayerJournal.ToPostgres\MyPrayerJournal.ToPostgres.fsproj", "{3114B8F4-E388-4804-94D3-A2F4D42797C6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -24,5 +24,9 @@ Global {72B57736-8721-4636-A309-49FA4222416E}.Debug|Any CPU.Build.0 = Debug|Any CPU {72B57736-8721-4636-A309-49FA4222416E}.Release|Any CPU.ActiveCfg = Release|Any CPU {72B57736-8721-4636-A309-49FA4222416E}.Release|Any CPU.Build.0 = Release|Any CPU + {3114B8F4-E388-4804-94D3-A2F4D42797C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3114B8F4-E388-4804-94D3-A2F4D42797C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3114B8F4-E388-4804-94D3-A2F4D42797C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3114B8F4-E388-4804-94D3-A2F4D42797C6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/MyPrayerJournal/Data.fs b/src/MyPrayerJournal/Data.fs index 6380765..a8102a3 100644 --- a/src/MyPrayerJournal/Data.fs +++ b/src/MyPrayerJournal/Data.fs @@ -97,7 +97,7 @@ module Request = /// Retrieve a request by its ID and user ID (excludes history and notes) let tryById reqId userId = backgroundTask { match! tryByIdFull reqId userId with - | Some req -> return Some { req with History = [||]; Notes = [||] } + | Some req -> return Some { req with History = []; Notes = [] } | None -> return None } @@ -134,7 +134,9 @@ module History = let add reqId userId hist = backgroundTask { let dbId = RequestId.toString reqId match! Request.tryByIdFull reqId userId with - | Some req -> do! Update.partialById Table.Request dbId {| History = Array.append [| hist |] req.History |} + | Some req -> + do! Update.partialById Table.Request dbId + {| History = (hist :: req.History) |> List.sortByDescending (fun it -> it.AsOf) |} | None -> invalidOp $"Request ID {dbId} not found" } @@ -189,11 +191,13 @@ module Note = let add reqId userId note = backgroundTask { let dbId = RequestId.toString reqId match! Request.tryByIdFull reqId userId with - | Some req -> do! Update.partialById Table.Request dbId {| Notes = Array.append [| note |] req.Notes |} + | Some req -> + do! Update.partialById Table.Request dbId + {| Notes = (note :: req.Notes) |> List.sortByDescending (fun it -> it.AsOf) |} | None -> invalidOp $"Request ID {dbId} not found" } /// Retrieve notes for a request by the request ID let byRequestId reqId userId = backgroundTask { - match! Request.tryByIdFull reqId userId with Some req -> return req.Notes | None -> return [||] + match! Request.tryByIdFull reqId userId with Some req -> return req.Notes | None -> return [] } diff --git a/src/MyPrayerJournal/Domain.fs b/src/MyPrayerJournal/Domain.fs index 4201135..2389ce0 100644 --- a/src/MyPrayerJournal/Domain.fs +++ b/src/MyPrayerJournal/Domain.fs @@ -169,10 +169,10 @@ type Request = Recurrence : Recurrence /// The history entries for this request - History : History[] + History : History list /// The notes for this request - Notes : Note[] + Notes : Note list } /// Functions to support requests @@ -186,8 +186,8 @@ module Request = SnoozedUntil = None ShowAfter = None Recurrence = Immediate - History = [||] - Notes = [||] + History = [] + Notes = [] } @@ -234,7 +234,8 @@ module JournalRequest = /// Convert a request to the form used for the journal (precomputed values, no notes or history) let ofRequestLite (req : Request) = - let lastHistory = req.History |> Array.sortByDescending (fun it -> it.AsOf) |> Array.tryHead + let history = Seq.ofList req.History + let lastHistory = Seq.tryHead history // Requests are sorted by the "as of" field in this record; for sorting to work properly, we will put the // largest of the last prayed date, the "snoozed until". or the "show after" date; if none of those are filled, // we will use the last activity date. This will mean that: @@ -247,19 +248,17 @@ module JournalRequest = let showAfter = defaultArg req.ShowAfter Instant.MinValue let snoozedUntil = defaultArg req.SnoozedUntil Instant.MinValue let lastPrayed = - req.History - |> Array.sortByDescending (fun it -> it.AsOf) - |> Array.filter History.isPrayed - |> Array.tryHead + history + |> Seq.filter History.isPrayed + |> Seq.tryHead |> Option.map (fun it -> it.AsOf) |> Option.defaultValue Instant.MinValue let asOf = List.max [ lastPrayed; showAfter; snoozedUntil ] { RequestId = req.Id UserId = req.UserId - Text = req.History - |> Array.filter (fun it -> Option.isSome it.Text) - |> Array.sortByDescending (fun it -> it.AsOf) - |> Array.tryHead + Text = history + |> Seq.filter (fun it -> Option.isSome it.Text) + |> Seq.tryHead |> Option.map (fun h -> Option.get h.Text) |> Option.defaultValue "" AsOf = if asOf > Instant.MinValue then asOf else lastActivity @@ -275,6 +274,6 @@ module JournalRequest = /// Same as `ofRequestLite`, but with notes and history let ofRequestFull req = { ofRequestLite req with - History = List.ofArray req.History - Notes = List.ofArray req.Notes + History = req.History + Notes = req.Notes } diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs index c81f837..d071877 100644 --- a/src/MyPrayerJournal/Handlers.fs +++ b/src/MyPrayerJournal/Handlers.fs @@ -270,7 +270,7 @@ module Components = // GET /components/request/[req-id]/notes let notes requestId : HttpHandler = requireUser >=> fun next ctx -> task { let! notes = Note.byRequestId (RequestId.ofString requestId) ctx.UserId - return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone (List.ofArray notes)) next ctx + return! renderComponent (Views.Request.notes (ctx.Now ()) ctx.TimeZone notes) next ctx } // GET /components/request/[req-id]/snooze @@ -454,12 +454,12 @@ module Request = EnteredOn = now ShowAfter = None Recurrence = parseRecurrence form - History = [| + History = [ { AsOf = now Status = Created Text = Some form.requestText } - |] + ] } do! Request.add req Messages.pushSuccess ctx "Added prayer request" "/journal" diff --git a/src/MyPrayerJournal/LiteData.fs b/src/MyPrayerJournal/LiteData.fs deleted file mode 100644 index b930f3f..0000000 --- a/src/MyPrayerJournal/LiteData.fs +++ /dev/null @@ -1,199 +0,0 @@ -module MyPrayerJournal.LiteData - -open LiteDB -open MyPrayerJournal -open System.Threading.Tasks - -/// LiteDB extensions -[] -module Extensions = - - /// Extensions on the LiteDatabase class - type LiteDatabase with - - /// The Request collection - member this.Requests = this.GetCollection "request" - - /// Async version of the checkpoint command (flushes log) - member this.SaveChanges () = - this.Checkpoint () - Task.CompletedTask - - -/// Map domain to LiteDB -// It does mapping, but since we're so DU-heavy, this gives us control over the JSON representation -[] -module Mapping = - - open NodaTime - open NodaTime.Text - - /// A NodaTime instant pattern to use for parsing instants from the database - let instantPattern = InstantPattern.CreateWithInvariantCulture "g" - - /// Mapping for NodaTime's Instant type - module Instant = - let fromBson (value : BsonValue) = (instantPattern.Parse value.AsString).Value - let toBson (value : Instant) : BsonValue = value.ToString ("g", null) - - /// Mapping for option types - module Option = - let instantFromBson (value : BsonValue) = if value.IsNull then None else Some (Instant.fromBson value) - let instantToBson (value : Instant option) = match value with Some it -> Instant.toBson it | None -> null - - let stringFromBson (value : BsonValue) = match value.AsString with "" -> None | x -> Some x - let stringToBson (value : string option) : BsonValue = match value with Some txt -> txt | None -> "" - - /// Mapping for Recurrence - module Recurrence = - let fromBson (value : BsonValue) = Recurrence.ofString value - let toBson (value : Recurrence) : BsonValue = Recurrence.toString value - - /// Mapping for RequestAction - module RequestAction = - let fromBson (value : BsonValue) = RequestAction.ofString value.AsString - let toBson (value : RequestAction) : BsonValue = RequestAction.toString value - - /// Mapping for RequestId - module RequestId = - let fromBson (value : BsonValue) = RequestId.ofString value.AsString - let toBson (value : RequestId) : BsonValue = RequestId.toString value - - /// Mapping for UserId - module UserId = - let fromBson (value : BsonValue) = UserId value.AsString - let toBson (value : UserId) : BsonValue = UserId.toString value - - /// Set up the mapping - let register () = - BsonMapper.Global.RegisterType(Instant.toBson, Instant.fromBson) - BsonMapper.Global.RegisterType(Option.instantToBson, Option.instantFromBson) - BsonMapper.Global.RegisterType(Recurrence.toBson, Recurrence.fromBson) - BsonMapper.Global.RegisterType(RequestAction.toBson, RequestAction.fromBson) - BsonMapper.Global.RegisterType(RequestId.toBson, RequestId.fromBson) - BsonMapper.Global.RegisterType(Option.stringToBson, Option.stringFromBson) - BsonMapper.Global.RegisterType(UserId.toBson, UserId.fromBson) - -/// Code to be run at startup -module Startup = - - /// Ensure the database is set up - let ensureDb (db : LiteDatabase) = - db.Requests.EnsureIndex (fun it -> it.UserId) |> ignore - Mapping.register () - - -/// Async wrappers for LiteDB, and request -> journal mappings -[] -module private Helpers = - - open System.Linq - - /// Convert a sequence to a list asynchronously (used for LiteDB IO) - let toListAsync<'T> (q : 'T seq) = - (q.ToList >> Task.FromResult) () - - /// Convert a sequence to a list asynchronously (used for LiteDB IO) - let firstAsync<'T> (q : 'T seq) = - q.FirstOrDefault () |> Task.FromResult - - /// Async wrapper around a request update - let doUpdate (db : LiteDatabase) (req : Request) = - db.Requests.Update req |> ignore - Task.CompletedTask - - -/// Retrieve a request, including its history and notes, by its ID and user ID -let tryFullRequestById reqId userId (db : LiteDatabase) = backgroundTask { - let! req = db.Requests.Find (Query.EQ ("_id", RequestId.toString reqId)) |> firstAsync - return match box req with null -> None | _ when req.UserId = userId -> Some req | _ -> None -} - -/// Add a history entry -let addHistory reqId userId hist db = backgroundTask { - match! tryFullRequestById reqId userId db with - | Some req -> do! doUpdate db { req with History = Array.append [| hist |] req.History } - | None -> invalidOp $"{RequestId.toString reqId} not found" -} - -/// Add a note -let addNote reqId userId note db = backgroundTask { - match! tryFullRequestById reqId userId db with - | Some req -> do! doUpdate db { req with Notes = Array.append [| note |] req.Notes } - | None -> invalidOp $"{RequestId.toString reqId} not found" -} - -/// Add a request -let addRequest (req : Request) (db : LiteDatabase) = - db.Requests.Insert req |> ignore - -/// Find all requests for the given user -let private getRequestsForUser (userId : UserId) (db : LiteDatabase) = backgroundTask { - return! db.Requests.Find (Query.EQ (nameof Request.empty.UserId, Mapping.UserId.toBson userId)) |> toListAsync -} - -/// Retrieve all answered requests for the given user -let answeredRequests userId db = backgroundTask { - let! reqs = getRequestsForUser userId db - return - reqs - |> Seq.map JournalRequest.ofRequestFull - |> Seq.filter (fun it -> it.LastStatus = Answered) - |> Seq.sortByDescending (fun it -> it.AsOf) - |> List.ofSeq -} - -/// Retrieve the user's current journal -let journalByUserId userId db = backgroundTask { - let! reqs = getRequestsForUser userId db - return - reqs - |> Seq.map JournalRequest.ofRequestLite - |> Seq.filter (fun it -> it.LastStatus <> Answered) - |> Seq.sortBy (fun it -> it.AsOf) - |> List.ofSeq -} - -/// Does the user have any snoozed requests? -let hasSnoozed userId now (db : LiteDatabase) = backgroundTask { - let! jrnl = journalByUserId userId db - return jrnl |> List.exists (fun r -> defaultArg (r.SnoozedUntil |> Option.map (fun it -> it > now)) false) -} - -/// Retrieve a request by its ID and user ID (without notes and history) -let tryRequestById reqId userId db = backgroundTask { - let! req = tryFullRequestById reqId userId db - return req |> Option.map (fun r -> { r with History = [||]; Notes = [||] }) -} - -/// Retrieve notes for a request by its ID and user ID -let notesById reqId userId (db : LiteDatabase) = backgroundTask { - match! tryFullRequestById reqId userId db with | Some req -> return req.Notes | None -> return [||] -} - -/// Retrieve a journal request by its ID and user ID -let tryJournalById reqId userId (db : LiteDatabase) = backgroundTask { - let! req = tryFullRequestById reqId userId db - return req |> Option.map JournalRequest.ofRequestLite -} - -/// Update the recurrence for a request -let updateRecurrence reqId userId recurType db = backgroundTask { - match! tryFullRequestById reqId userId db with - | Some req -> do! doUpdate db { req with Recurrence = recurType } - | None -> invalidOp $"{RequestId.toString reqId} not found" -} - -/// Update a snoozed request -let updateSnoozed reqId userId until db = backgroundTask { - match! tryFullRequestById reqId userId db with - | Some req -> do! doUpdate db { req with SnoozedUntil = until; ShowAfter = until } - | None -> invalidOp $"{RequestId.toString reqId} not found" -} - -/// Update the "show after" timestamp for a request -let updateShowAfter reqId userId showAfter db = backgroundTask { - match! tryFullRequestById reqId userId db with - | Some req -> do! doUpdate db { req with ShowAfter = showAfter } - | None -> invalidOp $"{RequestId.toString reqId} not found" -} diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index 20289fa..2ffa9ec 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -1,14 +1,12 @@  net7.0 - 3.2 + 3.3 embedded false - 3391 - @@ -26,7 +24,6 @@ - diff --git a/src/MyPrayerJournal/Views/Request.fs b/src/MyPrayerJournal/Views/Request.fs index 8549e46..e465f28 100644 --- a/src/MyPrayerJournal/Views/Request.fs +++ b/src/MyPrayerJournal/Views/Request.fs @@ -77,28 +77,31 @@ let full (clock : IClock) tz (req : Request) = let now = clock.GetCurrentInstant () let answered = req.History - |> Array.filter History.isAnswered - |> Array.tryHead + |> Seq.ofList + |> Seq.filter History.isAnswered + |> Seq.tryHead |> Option.map (fun x -> x.AsOf) - let prayed = (req.History |> Array.filter History.isPrayed |> Array.length).ToString "N0" + let prayed = (req.History |> List.filter History.isPrayed |> List.length).ToString "N0" let daysOpen = let asOf = defaultArg answered now - ((asOf - (req.History |> Array.filter History.isCreated |> Array.head).AsOf).TotalDays |> int).ToString "N0" + ((asOf - (req.History |> List.filter History.isCreated |> List.head).AsOf).TotalDays |> int).ToString "N0" let lastText = req.History - |> Array.filter (fun h -> Option.isSome h.Text) - |> Array.sortByDescending (fun h -> h.AsOf) - |> Array.map (fun h -> Option.get h.Text) - |> Array.head + |> Seq.ofList + |> Seq.filter (fun h -> Option.isSome h.Text) + |> Seq.sortByDescending (fun h -> h.AsOf) + |> Seq.map (fun h -> Option.get h.Text) + |> Seq.head // The history log including notes (and excluding the final entry for answered requests) let log = let toDisp (h : History) = {| asOf = h.AsOf; text = h.Text; status = RequestAction.toString h.Status |} let all = req.Notes - |> Array.map (fun n -> {| asOf = n.AsOf; text = Some n.Notes; status = "Notes" |}) - |> Array.append (req.History |> Array.map toDisp) - |> Array.sortByDescending (fun it -> it.asOf) - |> List.ofArray + |> Seq.ofList + |> Seq.map (fun n -> {| asOf = n.AsOf; text = Some n.Notes; status = "Notes" |}) + |> Seq.append (req.History |> List.map toDisp) + |> Seq.sortByDescending (fun it -> it.asOf) + |> List.ofSeq // Skip the first entry for answered requests; that info is already displayed match answered with Some _ -> all.Tail | None -> all article [ _class "container mt-3" ] [ -- 2.45.1 From 12041e03ffb908a1bcb80bce19a325b495ba693c Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 8 Oct 2023 17:42:58 -0400 Subject: [PATCH 05/11] Migrate data --- src/MyPrayerJournal.ToPostgres/Program.fs | 24 +++++++++++++++++++++++ src/MyPrayerJournal/Data.fs | 9 +++++---- src/MyPrayerJournal/appsettings.json | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/MyPrayerJournal.ToPostgres/Program.fs b/src/MyPrayerJournal.ToPostgres/Program.fs index d1b4ad3..fff4a16 100644 --- a/src/MyPrayerJournal.ToPostgres/Program.fs +++ b/src/MyPrayerJournal.ToPostgres/Program.fs @@ -1,9 +1,33 @@ open LiteDB +open MyPrayerJournal.Data open MyPrayerJournal.Domain open MyPrayerJournal.LiteData +open Microsoft.Extensions.Configuration let lite = new LiteDatabase "Filename=./mpj.db" Startup.ensureDb lite +let cfg = (ConfigurationBuilder().AddJsonFile "appsettings.json").Build () +Connection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously + +let reqs = lite.Requests.FindAll () + +reqs +|> Seq.map (fun old -> + { Request.empty with + Id = old.Id + EnteredOn = old.EnteredOn + UserId = old.UserId + SnoozedUntil = old.SnoozedUntil + ShowAfter = old.ShowAfter + Recurrence = old.Recurrence + History = old.History |> Array.sortByDescending (fun it -> it.AsOf) |> List.ofArray + Notes = old.Notes |> Array.sortByDescending (fun it -> it.AsOf) |> List.ofArray + }) +|> Seq.map Request.add +|> List.ofSeq +|> List.iter (Async.AwaitTask >> Async.RunSynchronously) + +System.Console.WriteLine $"Migration complete - {Seq.length reqs} requests migrated" diff --git a/src/MyPrayerJournal/Data.fs b/src/MyPrayerJournal/Data.fs index a8102a3..4b0345f 100644 --- a/src/MyPrayerJournal/Data.fs +++ b/src/MyPrayerJournal/Data.fs @@ -36,7 +36,8 @@ module Json = ] |> List.iter opts.Converters.Add let _ = opts.ConfigureForNodaTime NodaTime.DateTimeZoneProviders.Tzdb - opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + opts.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase + opts.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull opts @@ -165,9 +166,9 @@ module Journal = return reqs |> Seq.ofList - |> Seq.map JournalRequest.ofRequestFull - |> Seq.filter (fun it -> it.LastStatus = Answered) - |> Seq.sortByDescending (fun it -> it.AsOf) + |> Seq.map JournalRequest.ofRequestLite + |> Seq.filter (fun it -> it.LastStatus <> Answered) + |> Seq.sortBy (fun it -> it.AsOf) |> List.ofSeq } diff --git a/src/MyPrayerJournal/appsettings.json b/src/MyPrayerJournal/appsettings.json index 2e2d592..817bfde 100644 --- a/src/MyPrayerJournal/appsettings.json +++ b/src/MyPrayerJournal/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "db": "Filename=./mpj.db" + "mpj": "host=localhost;username=mpj;password=devpassword;database=mpj" }, "Kestrel": { "EndPoints": { -- 2.45.1 From 559f780f8eead0709fa2333aa5b53b0951b51d12 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 8 Oct 2023 21:19:35 -0400 Subject: [PATCH 06/11] Select based on answered status - Always retrieve full request by ID (needed for journal info) --- src/MyPrayerJournal/Data.fs | 39 +++++++++++++++++---------------- src/MyPrayerJournal/Handlers.fs | 2 +- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/MyPrayerJournal/Data.fs b/src/MyPrayerJournal/Data.fs index 4b0345f..f007339 100644 --- a/src/MyPrayerJournal/Data.fs +++ b/src/MyPrayerJournal/Data.fs @@ -88,20 +88,13 @@ module Request = let existsById (reqId : RequestId) (userId : UserId) = Exists.byContains Table.Request {| Id = reqId; UserId = userId |} - /// Retrieve a request by its ID and user ID (includes history and notes) - let tryByIdFull reqId userId = backgroundTask { + /// Retrieve a request by its ID and user ID + let tryById reqId userId = backgroundTask { match! Find.byId Table.Request (RequestId.toString reqId) with | Some req when req.UserId = userId -> return Some req | _ -> return None } - /// Retrieve a request by its ID and user ID (excludes history and notes) - let tryById reqId userId = backgroundTask { - match! tryByIdFull reqId userId with - | Some req -> return Some { req with History = []; Notes = [] } - | None -> return None - } - /// Update recurrence for a request let updateRecurrence reqId userId (recurType : Recurrence) = backgroundTask { let dbId = RequestId.toString reqId @@ -134,7 +127,7 @@ module History = /// Add a history entry let add reqId userId hist = backgroundTask { let dbId = RequestId.toString reqId - match! Request.tryByIdFull reqId userId with + match! Request.tryById reqId userId with | Some req -> do! Update.partialById Table.Request dbId {| History = (hist :: req.History) |> List.sortByDescending (fun it -> it.AsOf) |} @@ -147,22 +140,30 @@ module History = module Journal = /// Retrieve a user's answered requests - let answered userId = backgroundTask { - // TODO: only retrieve answered requests - let! reqs = Find.byContains Table.Request {| UserId = UserId.toString userId |} + let answered (userId : UserId) = backgroundTask { + let! reqs = + Custom.list + $"""{Query.Find.byContains Table.Request} AND {Query.whereJsonPathMatches "@stat"}""" + [ "@criteria", Query.jsonbDocParam {| UserId = userId |} + "@stat", Sql.string """$.history[0].status ? (@ == "Answered")""" + ] fromData return reqs |> Seq.ofList - |> Seq.map JournalRequest.ofRequestFull + |> Seq.map JournalRequest.ofRequestLite |> Seq.filter (fun it -> it.LastStatus = Answered) |> Seq.sortByDescending (fun it -> it.AsOf) |> List.ofSeq } /// Retrieve a user's current prayer journal (includes snoozed and non-immediate recurrence) - let forUser userId = backgroundTask { - // TODO: only retrieve unanswered requests - let! reqs = Find.byContains Table.Request {| UserId = UserId.toString userId |} + let forUser (userId : UserId) = backgroundTask { + let! reqs = + Custom.list + $"""{Query.Find.byContains Table.Request} AND {Query.whereJsonPathMatches "@stat"}""" + [ "@criteria", Query.jsonbDocParam {| UserId = userId |} + "@stat", Sql.string """$.history[0].status ? (@ <> "Answered")""" + ] fromData return reqs |> Seq.ofList @@ -191,7 +192,7 @@ module Note = /// Add a note let add reqId userId note = backgroundTask { let dbId = RequestId.toString reqId - match! Request.tryByIdFull reqId userId with + match! Request.tryById reqId userId with | Some req -> do! Update.partialById Table.Request dbId {| Notes = (note :: req.Notes) |> List.sortByDescending (fun it -> it.AsOf) |} @@ -200,5 +201,5 @@ module Note = /// Retrieve notes for a request by the request ID let byRequestId reqId userId = backgroundTask { - match! Request.tryByIdFull reqId userId with Some req -> return req.Notes | None -> return [] + match! Request.tryById reqId userId with Some req -> return req.Notes | None -> return [] } diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs index d071877..cd858c2 100644 --- a/src/MyPrayerJournal/Handlers.fs +++ b/src/MyPrayerJournal/Handlers.fs @@ -391,7 +391,7 @@ module Request = // GET /request/[req-id]/full let getFull requestId : HttpHandler = requireUser >=> fun next ctx -> task { - match! Request.tryByIdFull (RequestId.ofString requestId) ctx.UserId with + match! Request.tryById (RequestId.ofString requestId) ctx.UserId with | Some req -> return! partial "Prayer Request" (Views.Request.full ctx.Clock ctx.TimeZone req) next ctx | None -> return! Error.notFound next ctx } -- 2.45.1 From 7824169d514d9a6fb12bfcf22beaac07f84143dd Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 9 Oct 2023 10:06:47 -0400 Subject: [PATCH 07/11] Update deps - Remove recurrence conversion project --- .../MyPrayerJournal.ConvertRecurrence.fsproj | 16 --- .../Program.fs | 114 ------------------ src/MyPrayerJournal/MyPrayerJournal.fsproj | 4 +- 3 files changed, 2 insertions(+), 132 deletions(-) delete mode 100644 src/MyPrayerJournal.ConvertRecurrence/MyPrayerJournal.ConvertRecurrence.fsproj delete mode 100644 src/MyPrayerJournal.ConvertRecurrence/Program.fs diff --git a/src/MyPrayerJournal.ConvertRecurrence/MyPrayerJournal.ConvertRecurrence.fsproj b/src/MyPrayerJournal.ConvertRecurrence/MyPrayerJournal.ConvertRecurrence.fsproj deleted file mode 100644 index 88ac1e5..0000000 --- a/src/MyPrayerJournal.ConvertRecurrence/MyPrayerJournal.ConvertRecurrence.fsproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net6.0 - - - - - - - - - - - diff --git a/src/MyPrayerJournal.ConvertRecurrence/Program.fs b/src/MyPrayerJournal.ConvertRecurrence/Program.fs deleted file mode 100644 index 1be0e70..0000000 --- a/src/MyPrayerJournal.ConvertRecurrence/Program.fs +++ /dev/null @@ -1,114 +0,0 @@ -open MyPrayerJournal.Domain -open NodaTime - -/// The old definition of the history entry -[] -type OldHistory = - { /// The time when this history entry was made - asOf : int64 - /// The status for this history entry - status : RequestAction - /// The text of the update, if applicable - text : string option - } - -/// The old definition of of the note entry -[] -type OldNote = - { /// The time when this note was made - asOf : int64 - - /// The text of the notes - notes : string - } - -/// Request is the identifying record for a prayer request -[] -type OldRequest = - { /// The ID of the request - id : RequestId - - /// The time this request was initially entered - enteredOn : int64 - - /// The ID of the user to whom this request belongs ("sub" from the JWT) - userId : UserId - - /// The time at which this request should reappear in the user's journal by manual user choice - snoozedUntil : int64 - - /// The time at which this request should reappear in the user's journal by recurrence - showAfter : int64 - - /// The type of recurrence for this request - recurType : string - - /// How many of the recurrence intervals should occur between appearances in the journal - recurCount : int16 - - /// The history entries for this request - history : OldHistory[] - - /// The notes for this request - notes : OldNote[] - } - - -open LiteDB -open MyPrayerJournal.Data - -let db = new LiteDatabase ("Filename=./mpj.db") -Startup.ensureDb db - -/// Map the old recurrence to the new style -let mapRecurrence old = - match old.recurType with - | "Days" -> Days old.recurCount - | "Hours" -> Hours old.recurCount - | "Weeks" -> Weeks old.recurCount - | _ -> Immediate - -/// Convert an old history entry to the new form -let convertHistory (old : OldHistory) = - { AsOf = Instant.FromUnixTimeMilliseconds old.asOf - Status = old.status - Text = old.text - } - -/// Convert an old note to the new form -let convertNote (old : OldNote) = - { AsOf = Instant.FromUnixTimeMilliseconds old.asOf - Notes = old.notes - } - -/// Convert items that may be Instant.MinValue or Instant(0) to None -let noneIfOld ms = - match Instant.FromUnixTimeMilliseconds ms with - | instant when instant > Instant.FromUnixTimeMilliseconds 0 -> Some instant - | _ -> None - -/// Map the old request to the new request -let convert old = - { Id = old.id - EnteredOn = Instant.FromUnixTimeMilliseconds old.enteredOn - UserId = old.userId - SnoozedUntil = noneIfOld old.snoozedUntil - ShowAfter = noneIfOld old.showAfter - Recurrence = mapRecurrence old - History = old.history |> Array.map convertHistory |> List.ofArray - Notes = old.notes |> Array.map convertNote |> List.ofArray - } - -/// Remove the old request, add the converted one (removes recurType / recurCount fields) -let replace (req : Request) = - db.Requests.Delete (Mapping.RequestId.toBson req.Id) |> ignore - db.Requests.Insert req |> ignore - db.Checkpoint () - -db.GetCollection("request").FindAll () -|> Seq.map convert -|> List.ofSeq -|> List.iter replace - -// For more information see https://aka.ms/fsharp-console-apps -printfn "Done" diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index 2ffa9ec..0d59d24 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -21,10 +21,10 @@ - + - + -- 2.45.1 From ef10d492a8a722bc588b1a638e693b71fea5846d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 9 Oct 2023 10:22:40 -0400 Subject: [PATCH 08/11] Update htmx and Bootstrap --- src/MyPrayerJournal/.gitignore | 3 --- src/MyPrayerJournal/Views/Layout.fs | 8 ++++---- .../wwwroot/script/bootstrap.bundle.min.js | 6 +++--- src/MyPrayerJournal/wwwroot/script/htmx.min.js | 2 +- src/MyPrayerJournal/wwwroot/style/bootstrap.min.css | 7 +++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/MyPrayerJournal/.gitignore b/src/MyPrayerJournal/.gitignore index ee14a3f..090166d 100644 --- a/src/MyPrayerJournal/.gitignore +++ b/src/MyPrayerJournal/.gitignore @@ -1,5 +1,2 @@ -## LiteDB database file -*.db - ## Development settings appsettings.Development.json diff --git a/src/MyPrayerJournal/Views/Layout.fs b/src/MyPrayerJournal/Views/Layout.fs index b495980..cdd0022 100644 --- a/src/MyPrayerJournal/Views/Layout.fs +++ b/src/MyPrayerJournal/Views/Layout.fs @@ -77,9 +77,9 @@ let htmlHead ctx = meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] meta [ _name "description"; _content "Online prayer journal - free w/Google or Microsoft account" ] titleTag ctx - link [ _href "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" + link [ _href "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" _rel "stylesheet" - _integrity "sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" + _integrity "sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" _crossorigin "anonymous" ] link [ _href "https://fonts.googleapis.com/icon?family=Material+Icons"; _rel "stylesheet" ] link [ _href "/style/style.css"; _rel "stylesheet" ] @@ -118,8 +118,8 @@ let htmlFoot = rawText "if (!htmx) document.write('