Added recurrence SQL; updated API
API should support recurrence (#16); also updated for new F# match! statement
This commit is contained in:
parent
2bf3bc4865
commit
96f2f2f7e0
@ -88,7 +88,7 @@ module Entities =
|
|||||||
m.Property(fun e -> e.notes).IsRequired () |> ignore)
|
m.Property(fun e -> e.notes).IsRequired () |> ignore)
|
||||||
|> ignore
|
|> ignore
|
||||||
|
|
||||||
// Request is the identifying record for a prayer request.
|
/// Request is the identifying record for a prayer request
|
||||||
and [<CLIMutable; NoComparison; NoEquality>] Request =
|
and [<CLIMutable; NoComparison; NoEquality>] Request =
|
||||||
{ /// The ID of the request
|
{ /// The ID of the request
|
||||||
requestId : RequestId
|
requestId : RequestId
|
||||||
@ -96,8 +96,14 @@ module Entities =
|
|||||||
enteredOn : int64
|
enteredOn : int64
|
||||||
/// The ID of the user to whom this request belongs ("sub" from the JWT)
|
/// The ID of the user to whom this request belongs ("sub" from the JWT)
|
||||||
userId : string
|
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
|
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
|
/// The history entries for this request
|
||||||
history : ICollection<History>
|
history : ICollection<History>
|
||||||
/// The notes for this request
|
/// The notes for this request
|
||||||
@ -110,6 +116,9 @@ module Entities =
|
|||||||
enteredOn = 0L
|
enteredOn = 0L
|
||||||
userId = ""
|
userId = ""
|
||||||
snoozedUntil = 0L
|
snoozedUntil = 0L
|
||||||
|
showAfter = 0L
|
||||||
|
recurType = "immediate"
|
||||||
|
recurCount = 0s
|
||||||
history = List<History> ()
|
history = List<History> ()
|
||||||
notes = List<Note> ()
|
notes = List<Note> ()
|
||||||
}
|
}
|
||||||
@ -123,6 +132,9 @@ module Entities =
|
|||||||
m.Property(fun e -> e.enteredOn).IsRequired () |> ignore
|
m.Property(fun e -> e.enteredOn).IsRequired () |> ignore
|
||||||
m.Property(fun e -> e.userId).IsRequired () |> ignore
|
m.Property(fun e -> e.userId).IsRequired () |> ignore
|
||||||
m.Property(fun e -> e.snoozedUntil).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<History>)
|
m.HasMany(fun e -> e.history :> IEnumerable<History>)
|
||||||
.WithOne()
|
.WithOne()
|
||||||
.HasForeignKey(fun e -> e.requestId :> obj)
|
.HasForeignKey(fun e -> e.requestId :> obj)
|
||||||
@ -149,6 +161,12 @@ module Entities =
|
|||||||
lastStatus : string
|
lastStatus : string
|
||||||
/// The time that this request should reappear in the user's journal
|
/// The time that this request should reappear in the user's journal
|
||||||
snoozedUntil : int64
|
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 entries for the request
|
||||||
history : History list
|
history : History list
|
||||||
/// Note entries for the request
|
/// Note entries for the request
|
||||||
@ -236,8 +254,7 @@ type AppDbContext (opts : DbContextOptions<AppDbContext>) =
|
|||||||
/// Retrieve notes for a request by its ID and user ID
|
/// Retrieve notes for a request by its ID and user ID
|
||||||
member this.NotesById reqId userId =
|
member this.NotesById reqId userId =
|
||||||
task {
|
task {
|
||||||
let! req = this.TryRequestById reqId userId
|
match! this.TryRequestById reqId userId with
|
||||||
match req with
|
|
||||||
| Some _ -> return this.Notes.AsNoTracking().Where(fun n -> n.requestId = reqId) |> List.ofSeq
|
| Some _ -> return this.Notes.AsNoTracking().Where(fun n -> n.requestId = reqId) |> List.ofSeq
|
||||||
| None -> return []
|
| None -> return []
|
||||||
}
|
}
|
||||||
@ -252,16 +269,15 @@ type AppDbContext (opts : DbContextOptions<AppDbContext>) =
|
|||||||
/// Retrieve a request, including its history and notes, by its ID and user ID
|
/// Retrieve a request, including its history and notes, by its ID and user ID
|
||||||
member this.TryCompleteRequestById requestId userId =
|
member this.TryCompleteRequestById requestId userId =
|
||||||
task {
|
task {
|
||||||
let! req = this.TryJournalById requestId userId
|
match! this.TryJournalById requestId userId with
|
||||||
match req with
|
| Some req ->
|
||||||
| Some r ->
|
|
||||||
let! fullReq =
|
let! fullReq =
|
||||||
this.Requests.AsNoTracking()
|
this.Requests.AsNoTracking()
|
||||||
.Include(fun r -> r.history)
|
.Include(fun r -> r.history)
|
||||||
.Include(fun r -> r.notes)
|
.Include(fun r -> r.notes)
|
||||||
.FirstOrDefaultAsync(fun r -> r.requestId = requestId && r.userId = userId)
|
.FirstOrDefaultAsync(fun r -> r.requestId = requestId && r.userId = userId)
|
||||||
match toOption fullReq with
|
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
|
||||||
| None -> return None
|
| None -> return None
|
||||||
}
|
}
|
||||||
@ -269,15 +285,14 @@ type AppDbContext (opts : DbContextOptions<AppDbContext>) =
|
|||||||
/// Retrieve a request, including its history, by its ID and user ID
|
/// Retrieve a request, including its history, by its ID and user ID
|
||||||
member this.TryFullRequestById requestId userId =
|
member this.TryFullRequestById requestId userId =
|
||||||
task {
|
task {
|
||||||
let! req = this.TryJournalById requestId userId
|
match! this.TryJournalById requestId userId with
|
||||||
match req with
|
| Some req ->
|
||||||
| Some r ->
|
|
||||||
let! fullReq =
|
let! fullReq =
|
||||||
this.Requests.AsNoTracking()
|
this.Requests.AsNoTracking()
|
||||||
.Include(fun r -> r.history)
|
.Include(fun r -> r.history)
|
||||||
.FirstOrDefaultAsync(fun r -> r.requestId = requestId && r.userId = userId)
|
.FirstOrDefaultAsync(fun r -> r.requestId = requestId && r.userId = userId)
|
||||||
match toOption fullReq with
|
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
|
||||||
| None -> return None
|
| None -> return None
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,14 @@ open Giraffe
|
|||||||
open MyPrayerJournal
|
open MyPrayerJournal
|
||||||
open System
|
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 =
|
module Error =
|
||||||
|
|
||||||
open Microsoft.Extensions.Logging
|
open Microsoft.Extensions.Logging
|
||||||
@ -23,7 +31,7 @@ module Error =
|
|||||||
|> List.length
|
|> List.length
|
||||||
|> function
|
|> function
|
||||||
| 0 -> (setStatusCode 404 >=> json ([ "error", "not found" ] |> dict)) next ctx
|
| 0 -> (setStatusCode 404 >=> json ([ "error", "not found" ] |> dict)) next ctx
|
||||||
| _ -> htmlFile "wwwroot/index.html" next ctx
|
| _ -> Vue.app next ctx
|
||||||
|
|
||||||
|
|
||||||
/// Handler helpers
|
/// Handler helpers
|
||||||
@ -92,6 +100,10 @@ module Models =
|
|||||||
type Request =
|
type Request =
|
||||||
{ /// The text of the request
|
{ /// The text of the request
|
||||||
requestText : string
|
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
|
/// The time until which a request should not appear in the journal
|
||||||
@ -119,6 +131,15 @@ module Request =
|
|||||||
|
|
||||||
open NCuid
|
open NCuid
|
||||||
|
|
||||||
|
/// Ticks per recurrence
|
||||||
|
let private recurrence =
|
||||||
|
[ "immediate", 0L
|
||||||
|
"hours", 3600000L
|
||||||
|
"days", 86400000L
|
||||||
|
"weeks", 604800000L
|
||||||
|
]
|
||||||
|
|> Map.ofList
|
||||||
|
|
||||||
/// POST /api/request
|
/// POST /api/request
|
||||||
let add : HttpHandler =
|
let add : HttpHandler =
|
||||||
authorize
|
authorize
|
||||||
@ -130,10 +151,12 @@ module Request =
|
|||||||
let usrId = userId ctx
|
let usrId = userId ctx
|
||||||
let now = jsNow ()
|
let now = jsNow ()
|
||||||
{ Request.empty with
|
{ Request.empty with
|
||||||
requestId = reqId
|
requestId = reqId
|
||||||
userId = usrId
|
userId = usrId
|
||||||
enteredOn = now
|
enteredOn = now
|
||||||
snoozedUntil = 0L
|
showAfter = now
|
||||||
|
recurType = r.recurType
|
||||||
|
recurCount = defaultArg r.recurCount 0s
|
||||||
}
|
}
|
||||||
|> db.AddEntry
|
|> db.AddEntry
|
||||||
{ History.empty with
|
{ History.empty with
|
||||||
@ -144,9 +167,8 @@ module Request =
|
|||||||
}
|
}
|
||||||
|> db.AddEntry
|
|> db.AddEntry
|
||||||
let! _ = db.SaveChangesAsync ()
|
let! _ = db.SaveChangesAsync ()
|
||||||
let! req = db.TryJournalById reqId usrId
|
match! db.TryJournalById reqId usrId with
|
||||||
match req with
|
| Some req -> return! (setStatusCode 201 >=> json req) next ctx
|
||||||
| Some rqst -> return! (setStatusCode 201 >=> json rqst) next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,18 +177,22 @@ module Request =
|
|||||||
authorize
|
authorize
|
||||||
>=> fun next ctx ->
|
>=> fun next ctx ->
|
||||||
task {
|
task {
|
||||||
let db = db ctx
|
let db = db ctx
|
||||||
let! req = db.TryRequestById reqId (userId ctx)
|
match! db.TryRequestById reqId (userId ctx) with
|
||||||
match req with
|
| Some req ->
|
||||||
| Some _ ->
|
|
||||||
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
|
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
|
||||||
|
let now = jsNow ()
|
||||||
{ History.empty with
|
{ History.empty with
|
||||||
requestId = reqId
|
requestId = reqId
|
||||||
asOf = jsNow ()
|
asOf = now
|
||||||
status = hist.status
|
status = hist.status
|
||||||
text = match hist.updateText with null | "" -> None | x -> Some x
|
text = match hist.updateText with null | "" -> None | x -> Some x
|
||||||
}
|
}
|
||||||
|> db.AddEntry
|
|> db.AddEntry
|
||||||
|
match hist.status with
|
||||||
|
| "Prayed" ->
|
||||||
|
db.UpdateEntry { req with showAfter = now + (recurrence.[req.recurType] * int64 req.recurCount) }
|
||||||
|
| _ -> ()
|
||||||
let! _ = db.SaveChangesAsync ()
|
let! _ = db.SaveChangesAsync ()
|
||||||
return! created next ctx
|
return! created next ctx
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
@ -177,15 +203,14 @@ module Request =
|
|||||||
authorize
|
authorize
|
||||||
>=> fun next ctx ->
|
>=> fun next ctx ->
|
||||||
task {
|
task {
|
||||||
let db = db ctx
|
let db = db ctx
|
||||||
let! req = db.TryRequestById reqId (userId ctx)
|
match! db.TryRequestById reqId (userId ctx) with
|
||||||
match req with
|
|
||||||
| Some _ ->
|
| Some _ ->
|
||||||
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
|
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
|
||||||
{ Note.empty with
|
{ Note.empty with
|
||||||
requestId = reqId
|
requestId = reqId
|
||||||
asOf = jsNow ()
|
asOf = jsNow ()
|
||||||
notes = notes.notes
|
notes = notes.notes
|
||||||
}
|
}
|
||||||
|> db.AddEntry
|
|> db.AddEntry
|
||||||
let! _ = db.SaveChangesAsync ()
|
let! _ = db.SaveChangesAsync ()
|
||||||
@ -206,9 +231,8 @@ module Request =
|
|||||||
authorize
|
authorize
|
||||||
>=> fun next ctx ->
|
>=> fun next ctx ->
|
||||||
task {
|
task {
|
||||||
let! req = (db ctx).TryJournalById reqId (userId ctx)
|
match! (db ctx).TryJournalById reqId (userId ctx) with
|
||||||
match req with
|
| Some req -> return! json req next ctx
|
||||||
| Some r -> return! json r next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,9 +241,8 @@ module Request =
|
|||||||
authorize
|
authorize
|
||||||
>=> fun next ctx ->
|
>=> fun next ctx ->
|
||||||
task {
|
task {
|
||||||
let! req = (db ctx).TryCompleteRequestById reqId (userId ctx)
|
match! (db ctx).TryCompleteRequestById reqId (userId ctx) with
|
||||||
match req with
|
| Some req -> return! json req next ctx
|
||||||
| Some r -> return! json r next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,9 +251,8 @@ module Request =
|
|||||||
authorize
|
authorize
|
||||||
>=> fun next ctx ->
|
>=> fun next ctx ->
|
||||||
task {
|
task {
|
||||||
let! req = (db ctx).TryFullRequestById reqId (userId ctx)
|
match! (db ctx).TryFullRequestById reqId (userId ctx) with
|
||||||
match req with
|
| Some req -> return! json req next ctx
|
||||||
| Some r -> return! json r next ctx
|
|
||||||
| None -> return! Error.notFound next ctx
|
| None -> return! Error.notFound next ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,12 +270,11 @@ module Request =
|
|||||||
authorize
|
authorize
|
||||||
>=> fun next ctx ->
|
>=> fun next ctx ->
|
||||||
task {
|
task {
|
||||||
let db = db ctx
|
let db = db ctx
|
||||||
let! req = db.TryRequestById reqId (userId ctx)
|
match! db.TryRequestById reqId (userId ctx) with
|
||||||
match req with
|
| Some req ->
|
||||||
| Some r ->
|
|
||||||
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
|
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
|
||||||
{ r with snoozedUntil = until.until }
|
{ req with snoozedUntil = until.until; showAfter = until.until }
|
||||||
|> db.UpdateEntry
|
|> db.UpdateEntry
|
||||||
let! _ = db.SaveChangesAsync ()
|
let! _ = db.SaveChangesAsync ()
|
||||||
return! setStatusCode 204 next ctx
|
return! setStatusCode 204 next ctx
|
||||||
|
@ -53,7 +53,7 @@ module Configure =
|
|||||||
/// Routes for the available URLs within myPrayerJournal
|
/// Routes for the available URLs within myPrayerJournal
|
||||||
let webApp =
|
let webApp =
|
||||||
router Handlers.Error.notFound [
|
router Handlers.Error.notFound [
|
||||||
route "/" (htmlFile "wwwroot/index.html")
|
route "/" Handlers.Vue.app
|
||||||
subRoute "/api/" [
|
subRoute "/api/" [
|
||||||
GET [
|
GET [
|
||||||
route "journal" Handlers.Journal.journal
|
route "journal" Handlers.Journal.journal
|
||||||
|
31
src/sql/16-recurrence.sql
Normal file
31
src/sql/16-recurrence.sql
Normal file
@ -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;
|
Loading…
Reference in New Issue
Block a user