diff --git a/src/MyPrayerJournal.sln b/src/MyPrayerJournal.sln index bac65a6..f535d6b 100644 --- a/src/MyPrayerJournal.sln +++ b/src/MyPrayerJournal.sln @@ -5,8 +5,6 @@ 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.ToPostgres", "MyPrayerJournal.ToPostgres\MyPrayerJournal.ToPostgres.fsproj", "{3114B8F4-E388-4804-94D3-A2F4D42797C6}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,9 +22,5 @@ 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 f007339..92ccd87 100644 --- a/src/MyPrayerJournal/Data.fs +++ b/src/MyPrayerJournal/Data.fs @@ -15,24 +15,24 @@ 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> () + type WrappedJsonConverter<'T>(wrap : string -> 'T, unwrap : 'T -> string) = + inherit JsonConverter<'T>() override _.Read(reader, _, _) = - wrap (reader.GetString ()) + wrap (reader.GetString()) override _.Write(writer, value, _) = - writer.WriteStringValue (unwrap 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 () + 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 @@ -62,12 +62,12 @@ module Connection = /// Set up the data environment let setUp (cfg : IConfiguration) = backgroundTask { let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "mpj") - let _ = builder.UseNodaTime () - Configuration.useDataSource (builder.Build ()) + 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) + member _.Serialize<'T>(it : 'T) = JsonSerializer.Serialize(it, Json.options) + member _.Deserialize<'T>(it : string) = JsonSerializer.Deserialize<'T>(it, Json.options) } do! ensureDb () } @@ -80,9 +80,8 @@ module Request = open NodaTime /// Add a request - let add req = backgroundTask { - do! insert Table.Request (RequestId.toString req.Id) req - } + let add req = + insert Table.Request req /// Does a request exist for the given request ID and user ID? let existsById (reqId : RequestId) (userId : UserId) = @@ -100,7 +99,7 @@ module Request = 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" + | false -> invalidOp $"Request ID {dbId} not found" } /// Update the show-after time for a request @@ -108,7 +107,7 @@ module Request = 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" + | false -> invalidOp $"Request ID {dbId} not found" } /// Update the snoozed and show-after values for a request @@ -116,7 +115,7 @@ module Request = 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" + | false -> invalidOp $"Request ID {dbId} not found" } @@ -130,7 +129,7 @@ module History = match! Request.tryById reqId userId with | Some req -> do! Update.partialById Table.Request dbId - {| History = (hist :: req.History) |> List.sortByDescending (fun it -> it.AsOf) |} + {| History = (hist :: req.History) |> List.sortByDescending (_.AsOf) |} | None -> invalidOp $"Request ID {dbId} not found" } @@ -152,7 +151,7 @@ module Journal = |> Seq.ofList |> Seq.map JournalRequest.ofRequestLite |> Seq.filter (fun it -> it.LastStatus = Answered) - |> Seq.sortByDescending (fun it -> it.AsOf) + |> Seq.sortByDescending (_.AsOf) |> List.ofSeq } @@ -169,7 +168,7 @@ module Journal = |> Seq.ofList |> Seq.map JournalRequest.ofRequestLite |> Seq.filter (fun it -> it.LastStatus <> Answered) - |> Seq.sortBy (fun it -> it.AsOf) + |> Seq.sortBy (_.AsOf) |> List.ofSeq } @@ -195,7 +194,7 @@ module Note = match! Request.tryById reqId userId with | Some req -> do! Update.partialById Table.Request dbId - {| Notes = (note :: req.Notes) |> List.sortByDescending (fun it -> it.AsOf) |} + {| Notes = (note :: req.Notes) |> List.sortByDescending (_.AsOf) |} | None -> invalidOp $"Request ID {dbId} not found" } diff --git a/src/MyPrayerJournal/Domain.fs b/src/MyPrayerJournal/Domain.fs index de688ed..445aeb0 100644 --- a/src/MyPrayerJournal/Domain.fs +++ b/src/MyPrayerJournal/Domain.fs @@ -244,14 +244,14 @@ module JournalRequest = // them at the bottom of the list. // - Snoozed requests will reappear at the bottom of the list when they return. // - New requests will go to the bottom of the list, but will rise as others are marked as prayed. - let lastActivity = lastHistory |> Option.map (fun it -> it.AsOf) |> Option.defaultValue Instant.MinValue + let lastActivity = lastHistory |> Option.map (_.AsOf) |> Option.defaultValue Instant.MinValue let showAfter = defaultArg req.ShowAfter Instant.MinValue let snoozedUntil = defaultArg req.SnoozedUntil Instant.MinValue let lastPrayed = history |> Seq.filter History.isPrayed |> Seq.tryHead - |> Option.map (fun it -> it.AsOf) + |> Option.map (_.AsOf) |> Option.defaultValue Instant.MinValue let asOf = List.max [ lastPrayed; showAfter; snoozedUntil ] { RequestId = req.Id diff --git a/src/MyPrayerJournal/Handlers.fs b/src/MyPrayerJournal/Handlers.fs index 367b042..4816900 100644 --- a/src/MyPrayerJournal/Handlers.fs +++ b/src/MyPrayerJournal/Handlers.fs @@ -16,7 +16,7 @@ module private LogOnHelpers = let logOn url : HttpHandler = fun next ctx -> task { match url with | Some it -> - do! ctx.ChallengeAsync ("Auth0", AuthenticationProperties (RedirectUri = it)) + do! ctx.ChallengeAsync("Auth0", AuthenticationProperties(RedirectUri = it)) return! next ctx | None -> return! challenge "Auth0" next ctx } @@ -57,14 +57,14 @@ type HttpContext with |> Option.ofObj |> Option.map (fun user -> user.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier)) |> Option.flatten - |> Option.map (fun claim -> claim.Value) + |> Option.map (_.Value) /// The current user's ID // NOTE: this may raise if you don't run the request through the requireUser handler first member this.UserId = UserId this.CurrentUser.Value /// The system clock - member this.Clock = this.GetService () + member this.Clock = this.GetService() /// Get the current instant from the system clock member this.Now = this.Clock.GetCurrentInstant @@ -94,7 +94,7 @@ module private Helpers = /// Debug logger let debug (ctx : HttpContext) message = - let fac = ctx.GetService () + let fac = ctx.GetService() let log = fac.CreateLogger "Debug" log.LogInformation message @@ -115,7 +115,7 @@ module private Helpers = let renderComponent nodes : HttpHandler = noResponseCaching >=> fun _ ctx -> backgroundTask { - return! ctx.WriteHtmlStringAsync (ViewEngine.RenderView.AsString.htmlNodes nodes) + return! ctx.WriteHtmlStringAsync(ViewEngine.RenderView.AsString.htmlNodes nodes) } open Views.Layout @@ -125,7 +125,7 @@ module private Helpers = let pageContext (ctx : HttpContext) pageTitle content = backgroundTask { let! hasSnoozed = match ctx.CurrentUser with - | Some _ -> Journal.hasSnoozed ctx.UserId (ctx.Now ()) + | Some _ -> Journal.hasSnoozed ctx.UserId (ctx.Now()) | None -> Task.FromResult false return { IsAuthenticated = Option.isSome ctx.CurrentUser @@ -153,7 +153,7 @@ 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 = @@ -259,7 +259,7 @@ module Components = // GET /components/request-item/[req-id] let requestItem reqId : HttpHandler = requireUser >=> fun next ctx -> task { match! Journal.tryById (RequestId.ofString reqId) ctx.UserId with - | Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now ()) ctx.TimeZone req ] next ctx + | Some req -> return! renderComponent [ Views.Request.reqListItem (ctx.Now()) ctx.TimeZone req ] next ctx | None -> return! Error.notFound next ctx } @@ -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 notes) next ctx + return! renderComponent (Views.Request.notes (ctx.Now()) ctx.TimeZone notes) next ctx } // GET /components/request/[req-id]/snooze @@ -294,7 +294,7 @@ module Journal = let usr = ctx.User.Claims |> Seq.tryFind (fun c -> c.Type = ClaimTypes.GivenName) - |> Option.map (fun c -> c.Value) + |> Option.map (_.Value) |> Option.defaultValue "Your" let title = usr |> match usr with "Your" -> sprintf "%s" | _ -> sprintf "%s's" return! partial $"{title} Prayer Journal" (Views.Journal.journal usr) next ctx @@ -362,8 +362,8 @@ module Request = let reqId = RequestId.ofString requestId match! Request.existsById reqId userId with | true -> - let! notes = ctx.BindFormAsync () - do! Note.add reqId userId { AsOf = ctx.Now (); Notes = notes.notes } + let! notes = ctx.BindFormAsync() + do! Note.add reqId userId { AsOf = ctx.Now(); Notes = notes.notes } return! (withSuccessMessage "Added Notes" >=> hideModal "notes" >=> created) next ctx | false -> return! Error.notFound next ctx } @@ -371,13 +371,13 @@ module Request = // GET /requests/active let active : HttpHandler = requireUser >=> fun next ctx -> task { let! reqs = Journal.forUser ctx.UserId - return! partial "Active Requests" (Views.Request.active (ctx.Now ()) ctx.TimeZone reqs) next ctx + 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 = Journal.forUser ctx.UserId - let now = ctx.Now () + let now = ctx.Now() let snoozed = reqs |> List.filter (fun it -> defaultArg (it.SnoozedUntil |> Option.map (fun it -> it > now)) false) return! partial "Snoozed Requests" (Views.Request.snoozed now ctx.TimeZone snoozed) next ctx @@ -386,7 +386,7 @@ module Request = // GET /requests/answered let answered : HttpHandler = requireUser >=> fun next ctx -> task { let! reqs = Journal.answered ctx.UserId - return! partial "Answered Requests" (Views.Request.answered (ctx.Now ()) ctx.TimeZone reqs) next ctx + return! partial "Answered Requests" (Views.Request.answered (ctx.Now()) ctx.TimeZone reqs) next ctx } // GET /request/[req-id]/full @@ -413,11 +413,11 @@ module Request = let reqId = RequestId.ofString requestId match! Request.existsById reqId userId with | true -> - let! until = ctx.BindFormAsync () + let! until = ctx.BindFormAsync() let date = LocalDatePattern.CreateWithInvariantCulture("yyyy-MM-dd").Parse(until.until).Value .AtStartOfDayInZone(DateTimeZone.Utc) - .ToInstant () + .ToInstant() do! Request.updateSnoozed reqId userId (Some date) return! (withSuccessMessage $"Request snoozed until {until.until}" @@ -444,9 +444,9 @@ module Request = // POST /request let add : HttpHandler = requireUser >=> fun next ctx -> task { - let! form = ctx.BindModelAsync () + let! form = ctx.BindModelAsync() let userId = ctx.UserId - let now = ctx.Now () + let now = ctx.Now() let req = { Request.empty with Id = Cuid.generate () |> RequestId @@ -468,7 +468,7 @@ module Request = // PATCH /request let update : HttpHandler = requireUser >=> fun next ctx -> task { - let! form = ctx.BindModelAsync () + let! form = ctx.BindModelAsync() let userId = ctx.UserId // TODO: update the instance and save rather than all these little updates match! Journal.tryById (RequestId.ofString form.requestId) userId with @@ -483,10 +483,10 @@ module Request = | Immediate -> do! Request.updateShowAfter req.RequestId userId None | _ -> () // append history - let upd8Text = form.requestText.Trim () + let upd8Text = form.requestText.Trim() let text = if upd8Text = req.Text then None else Some upd8Text do! History.add req.RequestId userId - { AsOf = ctx.Now (); Status = (Option.get >> RequestAction.ofString) form.status; Text = text } + { AsOf = ctx.Now(); Status = (Option.get >> RequestAction.ofString) form.status; Text = text } let nextUrl = match form.returnTo with | "active" -> "/requests/active" @@ -510,7 +510,7 @@ module User = // GET /user/log-off let logOff : HttpHandler = requireUser >=> fun next ctx -> task { - do! ctx.SignOutAsync ("Auth0", AuthenticationProperties (RedirectUri = "/")) + do! ctx.SignOutAsync("Auth0", AuthenticationProperties (RedirectUri = "/")) do! ctx.SignOutAsync CookieAuthenticationDefaults.AuthenticationScheme return! next ctx } diff --git a/src/MyPrayerJournal/MyPrayerJournal.fsproj b/src/MyPrayerJournal/MyPrayerJournal.fsproj index 4ab242d..6f2dbd1 100644 --- a/src/MyPrayerJournal/MyPrayerJournal.fsproj +++ b/src/MyPrayerJournal/MyPrayerJournal.fsproj @@ -1,7 +1,7 @@ - net7.0 - 3.3 + net8.0 + 3.4 embedded false false @@ -20,16 +20,15 @@ - + - - - + + + - - + diff --git a/src/MyPrayerJournal/Program.fs b/src/MyPrayerJournal/Program.fs index 38e46e0..612e35e 100644 --- a/src/MyPrayerJournal/Program.fs +++ b/src/MyPrayerJournal/Program.fs @@ -31,10 +31,10 @@ let main args = let builder = WebApplication.CreateBuilder args let _ = builder.Configuration.AddEnvironmentVariables "MPJ_" let svc = builder.Services - let cfg = svc.BuildServiceProvider().GetRequiredService () + let cfg = svc.BuildServiceProvider().GetRequiredService() - let _ = svc.AddRouting () - let _ = svc.AddGiraffe () + let _ = svc.AddRouting() + let _ = svc.AddGiraffe() let _ = svc.AddSingleton SystemClock.Instance let _ = svc.AddSingleton DateTimeZoneProviders.Tzdb let _ = svc.Configure(fun (opts : ForwardedHeadersOptions) -> @@ -59,7 +59,7 @@ let main args = opts.ClientSecret <- auth0["Secret"] opts.ResponseType <- OpenIdConnectResponseType.Code - opts.Scope.Clear () + opts.Scope.Clear() opts.Scope.Add "openid" opts.Scope.Add "profile" @@ -67,7 +67,7 @@ let main args = opts.ClaimsIssuer <- "Auth0" opts.SaveTokens <- true - opts.Events <- OpenIdConnectEvents () + opts.Events <- OpenIdConnectEvents() opts.Events.OnRedirectToIdentityProviderForSignOut <- fun ctx -> let returnTo = match ctx.Properties.RedirectUri with @@ -82,7 +82,7 @@ let main args = | false -> redirUri Uri.EscapeDataString $"&returnTo={finalRedirUri}" ctx.Response.Redirect $"""https://{auth0["Domain"]}/v2/logout?client_id={auth0["Id"]}{returnTo}""" - ctx.HandleResponse () + ctx.HandleResponse() Task.CompletedTask opts.Events.OnRedirectToIdentityProvider <- fun ctx -> let uri = UriBuilder ctx.ProtocolMessage.RedirectUri @@ -92,20 +92,20 @@ let main args = Task.CompletedTask) let _ = svc.AddSingleton Json.options - let _ = svc.AddSingleton (SystemTextJson.Serializer Json.options) + let _ = svc.AddSingleton(SystemTextJson.Serializer Json.options) let _ = Connection.setUp cfg |> Async.AwaitTask |> Async.RunSynchronously - if builder.Environment.IsDevelopment () then builder.Logging.AddFilter (fun l -> l > LogLevel.Information) |> ignore + if builder.Environment.IsDevelopment() then builder.Logging.AddFilter(fun l -> l > LogLevel.Information) |> ignore let _ = builder.Logging.AddConsole().AddDebug() |> ignore - use app = builder.Build () - let _ = app.UseStaticFiles () - let _ = app.UseCookiePolicy () - let _ = app.UseRouting () - let _ = app.UseAuthentication () + use app = builder.Build() + let _ = app.UseStaticFiles() + let _ = app.UseCookiePolicy() + let _ = app.UseRouting() + let _ = app.UseAuthentication() let _ = app.UseGiraffeErrorHandler Handlers.Error.error - let _ = app.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes) + let _ = app.UseEndpoints(fun e -> e.MapGiraffeEndpoints Handlers.routes) - app.Run () + app.Run() 0 diff --git a/src/MyPrayerJournal/Views/Helpers.fs b/src/MyPrayerJournal/Views/Helpers.fs index b950342..63ee010 100644 --- a/src/MyPrayerJournal/Views/Helpers.fs +++ b/src/MyPrayerJournal/Views/Helpers.fs @@ -29,7 +29,7 @@ let noResults heading link buttonText text = /// Create a date with a span tag, displaying the relative date with the full date/time in the tooltip let relativeDate (date : Instant) now (tz : DateTimeZone) = - span [ _title (date.InZone(tz).ToDateTimeOffset().ToString ("f", null)) ] [ Dates.formatDistance now date |> str ] + span [ _title (date.InZone(tz).ToDateTimeOffset().ToString("f", null)) ] [ Dates.formatDistance now date |> str ] /// The version of myPrayerJournal let version = diff --git a/src/MyPrayerJournal/Views/Request.fs b/src/MyPrayerJournal/Views/Request.fs index e465f28..c78a6e5 100644 --- a/src/MyPrayerJournal/Views/Request.fs +++ b/src/MyPrayerJournal/Views/Request.fs @@ -74,13 +74,13 @@ let snoozed now tz reqs = /// View for Full Request page let full (clock : IClock) tz (req : Request) = - let now = clock.GetCurrentInstant () + let now = clock.GetCurrentInstant() let answered = req.History |> Seq.ofList |> Seq.filter History.isAnswered |> Seq.tryHead - |> Option.map (fun x -> x.AsOf) + |> Option.map (_.AsOf) let prayed = (req.History |> List.filter History.isPrayed |> List.length).ToString "N0" let daysOpen = let asOf = defaultArg answered now @@ -89,7 +89,7 @@ let full (clock : IClock) tz (req : Request) = req.History |> Seq.ofList |> Seq.filter (fun h -> Option.isSome h.Text) - |> Seq.sortByDescending (fun h -> h.AsOf) + |> Seq.sortByDescending (_.AsOf) |> Seq.map (fun h -> Option.get h.Text) |> Seq.head // The history log including notes (and excluding the final entry for answered requests) @@ -100,7 +100,7 @@ let full (clock : IClock) tz (req : Request) = |> 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) + |> Seq.sortByDescending (_.asOf) |> List.ofSeq // Skip the first entry for answered requests; that info is already displayed match answered with Some _ -> all.Tail | None -> all @@ -112,7 +112,7 @@ let full (clock : IClock) tz (req : Request) = match answered with | Some date -> str "Answered " - date.ToDateTimeOffset().ToString ("D", null) |> str + date.ToDateTimeOffset().ToString("D", null) |> str str " (" relativeDate date now tz rawText ") • " @@ -127,7 +127,7 @@ let full (clock : IClock) tz (req : Request) = p [ _class "m-0" ] [ str it.status rawText "  " - small [] [ em [] [ it.asOf.ToDateTimeOffset().ToString ("D", null) |> str ] ] + small [] [ em [] [ it.asOf.ToDateTimeOffset().ToString("D", null) |> str ] ] ] match it.text with | Some txt -> p [ _class "mt-2 mb-0" ] [ str txt ]