From 96f2f2f7e079c9cb10aad5b6ed53d9b6cb3be684 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 14 Aug 2018 21:01:21 -0500 Subject: [PATCH] Added recurrence SQL; updated API API should support recurrence (#16); also updated for new F# match! statement --- src/api/MyPrayerJournal.Api/Data.fs | 39 ++++++++---- src/api/MyPrayerJournal.Api/Handlers.fs | 85 +++++++++++++++---------- src/api/MyPrayerJournal.Api/Program.fs | 2 +- src/sql/16-recurrence.sql | 31 +++++++++ 4 files changed, 112 insertions(+), 45 deletions(-) create mode 100644 src/sql/16-recurrence.sql diff --git a/src/api/MyPrayerJournal.Api/Data.fs b/src/api/MyPrayerJournal.Api/Data.fs index fefb8de..5fef05f 100644 --- a/src/api/MyPrayerJournal.Api/Data.fs +++ b/src/api/MyPrayerJournal.Api/Data.fs @@ -88,7 +88,7 @@ module Entities = m.Property(fun e -> e.notes).IsRequired () |> ignore) |> ignore - // Request is the identifying record for a prayer request. + /// Request is the identifying record for a prayer request and [] Request = { /// The ID of the request requestId : RequestId @@ -96,8 +96,14 @@ module Entities = enteredOn : int64 /// The ID of the user to whom this request belongs ("sub" from the JWT) userId : string - /// The time that this request should reappear in the user's journal + /// 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 : ICollection /// The notes for this request @@ -110,6 +116,9 @@ module Entities = enteredOn = 0L userId = "" snoozedUntil = 0L + showAfter = 0L + recurType = "immediate" + recurCount = 0s history = List () notes = List () } @@ -123,6 +132,9 @@ module Entities = m.Property(fun e -> e.enteredOn).IsRequired () |> ignore m.Property(fun e -> e.userId).IsRequired () |> ignore m.Property(fun e -> e.snoozedUntil).IsRequired () |> ignore + m.Property(fun e -> e.showAfter).IsRequired () |> ignore + m.Property(fun e -> e.recurType).IsRequired() |> ignore + m.Property(fun e -> e.recurCount).IsRequired() |> ignore m.HasMany(fun e -> e.history :> IEnumerable) .WithOne() .HasForeignKey(fun e -> e.requestId :> obj) @@ -149,6 +161,12 @@ module Entities = lastStatus : string /// The time that this request should reappear in the user's journal snoozedUntil : int64 + /// The time after which this request should reappear in the user's journal by configured 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 /// History entries for the request history : History list /// Note entries for the request @@ -236,8 +254,7 @@ type AppDbContext (opts : DbContextOptions) = /// Retrieve notes for a request by its ID and user ID member this.NotesById reqId userId = task { - let! req = this.TryRequestById reqId userId - match req with + match! this.TryRequestById reqId userId with | Some _ -> return this.Notes.AsNoTracking().Where(fun n -> n.requestId = reqId) |> List.ofSeq | None -> return [] } @@ -252,16 +269,15 @@ type AppDbContext (opts : DbContextOptions) = /// Retrieve a request, including its history and notes, by its ID and user ID member this.TryCompleteRequestById requestId userId = task { - let! req = this.TryJournalById requestId userId - match req with - | Some r -> + match! this.TryJournalById requestId userId with + | Some req -> let! fullReq = this.Requests.AsNoTracking() .Include(fun r -> r.history) .Include(fun r -> r.notes) .FirstOrDefaultAsync(fun r -> r.requestId = requestId && r.userId = userId) match toOption fullReq with - | Some _ -> return Some { r with history = List.ofSeq fullReq.history; notes = List.ofSeq fullReq.notes } + | Some _ -> return Some { req with history = List.ofSeq fullReq.history; notes = List.ofSeq fullReq.notes } | None -> return None | None -> return None } @@ -269,15 +285,14 @@ type AppDbContext (opts : DbContextOptions) = /// Retrieve a request, including its history, by its ID and user ID member this.TryFullRequestById requestId userId = task { - let! req = this.TryJournalById requestId userId - match req with - | Some r -> + match! this.TryJournalById requestId userId with + | Some req -> let! fullReq = this.Requests.AsNoTracking() .Include(fun r -> r.history) .FirstOrDefaultAsync(fun r -> r.requestId = requestId && r.userId = userId) match toOption fullReq with - | Some _ -> return Some { r with history = List.ofSeq fullReq.history } + | Some _ -> return Some { req with history = List.ofSeq fullReq.history } | None -> return None | None -> return None } diff --git a/src/api/MyPrayerJournal.Api/Handlers.fs b/src/api/MyPrayerJournal.Api/Handlers.fs index cf41c4c..7df9836 100644 --- a/src/api/MyPrayerJournal.Api/Handlers.fs +++ b/src/api/MyPrayerJournal.Api/Handlers.fs @@ -6,6 +6,14 @@ open Giraffe open MyPrayerJournal open System +/// Handler to return Vue files +module Vue = + + /// The application index page + let app : HttpHandler = htmlFile "wwwroot/index.html" + + +/// Handlers for error conditions module Error = open Microsoft.Extensions.Logging @@ -23,7 +31,7 @@ module Error = |> List.length |> function | 0 -> (setStatusCode 404 >=> json ([ "error", "not found" ] |> dict)) next ctx - | _ -> htmlFile "wwwroot/index.html" next ctx + | _ -> Vue.app next ctx /// Handler helpers @@ -92,6 +100,10 @@ module Models = type Request = { /// The text of the request requestText : string + /// The recurrence type + recurType : string + /// The recurrence count + recurCount : int16 option } /// The time until which a request should not appear in the journal @@ -119,6 +131,15 @@ module Request = open NCuid + /// Ticks per recurrence + let private recurrence = + [ "immediate", 0L + "hours", 3600000L + "days", 86400000L + "weeks", 604800000L + ] + |> Map.ofList + /// POST /api/request let add : HttpHandler = authorize @@ -130,10 +151,12 @@ module Request = let usrId = userId ctx let now = jsNow () { Request.empty with - requestId = reqId - userId = usrId - enteredOn = now - snoozedUntil = 0L + requestId = reqId + userId = usrId + enteredOn = now + showAfter = now + recurType = r.recurType + recurCount = defaultArg r.recurCount 0s } |> db.AddEntry { History.empty with @@ -144,9 +167,8 @@ module Request = } |> db.AddEntry let! _ = db.SaveChangesAsync () - let! req = db.TryJournalById reqId usrId - match req with - | Some rqst -> return! (setStatusCode 201 >=> json rqst) next ctx + match! db.TryJournalById reqId usrId with + | Some req -> return! (setStatusCode 201 >=> json req) next ctx | None -> return! Error.notFound next ctx } @@ -155,18 +177,22 @@ module Request = authorize >=> fun next ctx -> task { - let db = db ctx - let! req = db.TryRequestById reqId (userId ctx) - match req with - | Some _ -> + let db = db ctx + match! db.TryRequestById reqId (userId ctx) with + | Some req -> let! hist = ctx.BindJsonAsync () + let now = jsNow () { History.empty with requestId = reqId - asOf = jsNow () + asOf = now status = hist.status text = match hist.updateText with null | "" -> None | x -> Some x } |> db.AddEntry + match hist.status with + | "Prayed" -> + db.UpdateEntry { req with showAfter = now + (recurrence.[req.recurType] * int64 req.recurCount) } + | _ -> () let! _ = db.SaveChangesAsync () return! created next ctx | None -> return! Error.notFound next ctx @@ -177,15 +203,14 @@ module Request = authorize >=> fun next ctx -> task { - let db = db ctx - let! req = db.TryRequestById reqId (userId ctx) - match req with + let db = db ctx + match! db.TryRequestById reqId (userId ctx) with | Some _ -> let! notes = ctx.BindJsonAsync () { Note.empty with requestId = reqId - asOf = jsNow () - notes = notes.notes + asOf = jsNow () + notes = notes.notes } |> db.AddEntry let! _ = db.SaveChangesAsync () @@ -206,9 +231,8 @@ module Request = authorize >=> fun next ctx -> task { - let! req = (db ctx).TryJournalById reqId (userId ctx) - match req with - | Some r -> return! json r next ctx + match! (db ctx).TryJournalById reqId (userId ctx) with + | Some req -> return! json req next ctx | None -> return! Error.notFound next ctx } @@ -217,9 +241,8 @@ module Request = authorize >=> fun next ctx -> task { - let! req = (db ctx).TryCompleteRequestById reqId (userId ctx) - match req with - | Some r -> return! json r next ctx + match! (db ctx).TryCompleteRequestById reqId (userId ctx) with + | Some req -> return! json req next ctx | None -> return! Error.notFound next ctx } @@ -228,9 +251,8 @@ module Request = authorize >=> fun next ctx -> task { - let! req = (db ctx).TryFullRequestById reqId (userId ctx) - match req with - | Some r -> return! json r next ctx + match! (db ctx).TryFullRequestById reqId (userId ctx) with + | Some req -> return! json req next ctx | None -> return! Error.notFound next ctx } @@ -248,12 +270,11 @@ module Request = authorize >=> fun next ctx -> task { - let db = db ctx - let! req = db.TryRequestById reqId (userId ctx) - match req with - | Some r -> + let db = db ctx + match! db.TryRequestById reqId (userId ctx) with + | Some req -> let! until = ctx.BindJsonAsync () - { r with snoozedUntil = until.until } + { req with snoozedUntil = until.until; showAfter = until.until } |> db.UpdateEntry let! _ = db.SaveChangesAsync () return! setStatusCode 204 next ctx diff --git a/src/api/MyPrayerJournal.Api/Program.fs b/src/api/MyPrayerJournal.Api/Program.fs index 9aa0ae7..a50e48a 100644 --- a/src/api/MyPrayerJournal.Api/Program.fs +++ b/src/api/MyPrayerJournal.Api/Program.fs @@ -53,7 +53,7 @@ module Configure = /// Routes for the available URLs within myPrayerJournal let webApp = router Handlers.Error.notFound [ - route "/" (htmlFile "wwwroot/index.html") + route "/" Handlers.Vue.app subRoute "/api/" [ GET [ route "journal" Handlers.Journal.journal diff --git a/src/sql/16-recurrence.sql b/src/sql/16-recurrence.sql new file mode 100644 index 0000000..33d05d6 --- /dev/null +++ b/src/sql/16-recurrence.sql @@ -0,0 +1,31 @@ +ALTER TABLE mpj.request + ADD COLUMN "showAfter" BIGINT NOT NULL DEFAULT 0; +ALTER TABLE mpj.request + ADD COLUMN "recurType" VARCHAR(10) NOT NULL DEFAULT 'immediate'; +ALTER TABLE mpj.request + ADD COLUMN "recurCount" SMALLINT NOT NULL DEFAULT 0; +CREATE OR REPLACE VIEW mpj.journal AS + SELECT + request."requestId", + request."userId", + (SELECT "text" + FROM mpj.history + WHERE history."requestId" = request."requestId" + AND "text" IS NOT NULL + ORDER BY "asOf" DESC + LIMIT 1) AS "text", + (SELECT "asOf" + FROM mpj.history + WHERE history."requestId" = request."requestId" + ORDER BY "asOf" DESC + LIMIT 1) AS "asOf", + (SELECT "status" + FROM mpj.history + WHERE history."requestId" = request."requestId" + ORDER BY "asOf" DESC + LIMIT 1) AS "lastStatus", + request."snoozedUntil", + request."showAfter", + request."recurType", + request."recurCount" + FROM mpj.request; \ No newline at end of file